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
4 changes: 2 additions & 2 deletions docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ the framework's own interrupt, and a framework with no such primitive does not n
## Where it stands

{/* 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,201 tests**, every version specified before it was written and every requirement mutation-tested.
- **Version 0.11.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14.
- **6,221 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).
Expand Down
58 changes: 29 additions & 29 deletions docs/CLAIMS.md

Large diffs are not rendered by default.

39 changes: 38 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ raises. The upgrade was checked against the **released** 0.9.0 from PyPI rather

Standards: A2A, as code. No conformance claim.

## v0.11 — Evidence
## v0.11 — Evidence · shipped 2026-09-14

One question: can the record be trusted after the fact, and kept?

Expand All @@ -384,6 +384,43 @@ One question: can the record be trusted after the fact, and kept?

**Does not close.** Authorship. An anchor proves the log existed in this form at that time; it does not prove who wrote it, and a malicious administrator who rewrites everything before the next anchor is still out of scope. Signed receipts stay off the roadmap for the reason `SPEC-v0.6.md` §11 gives.

Exit criteria met: `ctrlrun.guarantees/v7` with `G28` to `G32` each grading `PASS` and each grading
the same under `--only` as in a full run; `examples/anchored-chain` exercises an anchor and prints
what one does **not** prove; a chain written by the **released** 0.6.1, 0.7.0, 0.8.0, 0.9.0 and
0.10.0 wheels verifies end to end across five receipt schema versions; a prune across a checkpoint
verifies and anchors that checkpoint before deleting anything; a held range refuses to prune; and
two prunes racing under the multi-process standard against Postgres leave no break the store did not
already have. The upgrade was checked against the **released** 0.10.0 from PyPI rather than a
fixture: 0.11.0 migrates the store, the chain verifies across the boundary, and 0.10.0 then refuses
it with `SchemaMismatch` rather than corrupting it.

**Reconciled against what shipped**, because three sentences above were written before the code
existed and two of them were wrong.

- **The anchor detects truncation and NOT append**, and the bullet above said "erased or appended"
until 2026-09-14. A forged receipt lands at head + 1, above every anchored `seq`, so nothing stops
reproducing and the next anchor freezes it like any other. `T531` runs a forged append and
requires both reports to stay clean, so the limit is a tested property rather than a sentence
somebody has to remember.
- **Enforcement coverage does not come "from events already written."** `ACTION_PROPOSED` carries an
`action_hash` and nothing that maps it back to a name. It comes from receipts, which every decided
action leaves, **a denial included** -- so an action that is always denied counts as exercised.
- **`ctrlrun scan --coverage` is the surface**, and `docs/CONTROL-MAPPING.md` is still not written.
The v0.11 line cited it in the present tense; roadmap line 136 says it is written only when a
design partner asks, and none has.
- **Retention shipped twice.** The build order made an independent review required for the prune,
*the one not to skip*, and the merge did not wait for it. The review found seven defects, the
first of which made a forged checkpoint launderable with one row in the store's own `anchors`
table, and the fixes went in as a second pull request. `SPEC-v0.11.md` §13.4 records all seven
and the rule this milestone adds: **a required review is a merge gate, not a step in the item.**

Three surfaces this milestone amends rather than adds to, each named because an amendment to a
frozen surface is not a patch: `StateStore` gains anchor, checkpoint and hold methods
(`SPEC-v0.6.md` §9.2), `StateStore.receipts()` may now hand back an `UnreadableReceipt` instead of
raising, and `verify_chain` seeds from a checkpoint where a store has one. **`CHAIN_BREAKS` is
unchanged and stays closed at six**: the anchor has its own set, because putting its kinds in
`CHAIN_BREAKS` would fail `G11`'s control with `control failed` on every anchoring deployment.

Do not build: a SIEM · dashboards over receipts · a receipt query language · export formats beyond JSON and OTel.

Exit: **the truncation case** that `SPEC-v0.6.md` §6.4 lists as undetected now detects, with the anchor as the positive control and the two-statement attack as the scenario (append is **not** in this criterion, and §2.4 of `SPEC-v0.11.md` says why); a prune across a checkpoint verifies, and the prune's checkpoint is anchored before anything is deleted; a held range refuses to prune; a chain written across **five** receipt schema versions verifies end to end, and the checkpoint receipt of a prune carries the version current when it was written.
Expand Down
2 changes: 1 addition & 1 deletion docs/cookbook/verify-in-github-actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ by tag where you want a ref nobody can move.
The agent sees nothing; this is the operator's check. The build sees:

```text
CTRLRun verify — ctrlrun 0.10.0, catalogue ctrlrun.guarantees/v7
CTRLRun verify — ctrlrun 0.11.0, catalogue ctrlrun.guarantees/v7
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
2 changes: 1 addition & 1 deletion docs/guides/verify-in-ci.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ guarantees pass.
```

```text
CTRLRun verify — ctrlrun 0.10.0, catalogue ctrlrun.guarantees/v7
CTRLRun verify — ctrlrun 0.11.0, catalogue ctrlrun.guarantees/v7
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
4 changes: 2 additions & 2 deletions docs/production/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ need. `test_the_first_line_of_the_section_says_which_store_and_why` asserts the
## Where it stands

{/* generated from the suite, pyproject and the soak (full) — 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,201 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built).
- **Version 0.11.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14.
- **6,221 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built).
- **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. [Read more](/docs/security/verify-guarantees).
- **One host: a file.** SQLite, no server, no ops. **Many hosts: Postgres**, the same guarantees, graded by the same suite. [Read more](/docs/production/postgres).
- **Soaked for 20m 0s on postgres**: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. Nothing here establishes what only accumulates over days. [Read more](/docs/production/soak).
Expand Down
21 changes: 18 additions & 3 deletions docs/production/operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,24 @@ and there will not be one.
- **A reporting role still needs `CREATE` on the schema.** Opening the store migrates it, so a
read-only role cannot run `--verify-chain` — it is refused before a receipt is read, naming
the missing privilege. Read-only is for your own queries against the tables, not for the CLI.
- **Anchor on an interval you choose, and say what it is.** `ctrlrun anchor --provider ...`
records the chain's head outside the database, which is what makes a truncation detectable at
all: the head that would catch one is otherwise a row in the same database. The window you are
exposed to is `(last anchored seq, current head]`, so the interval **is** the claim, and it is
the number to quote rather than any sentence about tamper-evidence. See
[anchoring](/docs/production/anchoring).
- **Prune with a window, not with a `DELETE`.** `ctrlrun prune` removes a prefix and leaves a
checkpoint the reader seeds from; deleting receipts yourself is detected as a break, by design.
It refuses rather than warns, and there is no `--force`. See
[retention](/docs/production/retention).

## What this does not do

- **There is nothing to run.** No agent, no sidecar, no scheduler. Every command above is a
one-shot read of the store.
one-shot read of the store, and the two that write — `anchor` and `prune` — run when **your**
scheduler runs them. Nothing sweeps, nothing reaps, and **no hold expires**: a hold that lapsed
on a timer would release evidence on a schedule nobody reviewed, so a person places it and a
person ends it.
- **It does not retry for you.** A proven non-execution leaves the key retryable — the next
attempt is admitted rather than refused — and nothing performs that attempt but your code.
Every other outcome refuses one.
Expand All @@ -64,11 +77,13 @@ and there will not be one.

**Verified by** `T160` and `T177d` — an expired lease frees nothing, and displaying it changes
nothing — `T161` for the two authorities that can move an unknown outcome and what each is
recorded as, and `T177c` for the CLI surface these commands come from, asserted against the
command list so a new one cannot appear unnoticed.
recorded as, `T545c` for the hold that no timer lifts, and `T177c` for the CLI surface these
commands come from, asserted against the command list so a new one cannot appear unnoticed.

## Next

- [Recovery after a crash](/docs/production/recovery): why nothing sweeps.
- [Anchoring](/docs/production/anchoring) and [retention](/docs/production/retention): the two
things this milestone gives an operator to schedule.
- [Reconcile automatically](/docs/guides/reconcile-automatically): the hook that answers without a person.
- [Get started](/docs/get-started/quickstart) · [Why](/docs/why).
23 changes: 18 additions & 5 deletions docs/production/retention.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,32 @@ reviewed, so a person places it and a person ends it.
- **It does not run on a schedule.** Nothing sweeps, nothing reaps, and no hold expires. You run
the command, or your scheduler does.
- **It does not make a checkpoint unforgeable.** Anyone who can insert receipts can write a
checkpoint row. What narrows that is the anchor: a prune must anchor its checkpoint before
deleting, and an erasure with no anchored checkpoint behind it reports `anchor_broken`.
checkpoint row, and anyone who can write that row can write a row in the local `anchors` table
beside it. What narrows both is the provider: a checkpoint supersedes the receipts below it
only when **the provider still returns an anchor**, and only when that anchor's `(seq, hash)`
is the pair the checkpoint claims. A local row the provider does not confirm buys nothing, and
an erasure with no anchored checkpoint behind it reports `anchor_broken`.
- **It does not decide what you may delete.** Retention is not in your policy document,
deliberately: a policy key would make pruning subject to `require_approved_policy`, and a
deployment that had not approved its current policy could then never prune. What authorises a
prune is shell access to the store, which policy does not mediate. Put a human in front of the
command, where you already are for every other destructive operation on your database.

## What the prune itself leaves behind

Two receipts, not one, and they are distinguishable: the first records the request (`--through`,
`--older-than`, `--reason`) as `proposed`, and the second records what became of it as
`completed` or `refused`. A prune that was refused and a prune that succeeded therefore do not
leave the same bytes, which is the only reason the record of a refusal is worth keeping.

**Verified by** `T540` for the chain verifying across the gap, with a naive prefix delete as its
negative control; `T543` for a store that already had a break; `T545` for a hold; `T546b` for the
budget window, where pruning a `COMMITTED` row inside it manufactures authority; `T547b` for a
forged checkpoint with no anchor behind it; and `T549`, which races two prunes in separate OS
processes against a real Postgres server.
budget window, where pruning a `COMMITTED` row inside it manufactures authority; `T550` and
`T559` for a checkpoint the provider does not confirm, including a forged local `anchors` row
that names the checkpoint's own pair; `T552` for a `--through` above the head; `T554` for a
checkpoint asserting a pair that never existed; `T555` for the two receipts; `T551` and `T558`
for the prune holding its lock across every write on SQLite and on Postgres; and `T549`, which
races two prunes in separate OS processes against a real Postgres server.

## Next

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/Condition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "One `<argument>_<op>: operand` test against an action's arguments

{/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */}

