Human approvals & Slack
Actions classified as risky — or matched by a needs_approval policy rule — don't get silently blocked. They pause and wait for a human.
The approval lifecycle
- The agent's action is paused and an approval record is created.
- If a webhook is configured, Suvra notifies your channel (Slack, PagerDuty, or any HTTP receiver).
- A reviewer approves or denies from the dashboard or the API.
- The caller re-submits the action with
meta.approval_idattached; execution proceeds only if the approval is valid and matches the action exactly.
One exception: Hermes has no async confirmation channel, so for Hermes hooks needs_approval maps to block (fail-closed) — see Agent Runtimes.
Slack in three environment variables
export SUVRA_APPROVAL_WEBHOOK_URL="https://hooks.slack.com/services/..."
export SUVRA_APPROVAL_WEBHOOK_FORMAT=slack
export SUVRA_DASHBOARD_URL="https://suvra.example.com"
With SUVRA_APPROVAL_WEBHOOK_FORMAT=slack, Suvra posts a formatted Block Kit message — agent, actor, action type, params summary, approval id, expiry — instead of a raw JSON payload. SUVRA_DASHBOARD_URL adds a "Review & decide →" link straight to the approval.
Today the Slack message is notification-only: approving or denying happens through that link into the dashboard. Interactive approve/deny buttons inside Slack are on the roadmap — always set SUVRA_DASHBOARD_URL so the link is included.
Webhook configuration
Webhooks are disabled by default; setting SUVRA_APPROVAL_WEBHOOK_URL enables them.
| Variable | Required | Default | Description |
|---|---|---|---|
SUVRA_APPROVAL_WEBHOOK_URL | Yes | — | Destination URL for delivery |
SUVRA_APPROVAL_WEBHOOK_FORMAT | No | json | json or slack |
SUVRA_APPROVAL_WEBHOOK_SECRET | No | — | Shared secret for HMAC-SHA256 signing |
SUVRA_APPROVAL_WEBHOOK_TIMEOUT | No | 5 | HTTP request timeout in seconds |
SUVRA_BASE_URL | No | — | Added to json payloads as suvra_dashboard_url |
SUVRA_DASHBOARD_URL | No | — | Added to slack messages as the review link |
Events and payload
| Event | Fires when | status |
|---|---|---|
approval.created | An action requires human approval | pending |
approval.approved | An approver approved the request | approved |
approval.denied | An approver denied the request | denied |
The default json payload:
{
"event": "approval.created",
"timestamp": "2026-05-02T23:30:00.123456+00:00",
"approval_id": "approval-abc123",
"action_type": "fs.write_file",
"action_summary": "Agent billing-processor wants to write workspace/report.csv",
"agent": "billing-processor",
"actor": "dashboard-session",
"status": "pending",
"created_at": "2026-05-02T23:29:55.000000+00:00",
"decided_at": null,
"decided_by": null,
"note": null,
"suvra_dashboard_url": "https://suvra.example.com/dashboard/approvals"
}
Every request carries Content-Type: application/json, a User-Agent of Suvra-Webhook/<version>, plus X-Suvra-Event, X-Suvra-Approval-ID, and — when a secret is configured — X-Suvra-Signature.
Verifying signatures
With SUVRA_APPROVAL_WEBHOOK_SECRET set, each request includes X-Suvra-Signature: sha256=<hex> computed over the raw body:
import hashlib
import hmac
def verify_signature(payload_bytes: bytes, secret: str, header_value: str) -> bool:
expected = hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()
received = header_value.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
Testing your webhook
curl -X POST https://your-suvra-host/control/webhooks/test \
-H "X-Suvra-Token: your-token" \
-H "Content-Type: application/json" \
-d '{"event_type": "approval.created"}'
A successful response reports delivered, the configured webhook_url, and whether signing is enabled. If no webhook URL is configured, the endpoint returns HTTP 400.
Reliability
Webhook delivery is fire-and-forget: it never blocks or delays approval operations, failures are logged but not retried, and no delivery guarantees are made. If you need guaranteed processing, poll the approvals API instead, and use the test endpoint for health-check monitoring.
Reuse integrity
An approval is bound to the exact action it was requested for. Suvra stores it keyed by actor, action type, and a normalized params_hash over the action params and full identity context (plus node_id for node-brokered approvals). Before executing, Suvra re-verifies all of it — a modified action paired with a recycled approval id is rejected, and a previously approved token is never auto-consumed without being presented again via meta.approval_id.
Expiration
Approvals may carry expires_at. Once past it, the approval surfaces as expired, execution rejects the token, and it cannot be revived — the caller must request a fresh approval. Denied and expired approvals fail closed.
Related
- Approvals dashboard — the reviewer's queue: tabs, detail drawer, approve & execute
- Policy Model — write
needs_approvalrules - Deployment — the full environment variable reference