Suvra

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

  1. The agent's action is paused and an approval record is created.
  2. If a webhook is configured, Suvra notifies your channel (Slack, PagerDuty, or any HTTP receiver).
  3. A reviewer approves or denies from the dashboard or the API.
  4. The caller re-submits the action with meta.approval_id attached; 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.

VariableRequiredDefaultDescription
SUVRA_APPROVAL_WEBHOOK_URLYesDestination URL for delivery
SUVRA_APPROVAL_WEBHOOK_FORMATNojsonjson or slack
SUVRA_APPROVAL_WEBHOOK_SECRETNoShared secret for HMAC-SHA256 signing
SUVRA_APPROVAL_WEBHOOK_TIMEOUTNo5HTTP request timeout in seconds
SUVRA_BASE_URLNoAdded to json payloads as suvra_dashboard_url
SUVRA_DASHBOARD_URLNoAdded to slack messages as the review link

Events and payload

EventFires whenstatus
approval.createdAn action requires human approvalpending
approval.approvedAn approver approved the requestapproved
approval.deniedAn approver denied the requestdenied

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.