Cargo feature flags & ADRs
Every build-time feature flag and what it enables, plus the accepted and proposed architecture decision records.
Revka is one Rust binary, but not every capability is compiled into every build. Channels like Matrix and Nostr, the hardware subsystem, PDF text extraction, native browser automation, Prometheus and OpenTelemetry exporters, WASM plugins, WebAuthn, and the OS-level sandbox backends are all behind Cargo feature flags — opt-in switches you pass at build time. A minimal build stays small and dependency-light; you turn on only what you need.
Use this page when you are building Revka from source (cargo build, install.sh --force-source-build, or the Windows setup.bat/setup.ps1 modes) and need to know which flag enables which capability, or when a tool, channel, or endpoint reports that it is unavailable and you suspect the binary was built without the right feature. The second half of the page records the architecture decision records (ADRs) that govern how Revka’s tools share state and how the operator runtime is evolving.
Quick reference
Section titled “Quick reference”| Feature flag | Enables | Runtime config / surface |
|---|---|---|
channel-matrix | Matrix channel (E2EE rooms via matrix-sdk) | [channels_config.matrix] |
channel-lark | Lark / Feishu channel | [channels_config.lark] / [channels_config.feishu] |
channel-nostr | Nostr channel (NIP-04 / NIP-17) | [channels_config.nostr] |
whatsapp-web | WhatsApp Web native mode (wa-rs) | [channels_config.whatsapp] with session_path |
voice-wake | Local microphone wake-word channel | [channels_config.voice_wake] |
hardware | USB/serial board registry + Pico/Aardvark/GPIO tools | [hardware], [peripherals] |
peripheral-rpi | Native Raspberry Pi GPIO tools (Linux) | auto-detected at boot |
probe | Live STM32 chip info / memory read via probe-rs | revka hardware info, hardware_memory_read |
safety | Robot Kit SafetyMonitor + SafeDrive wrapper | crates/robot-kit robot.toml [safety] |
rag-pdf | PDF text extraction | pdf_read tool, file_read on PDFs, datasheet read |
browser-native | rust_native browser automation backend | [browser] backend = "native" |
observability-prometheus | Prometheus metrics endpoint | GET /metrics, [observability] backend = "prometheus" |
observability-otel | OpenTelemetry/OTLP trace export | [observability] backend = "otel" |
plugins-wasm | WASM plugin tools and revka plugin CLI | [plugins], ~/.revka/plugins/ |
webauthn | FIDO2 / passkey gateway auth | [security.webauthn], /api/webauthn/* |
sandbox-bubblewrap | Bubblewrap user-namespace sandbox backend | [security.sandbox] backend |
sandbox-landlock | Landlock kernel-enforced filesystem sandbox | [security.sandbox] backend |
Build profiles
Section titled “Build profiles”The installers bundle feature flags into named profiles so you do not have to memorize the list.
setup.bat --minimal :: default Cargo features only (~15 min)setup.bat --standard :: + channel-matrix, channel-lark (~20 min)setup.bat --full :: all features below (~30 min).\setup.ps1 -Mode Standard.\setup.ps1 -Mode Full--standard/-Mode Standardaddschannel-matrix,channel-lark.--full/-Mode Fulladdschannel-matrix,channel-lark,browser-native,hardware,rag-pdf,observability-otel.
# Minimal — default featurescargo build --release --locked
# Standard — workspace chat channelscargo build --release --locked --features channel-matrix,channel-lark
# Full — everything the Windows --full profile compilescargo build --release --locked \ --features channel-matrix,channel-lark,browser-native,hardware,rag-pdf,observability-otel
# Raspberry Pi, self-hosted with native GPIOcargo build --release --features hardware,peripheral-rpi
# STM32 Nucleo with live chip introspectioncargo build --release --features hardware,probeChannel feature flags
Section titled “Channel feature flags”Most channels (Telegram, Discord, Slack, Mattermost, Signal, WhatsApp Cloud API, IRC, the Asian platforms, email, webhooks, voice call) are always compiled in. These four are gated because they pull in heavy or platform-specific dependencies.
channel-matrix
Section titled “channel-matrix”Enables the Matrix channel, built on matrix-sdk, including transparent End-to-End Encryption for encrypted rooms, alias resolution, draft streaming (m.replace edits), and voice transcription. E2EE requires a correct device_id so the SDK can restore its session.
[channels_config.matrix]homeserver = "https://matrix.example.com"access_token = "syt_..."user_id = "@revka:matrix.example.com"device_id = "DEVICEID123"room_id = "!room:matrix.example.com"allowed_users = ["*"]stream_mode = "partial" # off | partial | multi_messagedraft_update_interval_ms = 1500 # higher than other channels for E2EE re-encryption overheadchannel-lark
Section titled “channel-lark”Enables the Lark / Feishu channel. Lark uses the Open Platform WebSocket long connection by default (receive_mode = "websocket", no inbound port) or an HTTP callback (receive_mode = "webhook", needs public HTTPS). tenant_access_token is cached and auto-refreshed; ACK reactions are locale-aware (zh-CN, zh-TW, en, ja).
[channels_config.lark]app_id = "cli_xxx"app_secret = "xxx"allowed_users = ["*"]receive_mode = "websocket" # websocket | webhookuse_feishu = falseFor China deployments, prefer the dedicated [channels_config.feishu] block over the legacy use_feishu = true flag.
channel-nostr
Section titled “channel-nostr”Enables the Nostr channel, a decentralized protocol supporting NIP-04 (legacy encrypted DMs) and NIP-17 (gift-wrapped private messages). Revka replies with whichever protocol the sender used; unsolicited sends default to NIP-17.
[channels_config.nostr]private_key = "nsec1..." # high-value secret — keep secrets.encrypt = truerelays = ["wss://relay.damus.io", "wss://nos.lol"]allowed_pubkeys = ["hex-or-npub-key"] # empty = deny all, "*" = allow allwhatsapp-web
Section titled “whatsapp-web”Enables native WhatsApp Web mode via the wa-rs crate (Baileys-parity: QR or pair-code linking, Signal-Protocol E2EE, groups, media, reactions, voice transcription and TTS). The WhatsApp channel negotiates its mode at runtime: setting session_path activates Web mode, while setting phone_number_id activates Cloud API mode (which needs no feature flag).
[channels_config.whatsapp]session_path = "~/.revka/whatsapp-session.db" # presence of this key selects Web modepair_phone = "15551234567" # omit for QR-code linkingallowed_numbers = ["*"]mode = "business" # or "personal"voice-wake
Section titled “voice-wake”Enables the local microphone wake-word channel. It listens continuously on the default audio device using energy-based voice-activity detection, transcribes a short window to check for the wake word, then captures and dispatches the full utterance. It uses cpal for cross-platform capture (macOS/Linux/Windows) and is output-only — send() is a no-op, so voice replies require separate TTS configuration.
[channels_config.voice_wake]wake_word = "hey revka" # case-insensitive substringsilence_timeout_ms = 2000energy_threshold = 0.01 # RMS floor for VADmax_capture_secs = 30# Also requires a [transcription] section for the STT backend.Hardware feature flags
Section titled “Hardware feature flags”The hardware subsystem is split into two layers, both gated. See the hardware quickstart for the end-to-end workflow.
hardware
Section titled “hardware”The master flag for the USB/serial device subsystem. When compiled in, the daemon auto-discovers connected boards at startup, assigns stable aliases (pico0, arduino0, nucleo0, aardvark0), and conditionally loads tools based on what is present. It enables:
- Always-loaded GPIO/serial tools (they report “no device found” gracefully when nothing is connected):
gpio_write,gpio_read,pico_flash,device_read_code,device_write_code,device_exec. - Aardvark tools (only when a Total Phase Aardvark adapter is detected):
i2c_scan,i2c_read,i2c_write,spi_transfer,gpio_aardvark,datasheet. - Board-info tools when peripheral boards are configured:
hardware_board_info,hardware_memory_map,hardware_capabilities. - CLI commands:
revka hardware discover,revka hardware introspect <path>,revka peripheral add,revka peripheral list,revka peripheral flash.
cargo build --release --features hardwarerevka hardware discover[peripherals]enabled = true
[[peripherals.boards]]board = "nucleo-f401re"transport = "serial" # serial | native | bridgepath = "/dev/ttyACM0"baud = 115200peripheral-rpi
Section titled “peripheral-rpi”Enables native Raspberry Pi GPIO when Revka runs directly on a Pi (Linux only). At boot it reads /proc/device-tree/model to confirm it is on a Pi (RPi 3B/3B+/4B/5/Zero 2W), auto-registers GPIO tools with no config entry required, writes an rpi0.md device profile, and injects board model, IP, RAM, CPU temperature, and GPIO usage rules into the system prompt. It adds: gpio_rpi_write, gpio_rpi_read, gpio_rpi_blink, and rpi_system_info.
cargo build --release --features hardware,peripheral-rpiEnables live STM32 chip introspection over USB/SWD using probe-rs — no firmware needs to be on the target. Without this feature the corresponding tools return a clear error with build instructions. It powers revka hardware info --chip nucleo-f401re and the hardware_memory_read tool, and upgrades hardware_board_info / hardware_memory_map from static datasheet data to live readings on Nucleo boards.
cargo build --release --features hardware,probecargo install probe-rs-tools --locked # the CLI crate is probe-rs-tools, not probe-rsrevka hardware info --chip nucleo-f401reSupported boards for live reads: Nucleo-F401RE and Nucleo-F411RE (RAM base 0x20000000).
safety
Section titled “safety”A feature flag of the standalone crates/robot-kit toolkit. It enables the SafetyMonitor background task and the SafeDrive wrapper, which gate every movement request through five safety layers (pre-move obstacle check, active monitoring, reactive stops, watchdog auto-stop, and a hardware E-stop override). The governing principle is “the AI can REQUEST movement, but SafetyMonitor ALLOWS it.”
cargo build --release --features safety # within crates/robot-kit[safety]min_obstacle_distance = 0.3 # meters — stop if closermax_drive_duration = 30 # auto-stop after inactivityestop_pin = 17 # optional BCM GPIO for a physical E-stop buttonSee the Robot Kit page for the full drive/sense/look/listen/speak/emote tool set.
Tooling & platform feature flags
Section titled “Tooling & platform feature flags”rag-pdf
Section titled “rag-pdf”Enables PDF text extraction. With it compiled in, the pdf_read tool extracts plain text from a workspace PDF, the file_read tool extracts text inline when handed a PDF, and the hardware datasheet tool’s read action returns extracted text instead of just a file path.
# pdf_read is config-gated by the rag-pdf compile feature{ "path": "docs/spec.pdf" }browser-native
Section titled “browser-native”Enables the rust_native backend of the browser tool, an in-process automation backend that does not require the external agent-browser CLI. Select it at runtime:
[browser]enabled = truebackend = "native" # agent_browser | native | computer_useallowed_domains = ["*"]native_headless = trueThe agent_browser (default) and computer_use backends do not need this flag; only rust_native does. See browser automation.
observability-prometheus
Section titled “observability-prometheus”Enables the Prometheus metrics endpoint. When compiled in and [observability] backend = "prometheus" is set, the gateway serves text-format metrics (request counts, latency, cost, and more).
GET /metricsContent-Type: text/plain; version=0.0.4; charset=utf-8No authentication is required for /metrics. When Prometheus is not enabled, the endpoint returns a hint comment instead of metrics.
observability-otel
Section titled “observability-otel”Enables OpenTelemetry / OTLP trace export. With the feature compiled in and the config set, spans are exported over OTLP HTTP using a blocking exporter (so spans work from non-Tokio contexts).
[observability]backend = "otel" # none | noop | log | prometheus | otel/opentelemetry/otlpotel_endpoint = "http://localhost:4318"otel_service_name = "revka"See observability & tracing for runtime trace storage and querying with revka doctor traces.
plugins-wasm
Section titled “plugins-wasm”Enables the WASM plugin system: tools loaded from WASM plugin files at startup (each manifest defines a name, description, and a single input string parameter) and the revka plugin CLI (list, install, remove, info).
[plugins]enabled = trueplugins_dir = "~/.revka/plugins"revka plugin listrevka plugin install <directory-or-url>See WASM plugins.
webauthn
Section titled “webauthn”Enables WebAuthn / FIDO2 hardware-key and passkey authentication for the gateway, as an alternative or addition to bearer tokens. Requires both the compile feature and [security.webauthn] enabled = true. Registered credentials are stored encrypted in the SecretStore.
[security.webauthn]enabled = truerp_id = "revka.example.com"rp_origin = "https://revka.example.com"rp_name = "Revka"POST /api/webauthn/register/start # body: {"user_id":"...","user_name":"..."}POST /api/webauthn/register/finishPOST /api/webauthn/auth/startPOST /api/webauthn/auth/finishGET /api/webauthn/credentials?user_id=...DELETE /api/webauthn/credentials/{id}All /api/webauthn/* endpoints require bearer auth. See TLS, rate limiting & WebAuthn.
sandbox-bubblewrap and sandbox-landlock
Section titled “sandbox-bubblewrap and sandbox-landlock”These two flags add OS-level sandbox backends that wrap command execution. The backend is selected (or auto-detected) at runtime:
[security.sandbox]enabled = truebackend = "auto" # auto | none | firejail | bubblewrap | landlock | sandbox-exec | dockersandbox-bubblewrap— compiles in the Bubblewrap backend (Linux/macOS user-namespace containers).sandbox-landlock— compiles in the Landlock backend (Linux kernel 5.13+, kernel-enforced filesystem restrictions — the strongest isolation).
The Firejail (Linux), sandbox-exec/Seatbelt (macOS), and Docker backends, plus the always-available NoopSandbox fallback, do not require a feature flag. If you request a backend that is unavailable, Revka falls back to NoopSandbox with a warning.
Architecture Decision Records
Section titled “Architecture Decision Records”ADRs are developer-facing records of significant design decisions. They are not end-user configuration, but they explain why Revka’s internals behave the way they do. Two are relevant here.
ADR-004: Tool Shared State Ownership Accepted
Section titled “ADR-004: Tool Shared State Ownership ”Accepted · 2026-03-22 · issue #4057
Revka runs as a single daemon serving many connected clients at once, and several tools already keep long-lived shared state — DelegateParentToolsHandle (parent tools for delegate agents), ChannelMapHandle (the global channel map used by the reaction tool), and CanvasStore (the Live Canvas frame store). These grew organically, so ADR-004 sets a contract for how tools own, identify, isolate, and reload shared state.
The decision, in five parts:
- Ownership — Tools MAY own long-lived shared state, but only through the proven handle pattern: wrap it in
Arc<RwLock<T>>, expose a named cloneable handle type, accept the handle at construction time, and document the concurrency contract. Tools MUST NOT use static mutable state (lazy_static!,OnceCellwith interior mutability) for per-request or per-client data. - Identity — The daemon assigns identity; tools MUST NOT mint their own client identity keys. A new opaque
ClientId(Clone + Eq + Hash + Send + Sync), generated by the gateway at connection time and passed through the execution context, replaces the current IP-address approach (which breaks behind NAT or proxies). - Lifecycle — Four explicit phases: Construction (no I/O), Registration (one-time startup validation allowed, e.g. credential checks), Execution (no blocking re-validation; use a cached fast path), and Shutdown (clean up via
Dropor an explicit method). - Isolation — Security-sensitive state (credentials, quotas, rate-limit counters, per-client authorization) and user-specific session data MUST be isolated per client by keying internal maps on
ClientId. Broadcast/display state (canvas frames, channel map) and read-only reference data MAY be shared, with optional{client_id}:{name}namespace prefixing. Per-client secrets MUST NOT live in shared structures. - Reload semantics — The daemon hashes the tool-relevant config section and, when the hash changes, signals affected tools to re-run registration-phase validation; tools MUST treat cached validation as stale when signaled. Credential rotation invalidates per-tool and per-client credential state; tool enable/disable triggers a full registry rebuild via
all_tools_with_runtime(); security-policy, workspace-directory, and provider changes each invalidate their dependent state.
The consequence is consistency and multi-tenant safety at the cost of migrating existing singletons (CanvasStore, ReactionTool) to accept ClientId. The tool registry stays immutable after startup, and SecurityPolicy stays per-agent — client isolation is orthogonal to agent-level policy.
ADR-005: Operator Liveness & Rust Migration Proposed
Section titled “ADR-005: Operator Liveness & Rust Migration ”Proposed · 2026-04-15
The Operator MCP orchestrates multi-agent workflows through a three-layer stack: a Python operator (workflow executor) → a Node.js session manager (over a Unix socket) → Claude/Codex agent subprocesses. ADR-005 documents a class of cross-boundary state bugs where the layers disagree about whether an agent is alive — zombie agents reported as running after death, recovery blindness, a 100% CPU spin loop, a tool-surface mismatch between the operator and sub-agent MCP, and empty rate-limited output being treated as success. Interim heuristic patches (a 120-second zombie-detection window, empty-output failure, etc.) shipped on 2026-04-15, but the root cause — the session manager not tracking process liveness — remains.
The proposal migrates agent lifecycle management out of Node.js and into the Rust daemon over three (optionally four) phases, while keeping the Python workflow executor for iteration speed:
- Phase 0 — liveness in Node.js (1–2 days, low risk). Add
processAlive/pumpAliveflags and alastEventAttimestamp to the session manager so the Python operator’s zombie detection becomes trivial: iflastEventAtis stale and status isrunning, the agent is dead. Additive, no migration. - Phase 1 — Rust agent process manager (1–2 weeks, medium risk). A new
src/agent/process_manager.rsspawns Claude/Codex CLIs viatokio::process::Command, detects death within ~1 second throughwaitpid(no polling), parses output into a unifiedAgentEvent, and serves the same HTTP API the Python operator already expects (/agents,/agents/:id,/agents/:id/events, …). The Node.js session manager is removed. - Phase 2 — consolidate the MCP tool surface (3–5 days, low risk). Move agent-lifecycle tools (
create_agent,wait_for_agent,send_agent_prompt,get_agent_activity,list_agents) into Rust-native tools, keep workflow tools in Python behind a single slim MCP server, and give sub-agents the full lifecycle surface automatically — eliminating the “forgot to addrun_workflowtosubagent_mcp.py” bug class. - Phase 3 — optional Rust executor (3–6 weeks, high risk). Only after Phases 1–2 are stable: port the workflow engine (parsing, ordering, checkpoints, recovery) to Rust. Recommended only if the workflow executor changes infrequently — Python’s seconds-long iteration loop is worth more than compile-time safety while step types are still evolving.
The explicit non-goals are equally important: do not rewrite everything at once, do not move Kumiho integration to Rust prematurely, do not eliminate Python entirely, and do not over-engineer the process manager (it spawns, monitors, kills, serves an HTTP API, and buffers events — nothing more).