`ctrlrun.Condition` — class, defined at `src/ctrlrun/policy.py:400`
`ctrlrun.Condition` — class, defined at `src/ctrlrun/policy.py:389`

```python
from ctrlrun import Condition
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/Decision.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "What may happen to an action: exactly three outcomes in v0.1 (SPEC

{/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */}

`ctrlrun.Decision` — class, defined at `src/ctrlrun/policy.py:337`
`ctrlrun.Decision` — class, defined at `src/ctrlrun/decision.py:41`

```python
from ctrlrun import Decision
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/Event.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "One ordered step in the life of an action (SPEC-v0.1 §6.2)."

{/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */}

`ctrlrun.Event` — class, defined at `src/ctrlrun/receipt.py:307`
`ctrlrun.Event` — class, defined at `src/ctrlrun/receipt.py:311`

```python
from ctrlrun import Event
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/EventSink.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Somewhere a copy of every `Event` and `Receipt` goes (SPEC-v0.2 §

{/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */}

`ctrlrun.EventSink` — class, defined at `src/ctrlrun/receipt.py:915`
`ctrlrun.EventSink` — class, defined at `src/ctrlrun/receipt.py:919`

```python
from ctrlrun import EventSink
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/JSONLEventSink.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "The JSONL half of the evidence: two append-only files in one direc

