raxit
Live demos

Human approval loop

A real defer → approve → permit run: a policy-deferred refund waits for a human, an operator approves it, and the agent resumes to permit — with the signed audit chain verified.

refund_charge matches an effect: review rule, so the engine returns defer instead of running it. A separate operator client (holding a Bearer token the agent never has) approves the pending request; the agent re-issues the same call with the approval token and the engine upgrades it to permit. Every decision lands in a hash-chained, Ed25519-signed Decision Provenance log before its verdict returns.

Run it yourself

From a clone of the repo — this builds a local engine, spawns raxit serve, runs the demo, and tears everything down:

make -C examples demo-approval-loop-py     # Python (the transcript below)
make -C examples demo-approval-loop-ts     # TypeScript, identical shape

In your own project, the same flow is available from the published SDK (pip install raxit / npm install @raxitlabs/raxit) — the whole integration is three calls:

from raxit import RaxitClient

agent = RaxitClient(agent_id="approval-bot")
operator = RaxitClient(agent_id="approval-bot", operator_token="<operator bearer token>")

decision = agent.govern.decide("refund_charge", {"amount_cents": 5000})   # -> defer.policy
record = operator.approvals.approve(decision.denial_token, approver="ops-oncall", reason="ok")
resumed = agent.govern.decide("refund_charge", {"amount_cents": 5000},
                               approval_token=decision.denial_token)       # -> permit.approved

The run, step by step

Every terminal line below is from a real captured run of examples/capabilities/approval-loop — nothing is mocked, and the approval token (dt_f99b7cbf2418437d) is the actual one-time token that run minted:

Human approval loop — defer → approve → permit

Two rules, default block. read_ticket is a plain allow; refund_charge is policy-deferred (`effect: review`) — it needs a human before it can run.

.raxit/security.yaml
apiVersion: raxit.security/v1
agent: approval-bot
default: block
rules:
  - effect: allow
    resource: read_ticket
  - effect: review              # review == defer (needs human approval)
    resource: refund_charge
terminal
$ $ make -C examples demo-approval-loop-py
raxit approval-loop · agent=approval-bot · daemon=http://127.0.0.1:8291

How it works

The whole policy is two rules plus a default:

.raxit/security.yaml
apiVersion: raxit.security/v1
agent: approval-bot
default: block
rules:
  - effect: allow
    resource: read_ticket
  - effect: review              # review == defer (needs human approval)
    resource: refund_charge
  • default: block — anything not listed is denied. The agent gets exactly what the policy names, nothing ambient.
  • effect: review — the defer verb. When refund_charge matches, decide() returns effect='defer' with reason defer.policy and a one-time denial token (dt_…). The tool has not run; the token is the handle a human uses to resolve the suspension.
  • The operator is a separate principal. Approving requires the daemon's operator Bearer token (RAXIT_APPROVALS_TOKEN here; raxit approvals reads a local 0600 token file automatically). The agent client is constructed without it — an agent cannot list or approve its own pending requests.
  • Resumption is explicit and bound. The agent re-issues the same call with approval_token=<the denial token>. The engine matches the now-approved record (which pins the resource and an args_hash of the original arguments) and resolves permit with reason permit.approved. An approval for one call does not become a standing grant.
  • Audit before execute. The defer, the approval, and the resumed permit are each written to the signed Decision Provenance log before returning. The finale, operator.audit.verify(signatures=True, pubkeys=[...]), checks the hash chain and every Ed25519 signature: ok=True signatures_verified=True signed_count=3 count=3.

Blocking inline vs. explicit resume

The SDK can also block inline on a defer: decide() polls the approval once a second, printing [raxit] waiting for human approval…, until a human resolves it or RAXIT_DEFER_TIMEOUT elapses — that's the flow shown on Approvals & warrants. This demo sets RAXIT_DEFER_TIMEOUT=0 and drives the two-step resolution explicitly (decide → approve → re-decide) so the transcript is race-free and CI-deterministic. The engine-side effect — a defer resolved by a human approval — is identical either way.

What got deferred — and why

Nothing in this run was hard-denied; the point of the demo is the suspend-and-resume path. The two reason codes the run asserts on:

CallDecisionReason codeWhy
refund_charge (first)deferdefer.policyThe effect: review rule matched — the call needs a human before it may run.
refund_charge (resumed)permitpermit.approvedThe echoed approval token resolved to an approved record for the same resource and args.

The paths not taken here still fail closed: if the operator had run raxit approvals deny dt_f99b7cbf2418437d, the resumed call would resolve to deny.approval_denied; if nobody had acted before the deadline, an inline-blocking agent would see deny.approval_expired and a [raxit BLOCKED] message instead of a result. See Approvals & warrants for both terminal shapes.

  • Approvals & warrants — the inline blocking flow, the raxit approvals CLI, timeouts, and how warrants skip the per-call wait.
  • Permit / defer / deny / modify / step-up — the full decision vocabulary and the Cedar resolution chain behind it.
  • Audit — the hash-chained, signed Decision Provenance log this demo verifies at the end.

On this page