Configuration

Vultrino uses a TOML configuration file located at:

  • macOS: ~/Library/Application Support/vultrino/config.toml
  • Linux: ~/.config/vultrino/config.toml
  • Windows: %APPDATA%\vultrino\config.toml

Default Configuration

# Vultrino Configuration

[server]
bind = "127.0.0.1:7878"
mode = "local"

[storage]
backend = "file"

[storage.file]
path = "~/.local/share/vultrino/credentials.enc"

[logging]
level = "info"
# audit_file = "~/.local/share/vultrino/audit.log"

[mcp]
enabled = true
transport = "stdio"

Configuration Options

Server Section

[server]
bind = "127.0.0.1:7878"  # legacy `serve` default; see note below
mode = "local"            # "local" or "server"
OptionDescriptionDefault
bindListen address for the serve subcommand. vultrino web (the HTTP JSON API + admin UI) defaults to 127.0.0.1:7879 and the --bind flag overrides per process.127.0.0.1:7878
modeDeployment mode: server sets require_auth = true on the in-process default (local otherwise).local

The JSON API and admin UI are served by vultrino web on 7879, not by this [server].bind. vultrino serve (which this key configures) no longer starts an API server on its own — see the CLI reference.

Storage Section

[storage]
backend = "file"  # Storage backend: "file", "keychain", or "vault"

[storage.file]
path = "~/.local/share/vultrino/credentials.enc"
OptionDescriptionDefault
backendStorage backend typefile
pathPath to encrypted credentials fileOS-specific

Logging Section

[logging]
level = "info"  # Log level: error, warn, info, debug, trace
# audit_file = "~/.local/share/vultrino/audit.log"  # Optional audit log
OptionDescriptionDefault
levelLogging verbosityinfo
audit_filePath to audit log (optional)disabled

MCP Section

[mcp]
enabled = true
transport = "stdio"  # "stdio" or "http"
OptionDescriptionDefault
enabledEnable MCP servertrue
transportTransport methodstdio

Enforcement Section

Controls what the policy engine decides for a credential that matches no policy at all.

[enforcement]
default_action = "deny"  # "deny" (fail-closed, default) or "allow" (fail-open)
OptionDescriptionDefault
default_actionDecision for a credential matched by no policy: deny or allowdeny

With deny (the default, and the recommended posture for shared/server deployments), an un-policied credential is denied with a distinct no_policy reason — closing the historical fail-open gap. Use allow for the legacy behavior where an un-policied credential is permitted. If the section is omitted the built-in default is deny. The config produced by vultrino init also ships with deny and prints a reminder that you must add an allow policy (or switch to allow) before credentials will work.

When default_action = "deny" and no policies are configured, every credential is denied. Vultrino logs a loud warning at startup in this case (and the symmetric allow + no-policies fail-open case is warned about too).

Upgrading (breaking change)

Before this change the engine was fail-open: a credential matching no policy was allowed. It is now fail-closed by default. A config that has no [enforcement] section will start denying un-policied credentials after upgrade. To preserve the pre-upgrade behavior, add:

[enforcement]
default_action = "allow"

Otherwise, add allow policies for the credentials your agents legitimately use.

Spend Extractors

For SpendCap policies (V3), Vultrino needs to know where the amount lives in the request body. Each extractor matches an action + credential and reads the amount (an integer in minor units, e.g. cents) from a JSON pointer, plus an asset (literal or a second pointer).

[[spend_extractors]]
action_pattern = "http.request"     # glob over plugin.action
credential_pattern = "stripe-*"     # glob over credential alias
amount_pointer = "/body/amount"     # JSON pointer to the integer amount
asset = "usd"                        # literal asset...
# asset_pointer = "/body/currency"  # ...or read it from the body

If a SpendCap policy applies to a credential but no extractor yields an amount (missing extractor or unparseable body), the request is denied (fail-closed) and a spend_unparseable warning is logged.

Egress Controls

Vultrino keeps proxied responses from carrying secrets back to the agent (V7), applied at the execution seam for every plugin:

  1. Always-on secret-material redaction. If an endpoint reflects the credential's own injected secret in its response (a header-echoing reflector, an open redirect, etc.), the secret — and its common re-encoded forms (percent-encoded, JSON-escaped) — is scrubbed from the body and headers and replaced with the constant [REDACTED] marker before the response is returned. This is not configurable. It is defense-in-depth, not absolute: an endpoint that transforms the secret (base64, hashing, splitting it) — or returns a compressed body (the http plugin requests Accept-Encoding: identity, but a server may compress anyway) — can still leak it. Use a block rule for endpoints you don't trust. Secrets shorter than 5 bytes are not scrubbed (too little entropy to match safely); a warning is logged when such a credential is created.
  2. Egress classification. For endpoints whose response is itself a secondary secret (an STS/login/secret-read endpoint), configure [[egress]] rules:
[[egress]]
credential_pattern = "sts-*"        # glob over credential alias
action_pattern = "http.request"     # glob over plugin.action (default "*")
block = true                         # withhold the body + headers entirely

[[egress]]
credential_pattern = "secrets-api-*"
redact_patterns = ['"token":\s*"[^"]+"', "AKIA[0-9A-Z]{16}"]  # extra regexes to redact

The first matching rule applies. block = true replaces the body with a marker and drops the headers; otherwise any redact_patterns (regexes) are scrubbed from the body (on top of the always-on redaction).

