Requiring human approval for production
Resolve a policy to a hold instead of a permit, and route the request to a Slack channel with a timeout and an escalation path.
This walks through the deploy-bot example used elsewhere in these docs end to end: an agent that can deploy to production, gated on a fresh, named approval instead of either a blanket permit or a blanket deny.
permit whose condition is an approval, not a role.From permit to hold
{ "name": "require-approval-prod-deploy", "effect": "hold", "agents": ["deploy-bot", "platform-*"], "actions": ["k8s.apply", "fs.delete", "db.migrate"], "resources": ["*"], "when": [{ "path": "environment", "op": "in", "value": ["production", "prod"] }], "approvers": ["platform-oncall"], "reason": "Destructive or state-changing action in production. Held for a named human; the call waits rather than failing."}hold is its own effect, alongside permit and forbid. A hold rule requires an approvers array — a rule set that holds a call without naming who can release it is rejected at validation, because a call held for nobody never resumes.
hold outranks a permit. If some other rule would have permitted this call, the hold still wins — otherwise adding a permissive rule would quietly route around the person the hold exists to reach.Route it to a channel
Where a hold is announced is an organisation setting on the control plane, not a field in the rule set. Channel kinds are slack, teams, webhook, and email.
$ curl -X POST $CIRVIX_API/v1/alert-channels \ -H "authorization: Bearer $TOKEN" \ -H "content-type: application/json" \ -d '{"kind":"slack","target":"https://hooks.slack.com/services/..."}' $ # Confirm it works before relying on it:$ curl -X POST $CIRVIX_API/v1/alert-channels/ch_7Q2X/test \ -H "authorization: Bearer $TOKEN"What the approver sees
Cirvix · approval required APP Agent deploy-bot (agt_3H8N1P) Owner marcus@example.com Action k8s.apply → production/checkout Policy require-approval-prod-deploy Waiting 00:42 ── manifest diff ───────────────────────────── replicas: 3 → 5 image: checkout:1.9.2 → checkout:1.9.3 ────────────────────────────────────────────── [ Approve ] [ Deny ] [ Open in Cirvix ] Expires in 15m · escalates to #platform-oncalldeploy-bot's run stays alive with its context intact, and nothing has touched production/checkout yet.Escalation and timeout
If nobody answers inside the configured window, the call fails closed and the run continues with a denial it can re-plan around, rather than hanging indefinitely. Escalating to a second group before that happens is a channel setting, not a policy change.
Approving from a terminal instead
$ curl -X POST $CIRVIX_API/v1/approvals/apr_4H2N/decide \ -H "authorization: Bearer $TOKEN" \ -H "content-type: application/json" \ -d '{"decision":"approved","reason":"release 1.9.3 signed off"}'