Skip to content
Browse documentation

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.

Identify the action
The one call that must never happen without a person looking at it first.
Write the hold policy
A permit whose condition is an approval, not a role.
Configure the channel
Where the request goes, and how long it waits.
Test it
Confirm the call actually pauses instead of erroring.
Handle timeout
Decide what happens when nobody answers.

From permit to hold

cirvix.policy.jsonJSON
{  "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.

Note
A 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.

TerminalShell
$ 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

Slack approval request
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-oncall
Warning
While it waits, the call is genuinely suspended — deploy-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

TerminalShell
$ 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"}'
Edit this page on GitHub
Was this page helpful?