Action Approvals
Some actions are too consequential to let an agent run unsupervised — a production refund, a DROP TABLE, a deploy. Action approvals put a human in the loop: Vultrino pauses before the action executes, and the agent never sees a result until someone signs off. The decision can be made in the admin panel, from a Telegram button, or via a link delivered by webhook/email.
What triggers an approval
An action is gated if any of these match:
- The credential is flagged:
vultrino meta set <alias> require_approval true - The request is authorized by a use token created with
--require-approval - A policy rule matches with
action = "prompt"
What the agent experiences
The flow is designed so the agent clearly understands it is waiting, not failing, and knows how to check back:
- The agent calls a tool. Instead of a result it receives an "APPROVAL REQUIRED" message containing an
approval_id. The action has not run. - The agent polls with that id — the
check_approvalMCP tool,GET /api/v1/approvals/{id}, orvultrino approval status <id> --wait. - A human approves or denies it.
- On the next poll after approval, Vultrino runs the action and returns the real result. If denied or expired, the agent is told to stop.
Execution happens lazily on that poll, so no background worker is required and the result is delivered the moment the agent next checks.
Configuration
Enable approvals and configure out-of-band notifiers under [approvals] in config.toml:
[approvals]
enabled = true
ttl_secs = 3600 # auto-expire undecided requests
public_base_url = "https://vultrino.example.com" # base for approve/deny links
[approvals.telegram] # inline Approve / Deny buttons
bot_token = "123456:ABC-DEF..."
chat_id = "987654321"
[approvals.webhook] # POST to any URL (email / Slack / ...)
url = "https://hooks.example.com/vultrino-approvals"
auth_header = "Bearer your-webhook-secret"
If approvals are enabled but no notifier is configured, decisions can still be made from the admin panel; Vultrino logs a warning that out-of-band approval is unavailable.
Out-of-band decision links
Telegram/webhook/email links carry a single-use capability token and open a confirmation page rather than deciding on load — so a link prefetch or scanner can't silently approve an action, and the admin session is never required to act on a notification.
Set
public_base_urlto an HTTPS address so these links stay confidential, and avoid running the web server atDEBUGlog level in production: request URIs (which carry the link's capability token) are logged atDEBUG.
Guarantees
- At most once. An approved action's execution is claimed atomically, so two racing polls can't both run it. A claim from a process that crashed mid-execution is auto-recovered after a timeout; a transient pre-execution failure (e.g. a plugin not yet loaded) is retried rather than marked done.
- Ownership. An agent may only poll approvals created by the same principal (API key or use token) that made the original request — checked before any execution.
- Bounded pending approvals. A use token's
uses + outstanding pending approvalscan never exceedmax_uses, enforced atomically under the vault lock — so a single-use token can't flood the approval queue (or the notifier) with requests it could never run. - Policy still applies at run time. Policy is re-evaluated when the action finally executes, so an explicit deny rule (URL / method / time-window) blocks even a human-approved action — a human approval is not a policy bypass. Rate limits are charged once, at request time; the deferred re-check never re-charges or re-denies an approved action against the rate limiter. When a deny does fire on resume, the use token is left unconsumed.
Managing approvals
vultrino approval list # pending and recent decisions
vultrino approval status <id> [--wait] # poll one approval (optionally block)
vultrino approval approve <id>
vultrino approval deny <id>
The Approvals page of the web UI shows pending requests with their requester, credential, action, and parameters, and offers Approve / Deny actions.