OTP gating & emergency stop
TOTP gating for sensitive actions and the emergency-stop levels with OTP-gated resume.
Revka gives you two complementary controls for high-stakes agent actions. OTP gating ([security.otp]) is a TOTP-based second factor configured to gate sensitive tools and domains — in the current implementation, OTP validation is wired into emergency-stop resume. Emergency stop ([security.estop]) is the kill switch: it lets you persist a stop across restarts, and (when you want it) requires that same OTP to resume.
Use OTP gating when you want the agent to act autonomously most of the time but pause for human confirmation on a defined set of actions (shell, file writes, banking domains). Use emergency stop when something has gone wrong — or might — and you need to halt the agent now and unfreeze it deliberately.
Both features are off by default. This page covers enrollment, configuration, the four estop levels, the OTP-gated resume flow, and the fail-closed design that backs it all.
TOTP OTP gating
Section titled “TOTP OTP gating”OTP gating uses standard TOTP (RFC 6238) codes — the same kind any authenticator app generates — so any TOTP app (Google Authenticator, 1Password, Aegis, etc.) works as the second factor. In the current implementation, OTP validation is wired into emergency-stop resume (see OTP-gated resume). The gated_actions, gated_domains, and gated_domain_categories config keys are parsed and validated at startup but do not yet gate tool or domain access at runtime.
How it works
Section titled “How it works”- A 20-byte secret is generated on first use, base32-encoded, encrypted via the secret store, and written to
~/.revka/otp-secretwith0600permissions. - Codes are 6 digits. Validation accepts the current 30-second step plus one step on either side (±1) to tolerate clock drift between Revka and your phone.
- A successful code is cached for
cache_valid_secsso a single code can authorize a short burst of gated actions without re-prompting on every call. Expired cache entries are pruned on the next validation. - The secret persists, so a daemon restart does not require re-enrolling.
Enroll a device
Section titled “Enroll a device”When you enable OTP and no secret file exists yet, Revka generates one and emits an otpauth:// enrollment URI once. Scan it into your authenticator app.
-
Enable OTP in
~/.revka/config.toml:[security.otp]enabled = true -
Start the daemon. On first use Revka prints an enrollment URI of the form:
otpauth://totp/Revka:revka?secret=BASE32SECRET&issuer=Revka&period=30 -
Scan the URI (or its QR code) into your TOTP app. The app now produces a fresh 6-digit code every
token_ttl_secsseconds.
[security.otp] configuration
Section titled “[security.otp] configuration”[security.otp]enabled = truemethod = "totp"token_ttl_secs = 30cache_valid_secs = 300gated_actions = ["shell", "file_write", "browser_open", "browser"]gated_domains = ["*.chase.com", "accounts.google.com"]gated_domain_categories = ["banking"]challenge_max_attempts = 3| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | false | Master switch for OTP gating. |
method | string | "totp" | OTP method. Only totp is implemented today; pairing and cli-prompt are reserved for future use. |
token_ttl_secs | u64 | 30 | TOTP time-step (period) in seconds. Must be greater than 0. |
cache_valid_secs | u64 | 300 | How long a validated code stays accepted before a new one is required. Must be ≥ token_ttl_secs. |
gated_actions | list | ["shell", "file_write", "browser_open", "browser"] | Tool/action names that require an OTP. Each entry must be alphanumeric plus _ or -. |
gated_domains | list | [] | Explicit domain patterns that require an OTP. Supports * wildcards (e.g. *.chase.com). |
gated_domain_categories | list | [] | Preset category names expanded into gated domains: banking, medical, government, identity_providers. |
challenge_max_attempts | u32 | 3 | Maximum failed OTP attempts before the challenge is locked out. Must be greater than 0. |
Gated domain categories
Section titled “Gated domain categories”Instead of listing every sensitive domain by hand, set gated_domain_categories to one or more presets and Revka expands them into the gated-domain set:
| Category | Covers |
|---|---|
banking | Banking and financial-institution domains |
medical | Healthcare and medical-record domains |
government | Government service domains |
identity_providers | Identity-provider / SSO login domains |
Categories and explicit gated_domains are additive — use both. Unknown category names are rejected at startup.
Emergency stop (Estop)
Section titled “Emergency stop (Estop)”Emergency stop is the operator’s kill switch. Engaging it writes a persisted state file that survives daemon restarts until you explicitly resume. In the current implementation, the persisted state is visible via revka estop status but is not yet consulted by the agent loop, tool-execution path, or network layer before acting. It is the first thing to reach for in a runaway-agent situation.
The four estop levels
Section titled “The four estop levels”Estop is not all-or-nothing. You choose how much to freeze, and the levels compose — engaging several at once layers their restrictions, and you can lift them one at a time.
| Level | CLI level name | Intended effect (state persisted; runtime enforcement not yet implemented) |
|---|---|---|
| Kill all | kill-all | Intended to halt all agent execution. |
| Network kill | network-kill | Intended to block all outbound network access. |
| Domain block | domain-block | Intended to block specific domains. Supports wildcard patterns (e.g. *.chase.com). |
| Tool freeze | tool-freeze | Intended to freeze specific tools by name. |
Blocked-domain and frozen-tool lists are normalized on write (lowercased, trimmed, de-duplicated, and sorted). Domain patterns are validated the same way as elsewhere in the policy engine; tool names must be alphanumeric plus _ or -.
Engage and resume from the CLI
Section titled “Engage and resume from the CLI”# Engage (kill-all is the default level)revka estoprevka estop --level network-killrevka estop --level domain-block --domain "*.chase.com"revka estop --level tool-freeze --tool shell
# Inspect current staterevka estop status
# Resume — lift specific levels, or kill-allrevka estop resume # clear kill-allrevka estop resume --network # lift network killrevka estop resume --domain "*.chase.com" # unblock a domainrevka estop resume --tool shell # unfreeze a toolrevka estop resume --otp 123456 # supply OTP when requiredBecause levels compose, lifting one leaves the others in place. In the example below, resuming the domain block still leaves the network kill engaged until you lift it explicitly.
[security.estop] configuration
Section titled “[security.estop] configuration”[security.estop]enabled = truestate_file = "~/.revka/estop-state.json"require_otp_to_resume = true| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | false | Master switch for emergency-stop controls. |
state_file | string | "~/.revka/estop-state.json" | Where the persisted estop state lives. Tilde-expanded; relative paths resolve against the config directory. Must not be empty. |
require_otp_to_resume | bool | true | When true, lifting any estop level requires a valid OTP code. |
State file format
Section titled “State file format”State is serialized as JSON and written atomically (temp file + rename, 0600 permissions on Unix):
{ "kill_all": false, "network_kill": false, "blocked_domains": ["*.chase.com"], "frozen_tools": ["shell"], "updated_at": "2026-06-18T10:00:00Z"}You normally never edit this file by hand — use revka estop so validation and normalization run. The runtime considers estop engaged whenever kill_all is set, network_kill is set, or either list is non-empty.
OTP-gated resume
Section titled “OTP-gated resume”The two features connect at resume time. When [security.estop].require_otp_to_resume = true, you cannot lift any estop level without a valid OTP code:
-
The agent runs away (or you suspect it might). You engage a stop:
Terminal window revka estop --level kill-all -
Later, you decide to resume. Without a code, resume is refused:
Terminal window revka estop resume# error: OTP code is required to resume estop state -
Read the current code from your authenticator app and supply it:
Terminal window revka estop resume --otp 123456The code is validated against the same TOTP secret used for action gating (±1 step tolerance). A wrong or empty code is rejected (
Invalid OTP code; estop resume denied) and the stop stays engaged.
Fail-closed design
Section titled “Fail-closed design”Emergency stop is built to fail safe. If the state file exists but cannot be read or parsed — corruption, truncation, a bad manual edit — Revka does not ignore it and carry on. Instead it enters a fail-closed state: kill_all = true. A corrupt kill switch is treated as “stop everything,” never as “proceed.”
- A missing state file means no stop is engaged (the normal starting condition).
- An unreadable or unparseable state file means
kill_all = true, and the fail-closed state is persisted back so the condition is explicit and visible inrevka estop status. - The fail-closed state is persisted and visible in
revka estop status. Gateway requests are not currently gated by estop state, but an operator can inspect and resume the stop explicitly.
This mirrors the broader Revka principle that ambiguous security state resolves to the safe choice. Combined with OTP-gated resume, it means a runaway or tampered-with agent cannot quietly bring itself back online.