Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SEO.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ that page's frontmatter, never here.
| `docs/production/recovery` | agent crashed mid action | A restarted process repairs nothing and cannot know the holder is dead. |
| `docs/production/receipt-integrity` | verify receipt chain | Run ctrlrun receipts --verify-chain and read the six names it can report. |
| `docs/production/anchoring` | anchor receipt chain outside database | Anchor the chain's head where your database's writer cannot reach it, and what that does not prove. |
| `docs/production/retention` | delete old receipts hash chain | Prune a prefix of the receipt chain and still verify across the gap, or be refused. |
| `docs/production/soak` | ctrlrun soak test results | One published run, its measured duration, and the exit criterion it does not meet. |
| `docs/production/operations` | ctrlrun monitoring | Watch how many effects are sitting in an unknown outcome that nobody has answered. |
| `docs/mcp/overview` | MCP gateway human approval | CTRLRun works with MCP in four ways. |
Expand Down
6 changes: 6 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"docs/production/recovery",
"docs/production/receipt-integrity",
"docs/production/anchoring",
"docs/production/retention",
"docs/production/soak",
"docs/production/operations"
]
Expand Down Expand Up @@ -491,6 +492,11 @@
"destination": "/docs/production/anchoring",
"permanent": true
},
{
"source": "/production/retention",
"destination": "/docs/production/retention",
"permanent": true
},
{
"source": "/production/soak",
"destination": "/docs/production/soak",
Expand Down
4 changes: 2 additions & 2 deletions docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ the framework's own interrupt, and a framework with no such primitive does not n

{/* generated from the suite, pyproject and the soak (mdx) — run the generator */}
- **Version 0.10.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14.
- **6,158 tests**, every version specified before it was written and every requirement mutation-tested.
- **29 guarantees you can check in your own setup**, with `ctrlrun verify` against your policy, on your store's backend, in a scratch store it creates.
- **6,192 tests**, every version specified before it was written and every requirement mutation-tested.
- **32 guarantees you can check in your own setup**, with `ctrlrun verify` against your policy, on your store's backend, in a scratch store it creates.
- **One host: a file.** SQLite, no server, no ops. **Many hosts: Postgres**, the same guarantees, graded by the same suite.
- **Soaked for 20m 0s on postgres**: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. Nothing here establishes what only accumulates over days. [What it does not establish](https://ctrlrun.dev/docs/production/soak).
- **Each receipt carries the hash of the one before it**, so an alteration is detected and named.
Expand Down
46 changes: 23 additions & 23 deletions docs/CLAIMS.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions docs/OWASP-AGENTIC-TOP10.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ mechanism, not the entry.
| **G26** a hop is named on both sides | The receipt of an action taken under a hop names the hop, and so does the record of the hop's creation, so an action and the delegation that authorised it are joined from either end without inference. | `ASI07:2026` (partly), `ASI10:2026` (partly), `ASI03:2026` (partly) | Attribution across a hop was reconstruction before this: a reader had to match timestamps and principals and hope. It is evidence, not prevention, and it is on the receipt rather than in a log that can be rotated away. A hop created *inside* an action is not named on its creator's receipt; the `DELEGATION_CREATED` event carries the `action_id` and holds the join instead. |
| **G27** a swapped upstream is denied | An action entry may pin the upstream it authorises, by the SHA-256 of the server's leaf certificate or by the hash of a tool's advertised schema. A server that is not the pinned one, or a tool whose schema moved under an approved action name, is refused `upstream_mismatch`; an upstream nothing observed is refused `upstream_unverified` and never admitted. | `ASI02:2026` (partly), `ASI07:2026` (partly) | An approved action name is a name, and until v0.10 nothing checked that the thing answering to it was the thing that was approved. Enforced by `ctrlrun gateway`, the surface that holds the connection: at startup, at the decision, and at the TLS handshake. In-process there is no upstream to observe, so a pinned action refuses on every call, which is fail-closed and is why `ctrlrun verify` skips a pinned action unless a scenario asks for it by name. This is one slice of a supply chain and not the category: nothing here inspects a package, a model, a build or a signature chain. |
| **G28** truncation past an anchor fails | The chain's head is a row in the same database, so erasing the end of the log and updating that row is two statements and the chain reports itself intact. An anchor records the pair the head holds (`seq` and the hash at it) through a provider **you** supply, outside the store, and anything at or below an anchored `seq` can then no longer be removed or altered without the anchored pair failing to reproduce. Reported as `anchor_broken`, `anchor_missing` or `anchor_repudiated`, in the anchor's own report. | `ASI09:2026` (partly) | **An anchor freezes a prefix, and the limits are the point.** An **append is not detected**: a forged receipt lands above every anchored `seq`, so nothing stops reproducing and the next anchor freezes it like any other. Receipts written and erased entirely between two anchors are not detected either. It is **not a signature** and says nothing about who wrote the log, and an administrator who rewrites everything before the next anchor is still out of scope. The window you are exposed to is `(last anchored seq, current head]`, and its size is your choice of interval: that is the number to tune, and the number to quote instead of any sentence about tamper-evidence. CTRLRun ships **no** anchor provider, because an RFC 3161 client is a network client; the anchor is worth exactly what the record you point it at is worth, and one in the same directory as the database is worth nothing. |
| **G29** a prune adds no new chain break | Receipts accumulate, and deleting them breaks the chain by design. `ctrlrun prune` removes a **prefix** and leaves a checkpoint the reader seeds from, so the chain verifies **across** the gap. It refuses rather than warns: a prune that would leave a `(kind, seq)` pair the store did not already report, one through the head, one moving the checkpoint backwards, or one deleting a budget ledger row whose charge is still held. There is no `--force`. | `ASI09:2026` (partly) | Retention and evidence pull against each other, and the honest answer is that a prune is the only operation here that **destroys** evidence: what it deletes is gone. What this makes true is that the deletion is bounded and visible rather than silent, and that a deletion nobody can verify around is **refused** rather than completed with a warning. The rule is a **delta** and not a promise that the chain verifies afterwards: a store carrying `unchained` rows from before v0.6 can still be pruned, because the alternative is retention being impossible on exactly the oldest stores. It does not prune events, approvals, delegations or continuations, and nothing runs on a schedule. |
| **G30** a held range refuses to prune | `ctrlrun hold place` names a range of receipts and a reason, and any prune overlapping it is refused with the hold named. | `ASI09:2026` (partly) | The case this is for is a legal hold arriving in the middle of a retention schedule, and the failure it prevents is a scheduled job quietly deleting what somebody has just been told to keep. **A hold has no expiry**: one that lapsed on a timer would release evidence on a schedule nobody reviewed, so a person places it and a person ends it. What it does not do is stop anyone with database access from deleting rows directly; it binds `ctrlrun prune`, not `DELETE`. |
| **G32** an honest prune keeps anchors | A prune anchors its checkpoint **before** it deletes anything, and an anchor at or below an anchored checkpoint is then **superseded** rather than broken. | `ASI09:2026` (partly) | Without this the two features cancel: anchoring hourly and pruning at ninety days, every anchor older than the retention window would be permanently `anchor_broken`, and a deployment would have to choose between pruning and a permanent tamper signal. The half that stops *superseded* becoming a hole is that the checkpoint must itself be anchored, through the provider, which is outside the store: an attacker who erases a prefix and writes a checkpoint to explain it has to leave a record of the prune in the operator's own anchor history. **A prune stays visible even though the receipts are gone**, which is the whole of what retention owes evidence. |
| **G31** five receipt schemas verify | One receipt chain can hold every schema version a store has been written under, and `ctrlrun verify` walks it end to end, hash by hash, **each row hashed by the rule its own version wrote**. A store kept since v0.6 holds five: `v3`, `v4`, `v5`, `v6`, `v7`. A receipt whose schema label this binary does not know is **named** and is not reported as a break. | `ASI09:2026` (partly) | The receipt chain has been checkable since v0.6, but nothing proved it stayed checkable **across an upgrade**, which is the only interesting case: a chain that verifies on the day it is written and stops verifying two releases later is evidence with a shelf life nobody stated. The proof is built from the released wheels rather than from fixtures, because a fixture is this build's opinion of what 0.6 wrote. What it does not close is everything `G11` does not close: this is about whether the record can still be **read and recomputed** years later, not about who wrote it, and an administrator who rewrites every row including the head is still out of scope. Nor does it make a future version's fields intelligible: an unknown label is named so an operator knows which row this binary could not fully interpret, which is a different sentence from *this row was tampered with*. |

---
Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ Standards: A2A, as code. No conformance claim.
One question: can the record be trusted after the fact, and kept?

- **An external anchor for the receipt chain.** The chain detects alteration and says on every page that it does not detect truncation or append — both measured at two statements, undetected, because the head is a row in the same database. v0.11 anchors the head outside the database at an interval (an RFC 3161 timestamp, or an equivalent the operator supplies) so that **anything at or below an anchored `seq` can no longer be removed or altered** without the anchored pair failing to reproduce. An anchor freezes a prefix: **an append is not detected**, because an appended row lands above every anchored `seq`, and nor is a receipt created and destroyed entirely between two anchors. This sentence said "erased or appended" until 2026-09-14, when `SPEC-v0.11.md`'s review ran the cases; §2.4 there is the table, and the named kinds are the anchor's own, not the six chain-break kinds. No keys of its own: it consumes a timestamp and issues nothing, which is why it is here and signing is not. **Built by item 2 on 2026-09-14**, with `G28` grading it and `ctrlrun anchor` running it. CTRLRun ships **no** provider: an RFC 3161 client is a network client, so the operator supplies four calls (`make`, `check`, `latest`, `since`) and `examples/anchored-chain/` shows the smallest one that works. `since()` is the call a review added and the reason the design holds: with `make` and `check` alone, the record of *which* anchors exist lived in CTRLRun's own table, so deleting the newest row there left the older anchor reproducing and the truncation invisible, at a cost of one more statement.
- **Retention and legal hold.** There is no retention policy today and `docs/postgres.md` says so, in the same breath as the reason one is hard to write: deleting receipts from the middle or the end of the chain is detected as a break by design. v0.11 pays that debt: a chain-preserving prune that leaves a checkpoint receipt verifiable across the gap, and a hold that refuses to prune, both recorded as receipts themselves. **v0.9 adds a second growing table and states the invariant rather than the command**: the budget ledger only grows, and `SPEC-v0.9.md` §7.3 says that rows older than the longest window on any budget of a grant cannot affect a future decision, so somebody else's archiving is safe. One caveat travels with it, because the invariant is about decisions and not about evidence: an `AMBIGUOUS` effect older than that window still **holds** a charge the operator surfaces display, so an archiver on a live ledger excludes un-released rows. `ctrlrun stats` reports the row count so the growth is visible before it matters.
- **Retention and legal hold.** There is no retention policy today and `docs/postgres.md` says so, in the same breath as the reason one is hard to write: deleting receipts from the middle or the end of the chain is detected as a break by design. v0.11 pays that debt: a chain-preserving prune that leaves a checkpoint receipt verifiable across the gap, and a hold that refuses to prune, both recorded as receipts themselves. **v0.9 adds a second growing table and states the invariant rather than the command**: the budget ledger only grows, and `SPEC-v0.9.md` §7.3 says that rows older than the longest window on any budget of a grant cannot affect a future decision, so somebody else's archiving is safe. One caveat travels with it, because the invariant is about decisions and not about evidence: an `AMBIGUOUS` effect older than that window still **holds** a charge the operator surfaces display, so an archiver on a live ledger excludes un-released rows. `ctrlrun stats` reports the row count so the growth is visible before it matters. **Built by item 3 on 2026-09-14**, as `ctrlrun prune` and `ctrlrun hold`, with `G29`, `G30` and `G32` grading it. Two things the build settled that the line above did not say. The ledger rule is **settlement and then a window**, not "un-released": `COMMITTED` holds permanently and only `FAILED` releases, so "un-released" would have been almost every row forever, and a `COMMITTED` row is prunable only **outside** §7.3's window, because pruning one inside it hands back authority nobody granted. And the window is **supplied** on the command line rather than derived: a ledger row carries no window and no limit, those travel on `Charge` from the authority document, and a store that resolved them would be reading the policy.
- **Enforcement coverage.** From events already written: policy entries never exercised, gateway tools never routed, `@protect` actions never seen. The runtime half of `ctrlrun scan`, under the same rule — a clean result is not a verdict, no score, no percentage, no badge.
- **One chain, several receipt schemas.** `ctrlrun.receipt/v7` is the schema today, and the rule since `SPEC-v0.3.md` §12.2 is that every reader upgrades before any writer switches, so an older receipt on disk still parses. v0.8 (the verified approver; the grant id under break-glass), v0.9 (budget consumption) and v0.10 (the hop) each add fields and each bump the version, so a chain kept from v0.6 across them holds **five receipt schema versions**: `v3`, which 0.6 wrote, `v4`, which v0.7 added, and `v5`, `v6` and `v7` after it. This sentence has now gone stale twice and is corrected here rather than quietly both times. It said *three shapes* and named `v3` as the schema today, before v0.7's precondition fields bumped it; v0.7's release pass fixed that and left *four* and `v4`, which v0.10's hop field made wrong again. A count of versions in a document is a number that goes stale at every release, which is the argument for reading `receipt.py`'s constants instead. And nothing yet proved that `verify` walks it end to end, hash by hash, each receipt hashed by the rule its own version wrote. **v0.11's item 4 proves it and `G31` grades it, since 2026-09-14.** No new field: the version string already existed. What is new is the proof, and the rule that a receipt whose version the binary does not know is *named* and not reported as a break — the same distinction v0.6 §3.2 draws for a `schema_version` row the binary does not know. **The proof is built from the released wheels rather than from fixtures** (`scripts/five_schema_chain.py`): five environments, `pip install ctrlrun==0.6.1`, `0.7.0`, `0.8.0`, `0.9.0`, `0.10.0`, one store, then this build verifies across the whole thing, because a fixture is only this build's opinion of what 0.6 wrote. One thing that proof got wrong first is worth keeping: run with `PYTHONPATH=src`, the variable is inherited by every child, so all five "released wheels" imported the build under test and the run reported **one** schema version while looking exactly like a pass. The script now strips it and checks, per release, that the interpreter ran from that release's own environment. Added 2026-09-10, proved 2026-09-14.

Expand Down
7 changes: 5 additions & 2 deletions docs/cookbook/verify-in-github-actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The agent sees nothing; this is the operator's check. The build sees:

```text
CTRLRun verify — ctrlrun 0.10.0, catalogue ctrlrun.guarantees/v7
policy /Users/arpanghoshal/ctrlrun-project/wt/v11-i2/examples/cookbook/verify-in-github-actions/ctrlrun.yaml (ctrlrun.policy/v2, mode: enforce)
policy /Users/arpanghoshal/ctrlrun-project/wt/v11-i3/examples/cookbook/verify-in-github-actions/ctrlrun.yaml (ctrlrun.policy/v2, mode: enforce)
authority none
store sqlite, scratch (created and destroyed for this run)

Expand Down Expand Up @@ -106,13 +106,16 @@ G25 a hop narrows or it is refused N/A no authority section
G26 a hop is named on both sides N/A no authority section
G27 a swapped upstream is denied N/A no action entry pins an upstream
G28 truncation past an anchor fails PASS k8s.delete_namespace
G29 a prune adds no new chain break PASS k8s.delete_namespace
G30 a held range refuses to prune PASS k8s.delete_namespace
G31 five receipt schemas verify PASS k8s.delete_namespace
G32 an honest prune keeps anchors PASS k8s.delete_namespace
(a token is unique only as far as your effect keys are:
two stores sharing a provider account must not produce the
same effect-key string for different effects, and nothing
here can check that)

17/17 declared guarantees pass. 12 not applicable: G8, G9, G13, G15, G17, G19, G22, G23, G24, G25, G26, G27.
20/20 declared guarantees pass. 12 not applicable: G8, G9, G13, G15, G17, G19, G22, G23, G24, G25, G26, G27.
```

The first line is on stderr, from G7's own scenario driving an action with no principal — the
Expand Down
Loading