Errors and exit codes
What a refusal carries, what each HTTP status means, and how to tell "the tool worked and the answer was no" from "the tool did not run".
A refusal, as the agent sees it
The SDKs raise a typed error rather than returning a status, because a wrapped tool has to interrupt the call. Carrying structure rather than a string is what lets an agent re-plan instead of retrying the same thing.
{ "name": "CirvixDenied", "policy": "deny-dotenv-read", "decisionId": "dec_01JQ8F2K7M", "reason": "Reading .env files is denied outside an approved secrets flow.", "remediation": "Request the value as a handle: secrets.get(\"STRIPE_KEY\")", "appealable": false, "resource": "/repo/.env.production", "action": "fs.read"}CirvixHeld extends it with approvers and approvalId, and is always appealable. Catch it first — it is a subclass, and a broad clause that swallows it teaches the agent that a call a person was about to approve is a failure.
Three fields, three questions
policy is which rule decided it — the name you would search for in your rule set. reason is the human sentence, for a person reading a log, and is not meant to be parsed. remediation is the legitimate path, and is frequently the field that turns a dead end into a next step.
A refusal, as the MCP client sees it
The gateway returns a denial as a **tool result**, not a transport error. A transport error tells the agent the plumbing broke, and it retries; a tool result carrying a refusal and a remediation tells it what happened and what to do instead.
HTTP statuses
| Status | Meaning |
|---|---|
200 / 201 / 204 | Success |
400 | Malformed body, or a validation failure |
401 | Missing or invalid credentials |
403 | Authenticated, but the role is insufficient. The body carries required |
404 | No such route, or no such resource in this tenant |
409 | Conflict — a stale version, or an unmet governance precondition |
413 | Body over 1 MiB |
429 | Rate limited. The body carries retryInSeconds |
503 | A subsystem is not configured — e.g. secrets without CIRVIX_MASTER_KEY |
Statuses worth recognising
| Situation | Status | What it means |
|---|---|---|
| Wrong role | 403 | { error: "Role \"member\" cannot policy:write.", required: "admin" } |
| Refresh token replayed | 401 | reuseDetected: true — the session family was revoked on the assumption of theft |
| Draft edited after approval | 409 | Approvals are bound to a content hash; editing un-approves |
| Reviewer requested changes | 409 | One reviewer's objection is not outvoted by later approvals |
| Handle spent off-path | 403 | Recorded as destination_denied, raises a high-severity alert |
| Directory removing the last owner | 409 | Refused so a misconfiguration cannot lock a tenant out of itself |
CLI exit codes
| Code | Meaning |
|---|---|
0 | Success, or a permitted/held decision |
1 | A substantive negative: findings over the gate, a deny, a broken chain, a changed replay |
2 | Usage error, or an unhandled exception |
1 and 2 are kept distinct so CI can tell "the tool worked and the answer was no" from "the tool did not run". A gate that treats those the same passes when the binary is missing.
