15Docs
For agents
Bounded is built to hand write access to agents. The contract an agent needs is small: deterministic failure semantics, one atomic batch primitive, and caps that hold by proof rather than by prompt. This page is that contract.
Failure semantics
Every rejection is atomic and fail-closed: nothing partial is ever applied, and the response tells you which gate fired.
| What failed | Status | What you get back | What committed |
|---|---|---|---|
| Invariant violated | 409 | invariant_violation. Full disclosure includes the invariant’s declared name and failed arithmetic; minimal disclosure returns the stable code without policy details | Nothing |
| Rule denied | 403 | policy_denied. Full disclosure includes the failed predicate trace; minimal disclosure returns a generic message plus the stable code | Nothing |
| Update/delete on a capped collection | 409 | append_only, rolling-cap collections reject history rewrites by design | Nothing |
| Policy rejected at deploy | deploy fails | A static validation error (malformed policy, impossible tier/invariant pairing, over-claimed coverage) or the server-side proof gate rejecting a blocking DISPROVED. Non-blocking advisories do not reject deploy; see Verification | Previous policy stays active |
Agent rule of thumb: 409 means the state forbids it (back off, or it will keep failing until the window moves); 403 means you may not do it (fix the caller or the payload, not the timing).
Worked example: the spend cap
With rollingSum(amount) ≤ 100 over 3600s declared on agents/$agentId/spend (see the quickstart policy), a sequence of individually plausible writes behaves like this:
$ USER_ID="$(bounded whoami --json | jq -r .id)"
$ bounded data set --path "agents/$USER_ID/spend/s1" --data '{"amount": 60}'
✓ committed # window sum: 60 / 100
$ bounded data set --path "agents/$USER_ID/spend/s2" --data '{"amount": 60}'
✗ 409 postcondition failed: invariant "spend_cap" requires
rolling sum(agents/$agentId/spend/$spendId.amount) <= 100
# 60 + 60 = 120
nothing committed
$ bounded data set --path "agents/$USER_ID/spend/s3" --data '{"amount": 40}'
✓ committed # window sum: 100 / 100
$ bounded data set --path "agents/$USER_ID/spend/s4" --data '{"amount": 1}'
✗ 409 postcondition failed: invariant "spend_cap" requires
rolling sum(agents/$agentId/spend/$spendId.amount) <= 100
# 100 + 1 = 101The second 60 is rejected even though it is identical to the first, the cap is about the window sum, not the write. The 40 lands exactly on the cap; the 1 after it fails. There is no payload an agent can construct that lands the window above 100: that is the property the prover discharged at deploy.
Atomic set-many & in-batch composition
bounded data set-many submits multiple writes as one transaction: every rule, every invariant, every hook either passes for the whole batch or the whole batch is rejected. This is what makes conserve usable, a transfer is a debit and a credit that only exist together:
# balanced: -50 here, +50 there, one atomic batch → accepted
$ cat transfer.json
[
{ "path": "accounts/alice", "document": { "balance": 50 } },
{ "path": "accounts/bob", "document": { "balance": 150 } }
]
$ bounded data set-many --from-json transfer.json
✓ committed 2 document(s) # 100+100 → 50+150, total preserved
# unbalanced: -50 / +40 → the whole batch is rejected
$ bounded data set-many --from-json bad-transfer.json
✗ 409 postcondition failed: invariant "no_minting" requires
sum(accounts/$id.balance) to be conserved
conserve(balance): write-set sum 190 != 200
nothing committed, neither document changedIn-batch composition
Rules evaluate against staged state: the rule for entry N sees the results of entries 0..N-1 via getAfter(). That turns set-many into a composition primitive, guard documents and the writes they gate travel in one atomic unit, with no TOCTOU window between check and act:
# gated/$docId create rule:
# "getAfter(/allowlist/@user.id).approved == true"
# Replace <your-user-id> below with: bounded whoami --json | jq -r .id
#
# One batch creates the allowlist entry AND the gated write.
# The rule on entry N sees entries 0..N-1, staged, atomic.
[
{ "path": "allowlist/<your-user-id>", "document": { "approved": true } },
{ "path": "gated/g1", "document": { "value": 7 } }
]
$ bounded data set-many --from-json compose.json
✓ committed 2 document(s)
# Reverse the order and the gate sees no staged entry → whole batch 403s.- Order matters: stage the guard before the write that reads it.
get()reads pre-batch state;getAfter()reads staged state. UsegetAfterfor any post-condition (“balance still ≥ floor after the transfer”).- Distinct paths per entry, in-batch path collisions reject.
Append-only caps
Collections under a rollingSum are append-only event logs: update and delete are rejected, so the history a cap is computed from cannot be rewritten, not by a compromised agent, not by your own buggy retry loop. Write each spend as a new document with a fresh id; idempotency comes from your ids, not from overwrites.
Machine-readable docs
- /llms.txt, entry point: setup flow, runtime semantics, SDK pointers.
- /llms-full.txt, the condensed policy reference: every key, every invariant type, failure semantics, and the worked examples above, in one plain-text file.
- /llms.txt provides the short public setup and execution loop for coding agents.