Downstream credentials. Blocking/redacting prevents an agent from reading a downstream secret out of a response. Deleting an OAuth2 credential that carries a revocation_url metadata key now propagates the revoke to the provider (R5/V7): Vultrino calls the RFC 7009 revocation endpoint for the credential's issued access and refresh tokens before removing it locally, so an already-issued downstream secret is actively revoked rather than left to expire, and a credential.revoked event is emitted to the signed outbox. Set it with vultrino meta set <oauth-cred> revocation_url https://idp/oauth/revoke (HTTPS required). Prefer credential types that mint short-lived, revocable downstream credentials (OAuth2 client-credentials, STS, SVIDs) so a revoke maps to a real resource-side revoke. OAuth2 in-path token rotation also emits a credential.rotated event.

Action Labels

Map a govder business verb to a canonical plugin.action (V8), so use-token scopes and the approval/audit trail can speak in business terms while vultrino executes the underlying plugin action. (Policy rules match on URL/method/credential/principal/spend — not on the action label — so the verb is a scoping and audit concept, not a policy-condition one.)

[[action_labels]]
label = "payments.refund"   # what govder / a token scopes against
action = "http.request"     # the canonical plugin.action vultrino runs

A request (or use-token action_scope) may then use the label payments.refund; it resolves to http.request for execution, the use-token scope is satisfied by either the label or the canonical action, and the approver sees the business verb in the approval. The typed /api/v1/execute endpoint also accepts an optional action field (default http.request) so it is no longer hardwired.

Event Outbox (V9)

Vultrino records security-relevant events to a durable, ordered, replayable, signed outbox: approval requested/approved/denied/escalated/expired, agent.halted, policy.changed, credential.rotated, credential.revoked (a downstream revoke propagated to the provider on delete), policy.observed_denial (an observe-only tenant's un-enforced denial), and policy.denied (an enforce-mode denial — a DETECT signal whose created_at is a per-incident detected_at that pairs, on the same subject, with the agent.halted contained_at for an MTTD/MTTC measurement). Configure push delivery with [outbox]:

[outbox]
url = "https://govder.example.com/vultrino/events"  # delivery endpoint
hmac_secret = "shared-signing-secret"                # required to push (deliveries are signed)
max_attempts = 8                                      # retries before dead-lettering (default 8)
retention_secs = 604800                               # replay window, default 7 days
  • Ordered + monotonic. Every event gets a process-global, gap-free sequence. Events for the same subject (e.g. an approval id) are delivered in order.
  • Signed. Each delivery carries Govder-Signature: sha256=<hex> = HMAC-SHA256(hmac_secret, body). A consumer recomputes it over the raw body to verify authenticity. Enabling the outbox requires both url and hmac_secret (an unsigned/undeliverable outbox is rejected at load).
  • Exactly-once-ish delivery across processes. Each event is atomically claimed (leased) under the vault lock before it is POSTed, so the web and MCP processes can't both deliver it; a failed delivery backs off (the lease holds it off the retry queue) before re-attempting, and a crashed deliverer's lease is reclaimed once stale.
  • Replayable. A consumer that drops offline replays from its last-seen sequence: GET /api/v1/events?after=<cursor> returns the next events — each as { "body": …, "signature": "sha256=…" }, the same body a push carries plus its signature — with no gaps and no dupes, within the retention window.
  • Dead-letter queue. An event that fails max_attempts deliveries is parked (GET /api/v1/events/dead) and re-queued with POST /api/v1/events/{sequence}/replay — it stops blocking its subject.
  • Events are appended even when push is unconfigured (still replayable via the API). GC prunes the oldest contiguous prefix past retention_secs (keeping the retained window gap-free); the window is the replay + dead-letter-resolution SLA.

Inbound Workload Identity (V10)

Resolve the principal vultrino evaluates from an inbound SPIFFE SVID or OIDC claims document instead of only the static vk_/vut_ id. A request carrying the configured header (an already transport-verified document — terminate mTLS / verify the token at the edge) has its principal resolved before policy evaluation.

[identity]
kind = "spiffe"                 # spiffe | oidc (the wireable resolvers)
header = "x-spiffe-verified"    # inbound header carrying the verified document
allowed = ["example.org"]       # SPIFFE trust domains (or OIDC issuers); empty = any

The resolved subject becomes the Principal.id a policy principal_pattern matches (and the SoD owner from an OIDC email/preferred_username). A malformed/untrusted document is ignored (the static principal stands). See Workload Identity.

Environment Variables

VariableDescription
VULTRINO_PASSWORDStorage encryption password (avoids prompts)
VULTRINO_CONFIGPath to config file
RUST_LOGOverride log level (e.g., vultrino=debug)

Policy Configuration

Policies control which requests are allowed for each credential:

[[policies]]
name = "github-readonly"
credential_pattern = "github-*"  # Glob pattern for credential aliases
default_action = "deny"

[[policies.rules]]
condition = { url_match = "https://api.github.com/*" }
action = "allow"

[[policies.rules]]
condition = { method_match = ["POST", "PUT", "DELETE"] }
action = "deny"

See Policy Configuration for detailed policy options.

Using a Custom Config File

vultrino --config /path/to/config.toml list

Regenerating Configuration

To reset to defaults:

vultrino init --force

Warning: This will overwrite your existing configuration and require re-entering admin credentials.