Skip to content

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.

Feature flagEnablesRuntime config / surface
channel-matrixMatrix channel (E2EE rooms via matrix-sdk)[channels_config.matrix]
channel-larkLark / Feishu channel[channels_config.lark] / [channels_config.feishu]
channel-nostrNostr channel (NIP-04 / NIP-17)[channels_config.nostr]
whatsapp-webWhatsApp Web native mode (wa-rs)[channels_config.whatsapp] with session_path
voice-wakeLocal microphone wake-word channel[channels_config.voice_wake]
hardwareUSB/serial board registry + Pico/Aardvark/GPIO tools[hardware], [peripherals]
peripheral-rpiNative Raspberry Pi GPIO tools (Linux)auto-detected at boot
probeLive STM32 chip info / memory read via probe-rsrevka hardware info, hardware_memory_read
safetyRobot Kit SafetyMonitor + SafeDrive wrappercrates/robot-kit robot.toml [safety]
rag-pdfPDF text extractionpdf_read tool, file_read on PDFs, datasheet read
browser-nativerust_native browser automation backend[browser] backend = "native"
observability-prometheusPrometheus metrics endpointGET /metrics, [observability] backend = "prometheus"
observability-otelOpenTelemetry/OTLP trace export[observability] backend = "otel"
plugins-wasmWASM plugin tools and revka plugin CLI[plugins], ~/.revka/plugins/
webauthnFIDO2 / passkey gateway auth[security.webauthn], /api/webauthn/*
sandbox-bubblewrapBubblewrap user-namespace sandbox backend[security.sandbox] backend
sandbox-landlockLandlock kernel-enforced filesystem sandbox[security.sandbox] backend

The installers bundle feature flags into named profiles so you do not have to memorize the list.

Terminal window
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)
Terminal window
.\setup.ps1 -Mode Standard
.\setup.ps1 -Mode Full
  • --standard / -Mode Standard adds channel-matrix,channel-lark.
  • --full / -Mode Full adds channel-matrix,channel-lark,browser-native,hardware,rag-pdf,observability-otel.

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.

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_message
draft_update_interval_ms = 1500 # higher than other channels for E2EE re-encryption overhead

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 | webhook
use_feishu = false

For China deployments, prefer the dedicated [channels_config.feishu] block over the legacy use_feishu = true flag.

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 = true
relays = ["wss://relay.damus.io", "wss://nos.lol"]
allowed_pubkeys = ["hex-or-npub-key"] # empty = deny all, "*" = allow all

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 mode
pair_phone = "15551234567" # omit for QR-code linking
allowed_numbers = ["*"]
mode = "business" # or "personal"

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 substring
silence_timeout_ms = 2000
energy_threshold = 0.01 # RMS floor for VAD
max_capture_secs = 30
# Also requires a [transcription] section for the STT backend.

The hardware subsystem is split into two layers, both gated. See the hardware quickstart for the end-to-end workflow.

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.
Terminal window
cargo build --release --features hardware
revka hardware discover
[peripherals]
enabled = true
[[peripherals.boards]]
board = "nucleo-f401re"
transport = "serial" # serial | native | bridge
path = "/dev/ttyACM0"
baud = 115200

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.

Terminal window
cargo build --release --features hardware,peripheral-rpi

Enables 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.

Terminal window
cargo build --release --features hardware,probe
cargo install probe-rs-tools --locked # the CLI crate is probe-rs-tools, not probe-rs
revka hardware info --chip nucleo-f401re

Supported boards for live reads: Nucleo-F401RE and Nucleo-F411RE (RAM base 0x20000000).

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.”

Terminal window
cargo build --release --features safety # within crates/robot-kit
crates/robot-kit/robot.toml
[safety]
min_obstacle_distance = 0.3 # meters — stop if closer
max_drive_duration = 30 # auto-stop after inactivity
estop_pin = 17 # optional BCM GPIO for a physical E-stop button

See the Robot Kit page for the full drive/sense/look/listen/speak/emote tool set.

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" }

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 = true
backend = "native" # agent_browser | native | computer_use
allowed_domains = ["*"]
native_headless = true

The agent_browser (default) and computer_use backends do not need this flag; only rust_native does. See browser automation.

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 /metrics
Content-Type: text/plain; version=0.0.4; charset=utf-8

No authentication is required for /metrics. When Prometheus is not enabled, the endpoint returns a hint comment instead of metrics.

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/otlp
otel_endpoint = "http://localhost:4318"
otel_service_name = "revka"

See observability & tracing for runtime trace storage and querying with revka doctor traces.

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 = true
plugins_dir = "~/.revka/plugins"
Terminal window
revka plugin list
revka plugin install <directory-or-url>

See WASM plugins.

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 = true
rp_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/finish
POST /api/webauthn/auth/start
POST /api/webauthn/auth/finish
GET /api/webauthn/credentials?user_id=...
DELETE /api/webauthn/credentials/{id}

All /api/webauthn/* endpoints require bearer auth. See TLS, rate limiting & WebAuthn.

These two flags add OS-level sandbox backends that wrap command execution. The backend is selected (or auto-detected) at runtime:

[security.sandbox]
enabled = true
backend = "auto" # auto | none | firejail | bubblewrap | landlock | sandbox-exec | docker
  • sandbox-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.

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:

  1. 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!, OnceCell with interior mutability) for per-request or per-client data.
  2. 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).
  3. 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 Drop or an explicit method).
  4. 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.
  5. 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:

  1. Phase 0 — liveness in Node.js (1–2 days, low risk). Add processAlive/pumpAlive flags and a lastEventAt timestamp to the session manager so the Python operator’s zombie detection becomes trivial: if lastEventAt is stale and status is running, the agent is dead. Additive, no migration.
  2. Phase 1 — Rust agent process manager (1–2 weeks, medium risk). A new src/agent/process_manager.rs spawns Claude/Codex CLIs via tokio::process::Command, detects death within ~1 second through waitpid (no polling), parses output into a unified AgentEvent, and serves the same HTTP API the Python operator already expects (/agents, /agents/:id, /agents/:id/events, …). The Node.js session manager is removed.
  3. 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 add run_workflow to subagent_mcp.py” bug class.
  4. 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).