Skip to content

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.

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.

  • A 20-byte secret is generated on first use, base32-encoded, encrypted via the secret store, and written to ~/.revka/otp-secret with 0600 permissions.
  • 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_secs so 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.

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.

  1. Enable OTP in ~/.revka/config.toml:

    [security.otp]
    enabled = true
  2. Start the daemon. On first use Revka prints an enrollment URI of the form:

    otpauth://totp/Revka:revka?secret=BASE32SECRET&issuer=Revka&period=30
  3. Scan the URI (or its QR code) into your TOTP app. The app now produces a fresh 6-digit code every token_ttl_secs seconds.

[security.otp]
enabled = true
method = "totp"
token_ttl_secs = 30
cache_valid_secs = 300
gated_actions = ["shell", "file_write", "browser_open", "browser"]
gated_domains = ["*.chase.com", "accounts.google.com"]
gated_domain_categories = ["banking"]
challenge_max_attempts = 3
KeyTypeDefaultMeaning
enabledboolfalseMaster switch for OTP gating.
methodstring"totp"OTP method. Only totp is implemented today; pairing and cli-prompt are reserved for future use.
token_ttl_secsu6430TOTP time-step (period) in seconds. Must be greater than 0.
cache_valid_secsu64300How long a validated code stays accepted before a new one is required. Must be ≥ token_ttl_secs.
gated_actionslist["shell", "file_write", "browser_open", "browser"]Tool/action names that require an OTP. Each entry must be alphanumeric plus _ or -.
gated_domainslist[]Explicit domain patterns that require an OTP. Supports * wildcards (e.g. *.chase.com).
gated_domain_categorieslist[]Preset category names expanded into gated domains: banking, medical, government, identity_providers.
challenge_max_attemptsu323Maximum failed OTP attempts before the challenge is locked out. Must be greater than 0.

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:

CategoryCovers
bankingBanking and financial-institution domains
medicalHealthcare and medical-record domains
governmentGovernment service domains
identity_providersIdentity-provider / SSO login domains

Categories and explicit gated_domains are additive — use both. Unknown category names are rejected at startup.

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.

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.

LevelCLI level nameIntended effect (state persisted; runtime enforcement not yet implemented)
Kill allkill-allIntended to halt all agent execution.
Network killnetwork-killIntended to block all outbound network access.
Domain blockdomain-blockIntended to block specific domains. Supports wildcard patterns (e.g. *.chase.com).
Tool freezetool-freezeIntended 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 -.

Terminal window
# Engage (kill-all is the default level)
revka estop
revka estop --level network-kill
revka estop --level domain-block --domain "*.chase.com"
revka estop --level tool-freeze --tool shell
# Inspect current state
revka estop status
# Resume — lift specific levels, or kill-all
revka estop resume # clear kill-all
revka estop resume --network # lift network kill
revka estop resume --domain "*.chase.com" # unblock a domain
revka estop resume --tool shell # unfreeze a tool
revka estop resume --otp 123456 # supply OTP when required

Because 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]
enabled = true
state_file = "~/.revka/estop-state.json"
require_otp_to_resume = true
KeyTypeDefaultMeaning
enabledboolfalseMaster switch for emergency-stop controls.
state_filestring"~/.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_resumebooltrueWhen true, lifting any estop level requires a valid OTP code.

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.

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:

  1. The agent runs away (or you suspect it might). You engage a stop:

    Terminal window
    revka estop --level kill-all
  2. 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
  3. Read the current code from your authenticator app and supply it:

    Terminal window
    revka estop resume --otp 123456

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

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 in revka 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.