{/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */}

`ctrlrun.JSONLEventSink` — class, defined at `src/ctrlrun/receipt.py:934`
`ctrlrun.JSONLEventSink` — class, defined at `src/ctrlrun/receipt.py:938`

```python
from ctrlrun import JSONLEventSink
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/Policy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Action-level autonomy policy: which actions may run, and under whi

{/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */}

`ctrlrun.Policy` — class, defined at `src/ctrlrun/policy.py:761`
`ctrlrun.Policy` — class, defined at `src/ctrlrun/policy.py:750`

```python
from ctrlrun import Policy
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/Receipt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Portable evidence of one action that reached a terminal state (SPE

{/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */}

`ctrlrun.Receipt` — class, defined at `src/ctrlrun/receipt.py:399`
`ctrlrun.Receipt` — class, defined at `src/ctrlrun/receipt.py:403`

```python
from ctrlrun import Receipt
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/jwt_identity-JWTIdentityProvider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Verify a bearer JWT and map its verified claims onto a `Principal`

{/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */}

`ctrlrun.jwt_identity.JWTIdentityProvider` — class, defined at `src/ctrlrun/jwt_identity.py:115`
`ctrlrun.jwt_identity.JWTIdentityProvider` — class, defined at `src/ctrlrun/jwt_identity.py:120`

```python
from ctrlrun.jwt_identity import JWTIdentityProvider
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/parse_conditions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Parse a `when:`-shaped mapping into conditions, keyed by the raw c

{/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */}

`ctrlrun.parse_conditions` — function, defined at `src/ctrlrun/policy.py:1328`
`ctrlrun.parse_conditions` — function, defined at `src/ctrlrun/policy.py:1317`

```python
from ctrlrun import parse_conditions
Expand Down
2 changes: 1 addition & 1 deletion docs/verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ what could not be tested at all.

```console
$ ctrlrun verify
CTRLRun verify — ctrlrun 0.10.0, catalogue ctrlrun.guarantees/v7
CTRLRun verify — ctrlrun 0.11.0, catalogue ctrlrun.guarantees/v7
policy examples/authority/payments.yaml (ctrlrun.policy/v7, mode: enforce)
authority same document, 3 grants
store sqlite, scratch (created and destroyed for this run)
Expand Down
4 changes: 2 additions & 2 deletions generated/readiness.full.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{/* generated from the suite, pyproject and the soak (full) — 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,201 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built).
- **Version 0.11.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14.
- **6,221 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built).
- **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. [Read more](/docs/security/verify-guarantees).
- **One host: a file.** SQLite, no server, no ops. **Many hosts: Postgres**, the same guarantees, graded by the same suite. [Read more](/docs/production/postgres).
- **Soaked for 20m 0s on postgres**: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. Nothing here establishes what only accumulates over days. [Read more](/docs/production/soak).
Expand Down
6 changes: 3 additions & 3 deletions generated/readiness.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"3.14"
]
},
"released": "0.10.0",
"released": "0.11.0",
"soak": {
"actions": 889735,
"backend": "postgres",
Expand All @@ -18,6 +18,6 @@
"positive_control": true,
"unexplained": 0
},
"tests": 6201,
"version": "0.10.0"
"tests": 6221,
"version": "0.11.0"
}
4 changes: 2 additions & 2 deletions generated/readiness.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{/* 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,201 tests**, every version specified before it was written and every requirement mutation-tested.
- **Version 0.11.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14.
- **6,221 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).
Expand Down
Loading