M-01
Treating “not blocked” as permission
The vocabulary is closed, but a client that sees a fourth value must refuse rather than guess. Only equality with allow does that. This is the single most important line in your integration.
Ask Anubis, then branch on an exact allow. Everything else is detail - but this page is specific about the detail, because a fail-open integration is worse than no integration at all.
Execute the tool call if and only if Anubis returned an exact, typed ALLOW.
Exact means string equality against a single value. Never write decision != "block". Never write “no error means go”.
Every example below is the real shape of the real client. None of them is pseudocode, and none contains a real credential.
from anubis import AnubisClient, AnubisError
# Reads ANUBIS_RUNTIME_CREDENTIAL and ANUBIS_BASE_URL from the
# environment. The credential carries org, agent and environment.
anubis = AnubisClient()
def refund_invoice(invoice_id: str, amount: int) -> None:
try:
decision = anubis.evaluate(
tool_name="billing.refund.create",
action=f"Refund invoice {invoice_id}",
input={"invoice_id": invoice_id, "amount": amount},
context={"correlation_id": run_id, "user": actor_email},
)
except AnubisError:
# Timeout, transport, auth, validation, persistence, unknown
# decision - every failure lands here. None of them is a yes.
raise RefundRefused("authorization unavailable")
decision.raise_for_decision() # raises unless exactly ALLOW
billing.issue_refund(invoice_id, amount) # exactly once
log.info("refund authorized",
event_id=decision.event_id,
policy_version=decision.policy_version) # immutableimport { AnubisClient, AnubisError } from "anubis-sdk";
const anubis = new AnubisClient();
export async function refundInvoice(invoiceId: string, amount: number) {
let decision;
try {
decision = await anubis.evaluate({
toolName: "billing.refund.create",
action: `Refund invoice ${invoiceId}`,
input: { invoiceId, amount },
context: { correlationId: runId },
});
} catch (err) {
if (err instanceof AnubisError) return refuse(err);
throw err;
}
decision.throwIfNotAllowed();
await billing.issueRefund(invoiceId, amount);
}curl --fail-with-body --max-time 5 \
-X POST "$ANUBIS_BASE_URL/api/runtime/evaluate" \
-H "Authorization: Bearer $ANUBIS_RUNTIME_CREDENTIAL" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $RUN_ID-step-4" \
-d '{ "tool_name": "billing.refund.create",
"action": "Refund invoice INV-88213",
"input": { "amount": 12400 } }'
# 200 with decision "allow" -> run the tool, once
# 200 with "block" or "approval_required" -> do not run it
# anything else, including a timeout -> do not run it
# Always set a timeout. Retry only with an Idempotency-Key, and
# derive that key from <run id>-<step id> rather than generating
# a fresh one per attempt.if anubis evaluate --tool "$TOOL" --arguments "$ARGS" --json > decision.json; then
run_the_tool # exit 0, and only 0, means ALLOW
else
case $? in
3) echo "blocked by policy" >&2 ;;
4) echo "waiting for a reviewer" >&2 ;;
*) echo "anubis unavailable" >&2 ;;
esac
exit 1
fi
# anubis evaluate never executes your tool. It returns a decision
# and an exit status. Branch on the status, never on the text.Placeholders only. A credential looks like anb_v1_live_<key_id>_<secret>, is shown once at issue, and belongs in your secret manager - never in source control, a client bundle or a log line.
| connect or DNS | Retry - never dispatched. |
|---|---|
| read timeout | Only with an Idempotency-Key. |
| 429 · 502 · 503 · 504 | Only with an Idempotency-Key. |
| 400 · 401 · 403 · 409 · 413 · 422 · 500 | No. |
Every accepted evaluation writes an evidence row, so a blind retry turns one tool call into several audit entries.
With a key, a repeated identical request returns the original decision with idempotent_replay: true and writes nothing. A repeated different request under the same key is a 409. Both SDKs send a per-call key by default, which covers in-process retries but not a process restart.
M-01
Treating “not blocked” as permission
The vocabulary is closed, but a client that sees a fourth value must refuse rather than guess. Only equality with allow does that. This is the single most important line in your integration.
M-02
Polling evaluate after a hold
A held decision is final. Polling floods the evidence trail and never produces a different answer for that call. Stop the run and escalate the approval_id.
M-03
Guarding late
Adapters wrap tool objects. If the agent already holds a reference to the unwrapped original, the guard is decorative. Guard at construction and never hand out the raw tool.
M-04
Caching an allow
An allow authorises the call that was evaluated, once. Reusing it for the next call - or re-deriving permission in a downstream step - puts the side effect outside the boundary.
The complete enforcement contract, SDK references, adapter details, failure matrix and the limitations register ship as Markdown inside the Anubis repository. There is no public documentation site yet - if you are evaluating Anubis, we give you repository access alongside your deployment.
Private access
That is usually the fastest way to see whether this boundary belongs in your stack.