From 133b6e2e7f44b13de9192945096535c48fdf0205 Mon Sep 17 00:00:00 2001 From: arpan Date: Mon, 14 Sep 2026 05:10:51 +0530 Subject: [PATCH 1/3] Regenerate for needs_approval's new signature, and readiness at 6,080 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with ctrlrun#191, which builds two of SPEC-v0.10 §9.4's three rows. `needs_approval` now takes `task=` and `hop=`, so its API page picks up the signature and the paragraph saying what they are for. `banner.mdx` moves with it because the two share a source line range. Readiness 6,070 to 6,080. All seven generators, 19 recipes 0 drifted, 83 pages, claims re-pointed 0 unresolved 0, and the kernel worktree came back clean after the cookbook write. --- docs.mdx | 2 +- docs/production/index.mdx | 2 +- docs/reference/api/banner.mdx | 2 +- docs/reference/api/needs_approval.mdx | 9 ++++++++- generated/readiness.full.mdx | 2 +- generated/readiness.json | 2 +- generated/readiness.mdx | 2 +- generated/readiness.readme.md | 2 +- 8 files changed, 15 insertions(+), 8 deletions(-) diff --git a/docs.mdx b/docs.mdx index f343dd4..9ffb2d3 100644 --- a/docs.mdx +++ b/docs.mdx @@ -219,7 +219,7 @@ 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,070 tests**, every version specified before it was written and every requirement mutation-tested. +- **6,080 tests**, every version specified before it was written and every requirement mutation-tested. - **27 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). diff --git a/docs/production/index.mdx b/docs/production/index.mdx index 1165fe8..20df822 100644 --- a/docs/production/index.mdx +++ b/docs/production/index.mdx @@ -28,7 +28,7 @@ need. `test_the_first_line_of_the_section_says_which_store_and_why` asserts the {/* 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,070 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). +- **6,080 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). - **27 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). diff --git a/docs/reference/api/banner.mdx b/docs/reference/api/banner.mdx index 92e33ba..9f01813 100644 --- a/docs/reference/api/banner.mdx +++ b/docs/reference/api/banner.mdx @@ -5,7 +5,7 @@ description: "Log SPEC-v0.3 §6.5's observe banner, once per `Control`. An adapt {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.banner` — function, defined at `src/ctrlrun/adapter.py:475` +`ctrlrun.banner` — function, defined at `src/ctrlrun/adapter.py:484` ```python from ctrlrun import banner diff --git a/docs/reference/api/needs_approval.mdx b/docs/reference/api/needs_approval.mdx index 86d5b20..eafed2c 100644 --- a/docs/reference/api/needs_approval.mdx +++ b/docs/reference/api/needs_approval.mdx @@ -13,7 +13,7 @@ from ctrlrun import needs_approval ```python -def needs_approval(control: Control, action: str, arguments: Mapping[str, Any], *, resource: str | None = None) -> bool +def needs_approval(control: Control, action: str, arguments: Mapping[str, Any], *, resource: str | None = None, task: str | None = None, hop: str | None = None) -> bool ``` Does this call need a human? For a framework that asks before it invokes (SPEC-v0.5 §3.5). @@ -42,6 +42,13 @@ it to belongs in the evidence log. matters: authority matches on resource patterns (SPEC-v0.3 §4.2), so a predicate that skipped it would evaluate a different action from the one that runs. +`task` and `hop` are that same argument one frame further out (SPEC-v0.10 §9). Without them +this predicate evaluates against the receiver's **whole candidate set** while `execute` +evaluates against the hop **alone** (§2.3), so it answers "no human needed" for a call +`execute` then refuses. That is not a wider grant -- `Control.execute` is the enforcement +point and decides against the hop either way -- but it costs the framework its own approval +item, and a human is not asked before an invocation that then fails. + ## Next - [Python API index](/docs/reference/api/index). diff --git a/generated/readiness.full.mdx b/generated/readiness.full.mdx index 3fbafe3..f797016 100644 --- a/generated/readiness.full.mdx +++ b/generated/readiness.full.mdx @@ -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,070 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). +- **6,080 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). - **27 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). diff --git a/generated/readiness.json b/generated/readiness.json index d031ea6..24cdd23 100644 --- a/generated/readiness.json +++ b/generated/readiness.json @@ -18,6 +18,6 @@ "positive_control": true, "unexplained": 0 }, - "tests": 6070, + "tests": 6080, "version": "0.10.0" } diff --git a/generated/readiness.mdx b/generated/readiness.mdx index fead10d..5d095a0 100644 --- a/generated/readiness.mdx +++ b/generated/readiness.mdx @@ -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,070 tests**, every version specified before it was written and every requirement mutation-tested. +- **6,080 tests**, every version specified before it was written and every requirement mutation-tested. - **27 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). diff --git a/generated/readiness.readme.md b/generated/readiness.readme.md index 4113bc2..2539a25 100644 --- a/generated/readiness.readme.md +++ b/generated/readiness.readme.md @@ -1,6 +1,6 @@ - **Version 0.10.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14. -- **6,070 tests**, every version specified before it was written and every requirement mutation-tested. +- **6,080 tests**, every version specified before it was written and every requirement mutation-tested. - **27 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). From 619d555761d5dc1a446c3b4b175c331a805b2735 Mon Sep 17 00:00:00 2001 From: arpan Date: Mon, 14 Sep 2026 05:14:54 +0530 Subject: [PATCH 2/3] CI re-measures the test count, which its own docstring said it already did `render_readiness.py`'s docstring calls `--check` "what CI runs". CI did not run it. That is the sentence one level up from the comment beside the audit step: a guard CI does not run is prose. The count is the one figure on the site that is a **measurement** rather than a claim, and nothing in CI re-took it. `test_the_readiness_block_is_the_generators_in_every_place_it_appears` compares the embedded block against the stored `generated/readiness.json`, so the page and the state file agreed with each other while both drifted away from the library. Today they are four apart: the site says 6,066, ctrlrun `main` collects 6,070. **It cannot flap between a paired kernel and docs merge**, which is why it is safe to add mid-stack. The count is a floor: `--check` fails when the suite has *fewer* tests than the page claims and passes when it has more, so a kernel PR that adds tests leaves docs `main` green until its docs pair lands. Verified both ways against real checkouts -- docs `main` against kernel `main`: 0 drifted; this branch against ctrlrun#191: 0 drifted. `test_ci_runs_the_three_checks_and_the_drift_check` reads the workflow, so the list and the job cannot disagree. --- .github/workflows/ci.yml | 14 ++++++++++++-- tests/test_docs_audit.py | 11 ++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 903afe3..cfa9db7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,9 +91,18 @@ jobs: CTRLRUN_SOURCE: ${{ github.workspace }}/ctrlrun run: ./scripts/check.sh - # The four drift checks, named individually because `STYLE.md` says the CI job runs them - # and `test_ci_runs_the_three_checks_and_the_drift_check` reads this file to prove it. + # The drift checks, named individually because `STYLE.md` says the CI job runs them and + # `test_ci_runs_the_three_checks_and_the_drift_check` reads this file to prove it. # A guard CI does not run is prose. + # + # `render_readiness.py --check` joined them on 2026-09-14. Its own docstring called it + # "what CI runs" and CI did not run it, which is the same sentence one level up: the test + # count is the one figure on the site that is a measurement rather than a claim, and the + # pytest check on it compares the embedded block against the *stored* `readiness.json` + # rather than against the suite, so the two agreed with each other while both drifted away + # from the library. It cannot flap between a paired kernel and docs merge, because the + # count is a **floor**: it fails when the suite has fewer tests than the page claims, and + # passes when it has more. - name: The documentation audit working-directory: ctrlrun-docs env: @@ -103,3 +112,4 @@ jobs: python tools/docs_audit/lint.py python tools/docs_audit/links.py python tools/docs_audit/render_capabilities.py --check + python tools/docs_audit/render_readiness.py --check diff --git a/tests/test_docs_audit.py b/tests/test_docs_audit.py index c1c4324..f70c19d 100644 --- a/tests/test_docs_audit.py +++ b/tests/test_docs_audit.py @@ -660,7 +660,16 @@ def test_ci_runs_the_three_checks_and_the_drift_check(): """`STYLE.md` says the `docs` job runs them. A guard that CI does not run is prose.""" workflow = (REPO_ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") - for script in ("snippets.py", "lint.py", "links.py", "render_capabilities.py --check"): + for script in ( + "snippets.py", + "lint.py", + "links.py", + "render_capabilities.py --check", + # The count is a measurement, and until 2026-09-14 nothing in CI re-took it: the pytest + # check compares the embedded block against the stored `readiness.json`, so the two + # agreed with each other while both drifted from the library. + "render_readiness.py --check", + ): assert f"python tools/docs_audit/{script}" in workflow, script From d79370c4121b3ff6d8a59ab556294e2cf060f851 Mon Sep 17 00:00:00 2001 From: arpan Date: Mon, 14 Sep 2026 05:38:01 +0530 Subject: [PATCH 3/3] Two claims in the roadmap that are not true, both in the v0.11 section Found while writing v0.11's build prompts, which would otherwise have inherited both. **The receipt schema count is wrong, for the second time.** The v0.11 line says a chain kept from v0.6 holds four receipt schema versions and names `ctrlrun.receipt/v4` as the schema today. It holds five, and the schema today is `v7`. The sentence already carried a note saying it had been corrected once, for v0.7's bump; v0.10's hop field made it wrong again. Corrected, with the note now saying that twice, because a count of versions in prose goes stale at every release and the argument for reading `receipt.py`'s constants is stronger than the count. **`docs/CONTROL-MAPPING.md` is cited in the present tense and does not exist.** The retention line says it "maps receipt retention to a clause". Line 136 of the same file says it is written only when a design partner asks, which is the truth. The retention debt is real and `docs/postgres.md` is the citation for it, so that half stays and gains the reason the debt is hard: deleting receipts from the middle or the end of the chain is detected as a break by design. Worth separating, because a compliance clause that does not exist is the worst possible input to a milestone's design. `docs/OWASP-AGENTIC-TOP10.md` opens by refusing exactly this shape of claim, and a roadmap that quietly assumes one contradicts it. --- docs/ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3958e75..c3af117 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -376,9 +376,9 @@ 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 a suffix erased or appended between two anchors is detected and named, in the same vocabulary as the six existing break kinds. No keys of its own: it consumes a timestamp and issues nothing, which is why it is here and signing is not. -- **Retention and legal hold.** There is no retention policy today and `docs/postgres.md` says so, while `docs/CONTROL-MAPPING.md` maps receipt retention to a clause. 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. - **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/v4` 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) and v0.9 (budget consumption) each add fields and each bump the version, so a chain kept from v0.6 across them holds **four receipt schema versions**: `v3`, which 0.6 wrote, `v4`, which v0.7 added, and the two that follow. This sentence said *three shapes* and named `v3` as the schema today. It was written before v0.7's precondition fields bumped the schema, and v0.7's release pass corrects it here rather than quietly. And nothing yet proves that `verify` walks it end to end, hash by hash, each receipt hashed by the rule its own version wrote. v0.11 proves it, here, because this is the milestone about whether the record can be trusted after the fact. No new field: the version string already exists. What is new is the test, and the rule that a receipt whose version the binary does not know is *named* and not reported as a break — which is the same distinction v0.6 §3.2 draws for a `schema_version` row the binary does not know. Added 2026-09-10. +- **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 proves that `verify` walks it end to end, hash by hash, each receipt hashed by the rule its own version wrote. v0.11 proves it, here, because this is the milestone about whether the record can be trusted after the fact. No new field: the version string already exists. What is new is the test, and the rule that a receipt whose version the binary does not know is *named* and not reported as a break — which is the same distinction v0.6 §3.2 draws for a `schema_version` row the binary does not know. Added 2026-09-10. - **A malformed value in a receipt row blinds every reader of the chain, and one `UPDATE` is enough.** Found while building v0.7's item 5, deferred there with a written decision, and named here because it is the evidence surface and this is the evidence milestone. A receipt whose *schema label* is unknown, and a receipt carrying an *added key*, are each reported at their `seq` and leave every other row readable. A malformed **value** of a key the schema declares is not: a float among a receipt's `controls` raises out of `Receipt.from_dict`, so `ctrlrun receipts`, `receipts --verify-chain`, `ctrlrun inspect`, `ctrlrun stats` and `G11` all stop together, and a single tampered row hides the whole document rather than naming itself. 0.6.1 behaves the same way and v0.7 neither introduced nor widened it. Fixing it needs one of two things, and both are amendments rather than patches: a new name in `CHAIN_BREAKS`, which is a closed set on a `SPEC-v0.6.md` §6.5 surface, or a reader that walks raw rows and reports per row without constructing a `Receipt` at all. `SPEC-v0.7.md` §12.5 carries the argument. Added 2026-09-12.