Skip to content

Policy decision

The policy engine is a pure function: given the canonicalised APRP envelope and a policy snapshot, it returns a decision (allow / deny) and a deny code. Pure means no clocks (expiry is read from the envelope, not the wall), no I/O, no randomness. Two evaluations of the same (request, snapshot) always agree — that’s what makes audit replay possible.

What the policy receives

fn decide(request: &AprpEnvelope, snapshot: &PolicySnapshot) -> PolicyDecision

PolicySnapshot is the parsed policy file at the moment of decision, content-hashed. The hash travels in the audit event (policy_snapshot_hash) and the Passport capsule (so strict verification can confirm the decision was reproducible).

Decision shape

{
"decision": "allow",
"deny_code": null,
"deny_detail": null,
"policy_snapshot_hash": "0xe044f1...",
"matched_rules": ["risk.low", "asset.weth", "chain.sepolia"]
}

When decision: deny, deny_code is one of a closed set of domain codes (see error codes reference) and deny_detail carries human-readable context.

Snapshot versioning

A policy snapshot is the whole policy file, not just the matched rule. This is deliberate:

  • Diffing two snapshots tells you exactly what policy changed between two decisions.
  • A capsule from 2026-04-30 is verifiable in 2027 against its embedded snapshot, even if today’s policy file diverged.
  • Operators can evolve the policy without invalidating older capsules.

The cost is capsule size — embedded snapshots are typically 1-3 KB. Worth it for offline reproducibility.

Common deny codes

CodeWhen it fires
policy.deny_unknown_providersponsor not registered with this daemon
policy.budget_exceededrequest would exceed a multi-scope budget
policy.expiry_in_pastexpiry has already passed
policy.risk_class_blockedrisk class blocked by current operator policy
policy.asset_unknownasset not in the allowed list

Full list in the error codes reference.

Editing the policy

Production deployments load policy from /etc/sbo3l/policy.json. Local dev reads ~/.sbo3l/policy.json. The daemon does not auto-reload: edit, then kill -HUP <pid> (or restart) — and the new snapshot’s hash differs from the old, so subsequent capsules embed the new snapshot.

For zero-downtime policy changes in production, use rolling daemon restarts behind a load balancer. Audit chain integrity is preserved (each event records its snapshot hash).

See also