From 5ff43241c2ba44ae9166a8888a57568da4deb5b7 Mon Sep 17 00:00:00 2001 From: arpan Date: Mon, 14 Sep 2026 00:58:27 +0530 Subject: [PATCH] Pages for v0.10 item 3: upstream identity pinning, G27 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerated against CTRLRun/ctrlrun's v0.10/3-upstream-pinning, the branch of the same name, which the kernel's docs job now checks this one against. Rendered: the API pages and the errors page (the two upstream refusals), the readiness block (6,019 tests, 27 guarantees) and its two copies, CLAIMS.md repointed (54 citations, none unresolved), the three verify transcripts at catalogue v6 with G27. Written: docs/reference/policy-yaml.mdx names `upstream`, the action-entry key `ctrlrun.policy/v8` adds, with its three pins and the three places one rule is checked, and the version table gains the v7 and v8 rows it was missing. The OWASP mapping gains G27 (a swapped upstream is denied), mapped partly to ASI02 and ASI07 with its three limits in the row, and the ASI04 row keeps its "out of scope" verdict and gains the one sentence SPEC-v0.10 §4.1 allows it. Signed-off-by: arpan --- docs.mdx | 4 +- docs/CLAIMS.md | 80 +++++++++++----------- docs/OWASP-AGENTIC-TOP10.md | 3 +- docs/cookbook/verify-in-github-actions.mdx | 3 +- docs/guides/verify-in-ci.mdx | 3 +- docs/production/index.mdx | 4 +- docs/reference/api/Condition.mdx | 2 +- docs/reference/api/Control.mdx | 4 +- docs/reference/api/Decision.mdx | 2 +- docs/reference/api/Policy.mdx | 2 +- docs/reference/api/context.mdx | 2 +- docs/reference/api/idempotency_token.mdx | 2 +- docs/reference/api/parse_conditions.mdx | 2 +- docs/reference/api/protect.mdx | 2 +- docs/reference/api/with_approval.mdx | 2 +- docs/reference/policy-yaml.mdx | 3 + docs/verify.md | 3 +- generated/readiness.full.mdx | 4 +- generated/readiness.json | 4 +- generated/readiness.mdx | 4 +- generated/readiness.readme.md | 4 +- 21 files changed, 73 insertions(+), 66 deletions(-) diff --git a/docs.mdx b/docs.mdx index 3a5b033..6c0b268 100644 --- a/docs.mdx +++ b/docs.mdx @@ -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.9.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later. -- **6,005 tests**, every version specified before it was written and every requirement mutation-tested. -- **26 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,019 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). - **Each receipt carries the hash of the one before it**, so an alteration is detected and named. diff --git a/docs/CLAIMS.md b/docs/CLAIMS.md index 09c10af..c4449d7 100644 --- a/docs/CLAIMS.md +++ b/docs/CLAIMS.md @@ -25,9 +25,9 @@ by its quoted claim, and `tests/test_docs_audit.py` fails if a named row is not | Claim | Code | Proof | |---|---|---| -| "The last check before an AI agent does something it can't undo." | `Control.execute` — `control.py:1181` — resolves the principal, evaluates authority and policy, consumes the approval and reserves the effect key **before** the executor runs; nothing in the wrapper calls the function first | `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote`, `test_T3_the_fake_remote_is_called_exactly_once` | -| "Autonomy belongs to the action, not the agent." | `Policy.evaluate(action)` — `policy.py:596` — passes only the action's **name and arguments** to `_ActionPolicy.evaluate` (`policy.py:596`), whose signature has no principal in it. A rule cannot read who is acting even by accident. `agent_eq` and `user_eq` are refused at load by `RESERVED_ARGUMENTS` (`policy.py:224`) rather than silently matching nothing. | `test_T6_an_action_name_is_matched_exactly`, `test_a_condition_naming_an_action_field_is_refused_at_load` | -| "A consequential action happens at most once, exactly as approved, and leaves a receipt — and when the outcome is unknown, CTRLRun says so instead of guessing." | At most once: `plan_reservation` — `effect.py:248`. Exactly as approved: the approval is bound to `action_hash` and consumed with the reservation — `_authorize_and_reserve` — `state.py:1196`. Or not at all: a refusal raises before the executor — `Control.execute` — `control.py:1181`. Says so instead of guessing: only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2054` — and everything else is `AMBIGUOUS`. A receipt: `Receipt` — `receipt.py:250`. **This sentence read *happens once … or not at all* until 0.6**, a two-way disjunction that excluded the third outcome the product exists for: a lost reply is neither, and the README's own first section says so. | `test_T3_exactly_one_agent_reserves_and_seven_are_blocked`, `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch`, `test_T1_a_lost_response_leaves_the_effect_ambiguous`, `test_T11_every_demo_receipt_carries_every_field_in_the_spec` | +| "The last check before an AI agent does something it can't undo." | `Control.execute` — `control.py:1187` — resolves the principal, evaluates authority and policy, consumes the approval and reserves the effect key **before** the executor runs; nothing in the wrapper calls the function first | `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote`, `test_T3_the_fake_remote_is_called_exactly_once` | +| "Autonomy belongs to the action, not the agent." | `Policy.evaluate(action)` — `policy.py:655` — passes only the action's **name and arguments** to `_ActionPolicy.evaluate` (`policy.py:655`), whose signature has no principal in it. A rule cannot read who is acting even by accident. `agent_eq` and `user_eq` are refused at load by `RESERVED_ARGUMENTS` (`policy.py:251`) rather than silently matching nothing. | `test_T6_an_action_name_is_matched_exactly`, `test_a_condition_naming_an_action_field_is_refused_at_load` | +| "A consequential action happens at most once, exactly as approved, and leaves a receipt — and when the outcome is unknown, CTRLRun says so instead of guessing." | At most once: `plan_reservation` — `effect.py:248`. Exactly as approved: the approval is bound to `action_hash` and consumed with the reservation — `_authorize_and_reserve` — `state.py:1196`. Or not at all: a refusal raises before the executor — `Control.execute` — `control.py:1187`. Says so instead of guessing: only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2065` — and everything else is `AMBIGUOUS`. A receipt: `Receipt` — `receipt.py:250`. **This sentence read *happens once … or not at all* until 0.6**, a two-way disjunction that excluded the third outcome the product exists for: a lost reply is neither, and the README's own first section says so. | `test_T3_exactly_one_agent_reserves_and_seven_are_blocked`, `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch`, `test_T1_a_lost_response_leaves_the_effect_ambiguous`, `test_T11_every_demo_receipt_carries_every_field_in_the_spec` | | "A Python library that sits between the decision to act and the call that acts." | `@protect` — `control.py` — wraps the callable that acts, and `Control.execute` runs every check before invoking it. The category noun was on `docs.mdx` and in `pyproject.toml`'s `description` and nowhere in the README until 0.6, so a reader had to infer what CTRLRun **is** from three slogans. | `test_the_header_carries_the_fixed_copy_and_the_five_badges`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote` | | "Runs in production on a single file, or on Postgres across hosts" | SQLite: `SQLiteStateStore` reserves inside the `BEGIN IMMEDIATE` of `_authorize_and_reserve` — `state.py:1476` — which is a write lock on the file and holds across OS processes. Postgres: `PostgresStateStore` over `UNIQUE(effect_key)` with `INSERT … ON CONFLICT DO NOTHING` and checked row counts (SPEC-v0.6 §4.2), the same `StateStore` protocol, extended by nothing | `test_T3_exactly_one_agent_reserves_and_seven_are_blocked` (8 OS processes, both backends), `test_T141_the_shipped_backends_pass`, `test_T154_postgres_passes_the_store_conformance_suite` | @@ -35,7 +35,7 @@ by its quoted claim, and `tests/test_docs_audit.py` fails if a named row is not | Claim | Code | Proof | |---|---|---| -| "A lost reply is `AMBIGUOUS`, never `FAILED`, and a retry against an `AMBIGUOUS` effect is refused — until a human, or a `reconcile` hook, says what happened." | Only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:1188`; a retry against an `AMBIGUOUS` key is refused by `plan_reservation` — `effect.py:248`; the two things permitted to move the record on and nothing else — `resolve` — `cli/main.py:587` — and `Control._reconciled` — `control.py:2702` | `test_T1_a_lost_response_leaves_the_effect_ambiguous`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote`, `test_T160_there_is_no_reaper`, `test_T13_a_hook_answering_not_executed_moves_the_record_to_failed` | +| "A lost reply is `AMBIGUOUS`, never `FAILED`, and a retry against an `AMBIGUOUS` effect is refused — until a human, or a `reconcile` hook, says what happened." | Only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:1194`; a retry against an `AMBIGUOUS` key is refused by `plan_reservation` — `effect.py:248`; the two things permitted to move the record on and nothing else — `resolve` — `cli/main.py:587` — and `Control._reconciled` — `control.py:2719` | `test_T1_a_lost_response_leaves_the_effect_ambiguous`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote`, `test_T160_there_is_no_reaper`, `test_T13_a_hook_answering_not_executed_moves_the_record_to_failed` | | "The customer is refunded twice, and nothing in the stack noticed." — said of a stack without CTRLRun; the demo runs the same sequence with it, and counts the calls the remote received | `ctrlrun demo` scenario 1, which retries against a fake remote that counts its calls and prints the count | `test_T3_the_fake_remote_is_called_exactly_once`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote` | ## Protect your first action @@ -50,31 +50,31 @@ by its quoted claim, and `tests/test_docs_audit.py` fails if a named row is not | Claim | Code | Proof | |---|---|---| -| "A lost reply is `AMBIGUOUS`, never `FAILED`, and a retry against an `AMBIGUOUS` effect is refused." | Only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2054`; `plan_reservation` — `effect.py:248` | `test_T1_a_lost_response_leaves_the_effect_ambiguous`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote` | +| "A lost reply is `AMBIGUOUS`, never `FAILED`, and a retry against an `AMBIGUOUS` effect is refused." | Only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2065`; `plan_reservation` — `effect.py:248` | `test_T1_a_lost_response_leaves_the_effect_ambiguous`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote` | | "reserved atomically across processes and hosts; one worker wins" | `reserve_effect` — `state.py:637`; `PostgresStateStore.reserve_effect` — `postgres.py:740` | `test_T3_exactly_one_agent_reserves_and_seven_are_blocked`, `test_T154_postgres_passes_the_store_conformance_suite` | | "bound to the hash of the exact action a human saw, used once, and refused for anything else" | `_authorize_and_reserve` — `state.py:1196` | `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch`, `test_T4_replaying_the_approval_raises_ApprovalMismatch_with_reason_consumed` | -| "An action the policy does not list is denied" | `Policy.evaluate` — `policy.py:596` | `test_T6_unknown_action_is_denied_with_reason_unknown_action` | -| "Authority first ... then policy" / "authority first" | `Control.execute` evaluates authority before policy and a denial appends `AUTHORITY_DENIED` and never `POLICY_EVALUATED` — `control.py:1181` | `test_T74_a_denial_leaves_no_pending_approval_request` | -| "Neither axis reads the agent's instructions" | `Policy.evaluate` — `policy.py:596` — sees the action's name and arguments; `Authority.evaluate` — `authority.py:1256` — sees the action and the principal; neither is handed a prompt, a message or a tool result | `test_T6_an_action_name_is_matched_exactly`, `test_T67_a_principal_with_no_grant_is_denied` | +| "An action the policy does not list is denied" | `Policy.evaluate` — `policy.py:655` | `test_T6_unknown_action_is_denied_with_reason_unknown_action` | +| "Authority first ... then policy" / "authority first" | `Control.execute` evaluates authority before policy and a denial appends `AUTHORITY_DENIED` and never `POLICY_EVALUATED` — `control.py:1187` | `test_T74_a_denial_leaves_no_pending_approval_request` | +| "Neither axis reads the agent's instructions" | `Policy.evaluate` — `policy.py:655` — sees the action's name and arguments; `Authority.evaluate` — `authority.py:1256` — sees the action and the principal; neither is handed a prompt, a message or a tool result | `test_T6_an_action_name_is_matched_exactly`, `test_T67_a_principal_with_no_grant_is_denied` | | "canonical arguments (sorted keys, no floats) ... Its SHA-256 is the action hash" | `canonicalize` / `action_hash` — `action.py`; `float` refused at any depth — `action.py:79` | `test_T7_canonical_form_is_exactly_the_specified_serialization`, `test_T7_nested_dicts_are_sorted_recursively` | | "The approval is single-use, expires, and matches nothing but that exact action." | `_authorize_and_reserve` — `state.py:1196` — checks expiry at consumption | `test_T5_expiry_is_checked_at_consumption_not_only_at_grant`, `test_T4_replaying_the_approval_raises_ApprovalMismatch_with_reason_consumed` | -| "Only `NotExecuted`, raised by you, means `FAILED`." | `_outcome` — `control.py:2054`; `NotExecuted` — `errors.py:157` | `test_T1_a_lost_response_leaves_the_effect_ambiguous` | -| "the hash of the policy that decided it, chained to the receipt before it" | `Policy.policy_hash` — `policy.py:734`; `prev_hash`, `GENESIS_HASH` for the first — `receipt.py:125` | `test_T172_every_receipt_carries_the_hash_and_the_declared_version`, `test_T164_an_altered_receipt_is_content_altered_at_its_seq` | +| "Only `NotExecuted`, raised by you, means `FAILED`." | `_outcome` — `control.py:2065`; `NotExecuted` — `errors.py:157` | `test_T1_a_lost_response_leaves_the_effect_ambiguous` | +| "the hash of the policy that decided it, chained to the receipt before it" | `Policy.policy_hash` — `policy.py:793`; `prev_hash`, `GENESIS_HASH` for the first — `receipt.py:125` | `test_T172_every_receipt_carries_the_hash_and_the_declared_version`, `test_T164_an_altered_receipt_is_content_altered_at_its_seq` | ## Three ways to use it | Claim | Code | Proof | |---|---|---| -| "You probably do not need an adapter" | Three ways in, and `@protect` (`control.py:4744`) covers this process while the gateway covers MCP — an adapter buys only the interrupt | `test_T139_the_adapter_section_says_when_you_do_not_need_one_up_front` | +| "You probably do not need an adapter" | Three ways in, and `@protect` (`control.py:4795`) covers this process while the gateway covers MCP — an adapter buys only the interrupt | `test_T139_the_adapter_section_says_when_you_do_not_need_one_up_front` | | "`ctrlrun init` writes a starter" | `init` — `cli/main.py:353` | CI's `package` job runs `ctrlrun init` from the wheel and asserts `ctrlrun.yaml` exists | -| "The human runs `ctrlrun approve ` and the agent calls again inside `ctrlrun.with_approval(request_id)`" | `approve` — `cli/main.py:377`; `with_approval` — `control.py:386`; `ApprovalRequired` (`errors.py:88`) carries `request_id` | `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch` (the granted path first), `test_T4_replaying_the_approval_raises_ApprovalMismatch_with_reason_consumed` | +| "The human runs `ctrlrun approve ` and the agent calls again inside `ctrlrun.with_approval(request_id)`" | `approve` — `cli/main.py:377`; `with_approval` — `control.py:387`; `ApprovalRequired` (`errors.py:88`) carries `request_id` | `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch` (the granted path first), `test_T4_replaying_the_approval_raises_ApprovalMismatch_with_reason_consumed` | | "No agent changes" | `INTERCEPTED_METHOD` is `tools/call` and every other method is relayed unchanged — `gateway/mcp.py:40` | `test_a_non_intercepted_method_is_relayed_with_no_ctrlrun_outcome` | -| "Point the MCP client at the gateway instead of at the tool server" | `Gateway.handle` — `gateway/server.py:388`; `serve` — `gateway/__init__.py:41` | `test_T19_the_upstream_receives_the_canonical_arguments` | +| "Point the MCP client at the gateway instead of at the tool server" | `Gateway.handle` — `gateway/server.py:397`; `serve` — `gateway/__init__.py:41` | `test_T19_the_upstream_receives_the_canonical_arguments` | | "Tools become actions named `mcp..`" | `Gateway._intercept` — `gateway/server.py:442` | `test_T19_the_action_is_named_for_the_alias_and_the_tool` | -| "they are declared in the policy" (effect and resource templates for a tool call) | `Policy.effect_template` / `resource_template` — `policy.py:901`; `McpOptions` — `policy.py:543` | `test_T16_a_v2_document_loads_and_exposes_its_templates`, `test_T16_a_decorator_and_a_policy_template_produce_the_same_action_hash` | +| "they are declared in the policy" (effect and resource templates for a tool call) | `Policy.effect_template` / `resource_template` — `policy.py:979`; `McpOptions` — `policy.py:600` | `test_T16_a_v2_document_loads_and_exposes_its_templates`, `test_T16_a_decorator_and_a_policy_template_produce_the_same_action_hash` | | "Everything but `tools/call` is relayed untouched" | `parse_request(...).intercept` — `gateway/mcp.py:93` | `test_every_other_method_is_relayed_not_intercepted` | | "A lost response over the wire blocks the retry exactly as it does in process" | `classify` — `gateway/outcome.py:129`, translated into v0.1 §5.5's own vocabulary by the gateway's executor | `test_T23_the_identical_call_sent_again_is_refused_and_the_upstream_called_once` | -| "the gateway prints, on the line that starts it, every action in your policy that has no `effect:` template" | `_announce` — `gateway/__init__.py:146` | `test_the_startup_block_names_the_environment_identity_and_authority`, `test_the_startup_block_says_so_when_there_is_no_authority_section` | +| "the gateway prints, on the line that starts it, every action in your policy that has no `effect:` template" | `_announce` — `gateway/__init__.py:150` | `test_the_startup_block_names_the_environment_identity_and_authority`, `test_the_startup_block_says_so_when_there_is_no_authority_section` | | "route an `approve` decision through **the framework's own interrupt**" | `FrameworkInterrupt` — `adapter.py:180` — is a Protocol with one method returning a value; it holds no state and writes nothing | `test_T135b_the_adapter_reuses_the_sdks_primitive_and_reimplements_nothing` | | "one core provider writes the grant through the same calls `ctrlrun approve` makes" / "There is never a second place to say yes" | `InterruptApprovalProvider.wait` — `adapter.py:254` — calls `grant_approval` / `deny_approval`, and an adapter calls neither | `test_T130_each_broken_fixture_fails_the_suite_named_for_it` | | "an adapter never constructs one and never supplies a principal" | `needs_approval` — `adapter.py:428` — resolves the principal from the `Control` so no adapter builds an `Action` | `test_T129_no_public_callable_takes_a_principal`, `test_T129_the_module_exposes_no_way_to_construct_a_control` | @@ -85,24 +85,24 @@ by its quoted claim, and `tests/test_docs_audit.py` fails if a named row is not | Claim | Code | Proof | |---|---|---| -| "cheap to undo is autonomous, anything that leaves the building needs a human, money is by amount with both ends bound" | `Decision` — `policy.py:308` — is exactly `allow`, `approve`, `deny`; rules match first-wins over `Condition` (`policy.py:372`) with the operators `eq`, `neq`, `in`, `lt`, `lte`, `gt`, `gte` — `_OPERATORS` — `policy.py:119` | `test_T6_an_action_name_is_matched_exactly`, `test_T176_the_operators_behave_as_they_do_everywhere_else` | -| "Unknown actions are denied; there is no default-allow." | `Policy.evaluate` — `policy.py:596` | `test_T6_unknown_action_is_denied_with_reason_unknown_action` | +| "cheap to undo is autonomous, anything that leaves the building needs a human, money is by amount with both ends bound" | `Decision` — `policy.py:308` — is exactly `allow`, `approve`, `deny`; rules match first-wins over `Condition` (`policy.py:399`) with the operators `eq`, `neq`, `in`, `lt`, `lte`, `gt`, `gte` — `_OPERATORS` — `policy.py:119` | `test_T6_an_action_name_is_matched_exactly`, `test_T176_the_operators_behave_as_they_do_everywhere_else` | +| "Unknown actions are denied; there is no default-allow." | `Policy.evaluate` — `policy.py:655` | `test_T6_unknown_action_is_denied_with_reason_unknown_action` | | "Amounts are integer minor units; floats are rejected outright" | `float` refused at any depth — `action.py:79` | `test_T7_canonical_form_is_exactly_the_specified_serialization` | -| "The policy cannot see who is asking — deliberately, since v0.1" | `Policy.evaluate` still takes only the action's name and arguments; `RESERVED_ARGUMENTS` — `policy.py:596` — refuses `agent_eq` and every other principal-addressing condition at load, in a document of **every** schema version | `test_T74b_a_reserved_name_in_a_policy_rule_is_a_load_error`, `test_T74b_a_reserved_name_in_a_grant_constraint_is_a_load_error` | -| "the second axis, `authority:`" | `Authority.evaluate` — `authority.py:1248`; `Control._authority_result` — `control.py:1004` | `test_T67_a_principal_with_no_grant_is_denied` | +| "The policy cannot see who is asking — deliberately, since v0.1" | `Policy.evaluate` still takes only the action's name and arguments; `RESERVED_ARGUMENTS` — `policy.py:655` — refuses `agent_eq` and every other principal-addressing condition at load, in a document of **every** schema version | `test_T74b_a_reserved_name_in_a_policy_rule_is_a_load_error`, `test_T74b_a_reserved_name_in_a_grant_constraint_is_a_load_error` | +| "the second axis, `authority:`" | `Authority.evaluate` — `authority.py:1248`; `Control._authority_result` — `control.py:1010` | `test_T67_a_principal_with_no_grant_is_denied` | | "opt-in, and then fail-closed" | `_optional_authority` returns `None` for a document with no section — `control.py`; `Control.authority is None` is v0.2 behaviour exactly | `test_T66_a_document_with_no_authority_section_leaves_control_authority_none`, `test_T66_no_authority_event_is_appended_without_a_section`, and T66's session-wide guard in `tests/conftest.py` | | "every principal needs a grant and no grant means denied" | `NO_AUTHORITY` — the fail-closed default of `Authority.evaluate` (`authority.py:84`), reached for reads and for actions with no effect key alike | `test_T67_an_action_the_policy_allows_outright_still_needs_a_grant` | | "A grant carries no `decision:`" | `_GRANT_KEYS` — `authority.py` — is a closed set that does not contain `decision` | `test_T73b_grant_refuses_what_the_loader_refuses` | | "combine as the **stricter of the two**" | `Control.evaluate` returns the combined result — `control.py`; a denial on either axis is a denial | `test_T70_the_stricter_of_the_two_wins` | -| "narrow it at runtime with `ctrlrun delegate`" | `Control.delegate` — `control.py:3956`; `Authority.plan_delegation` — `authority.py:1500`; `ctrlrun delegate` — `cli/main.py:1045` | `test_t75_the_delegation_authorizes_an_action_within_its_limits` | +| "narrow it at runtime with `ctrlrun delegate`" | `Control.delegate` — `control.py:4007`; `Authority.plan_delegation` — `authority.py:1500`; `ctrlrun delegate` — `cli/main.py:1045` | `test_t75_the_delegation_authorizes_an_action_within_its_limits` | | "provably a subset of its parent on every dimension, at creation and again at every evaluation" | `contained_dimension` — `authority.py:946` — runs from `plan_delegation` (`authority.py:1500`) **and** from the chain walk in `Authority.evaluate` (`authority.py:1256`) | `test_t76_each_dimension_violated_alone`, `test_t77b_a_narrowed_parent_narrows_its_children` | | "provably a subset of its parent on every dimension, at creation and again at every evaluation; a consequence budget is consumed inside the reservation's own transaction, and a rolling window bounds what may start rather than recalling what already did" | Containment as in the row above. The budget: `check_charges` — `state.py:569` — is evaluated inside `reserve_effect`'s own transaction on all three backends, and `_charges_for` — `authority.py:1355` — charges every ancestor in the chain. The window is rolling and bounds the next reserve only: `_spent` sums `[now - window, now]` and nothing reads it again after a reservation is taken | `test_T408_a_charge_and_its_reservation_are_one_transaction`, `test_T409_N_processes_racing_one_budget_spend_at_most_the_limit`, `test_T412b_every_ancestor_is_charged_through_a_real_chain`, `test_T408c_the_rolling_window_forgets` | | "omitting a dimension the parent constrains is rejected rather than inherited" | `contained_dimension` treats an absent child dimension as unconstrained and therefore wider — `authority.py:946`; the subject half is `_subject_contained` (`authority.py:1025`) | `test_t81_omission_is_not_unlimited`, `test_T73b_a_subject_addressed_to_every_principal_is_refused`, `test_t76_each_dimension_violated_alone` | -| "`ctrlrun revoke` cuts a chain of any depth with one write" | `Control.revoke` — `control.py:4293` — writes one row — `revoke_delegation` — `state.py:835` and visits no children; every evaluation walks to the root | `test_t78_a_revoked_parent_denies_its_grandchild`, `test_put_delegation_is_never_an_upsert` | -| "`mode: observe` … records what *would* have been blocked, without blocking anything" | `_parse_mode` — `policy.py:719`; `Control._observed` — `control.py:1565`; `_WouldHave` — `receipt.py:344`; `ReceiptResult.OBSERVED` — `receipt.py:256` | `test_T82_observe_executes_what_enforce_would_deny`, `test_T83_a_duplicate_is_recorded_and_still_runs` | -| "One top-level line" | `mode:` is refused anywhere but the top level — `reject_nested_mode`, `policy.py:719` | `test_T84_mode_is_refused_anywhere_but_the_top_level` | +| "`ctrlrun revoke` cuts a chain of any depth with one write" | `Control.revoke` — `control.py:4344` — writes one row — `revoke_delegation` — `state.py:835` and visits no children; every evaluation walks to the root | `test_t78_a_revoked_parent_denies_its_grandchild`, `test_put_delegation_is_never_an_upsert` | +| "`mode: observe` … records what *would* have been blocked, without blocking anything" | `_parse_mode` — `policy.py:778`; `Control._observed` — `control.py:1571`; `_WouldHave` — `receipt.py:344`; `ReceiptResult.OBSERVED` — `receipt.py:256` | `test_T82_observe_executes_what_enforce_would_deny`, `test_T83_a_duplicate_is_recorded_and_still_runs` | +| "One top-level line" | `mode:` is refused anywhere but the top level — `reject_nested_mode`, `policy.py:778` | `test_T84_mode_is_refused_anywhere_but_the_top_level` | | "`ctrlrun stats` gives you the numbers" | `stats` — `cli/main.py:866`; counted from `would_have.blocked_reason` and nothing else | `test_T86_stats_counts_what_observe_mode_recorded`, `test_T86_stats_reaches_no_network` | -| "It is not a dry run: it executes" | `_observed` runs the executor on every path, including the ones enforce mode would have refused — `control.py:1565` | `test_T82_observe_executes_what_enforce_would_deny`, `test_T83_an_executor_that_fails_on_a_held_key_still_writes_the_record` | +| "It is not a dry run: it executes" | `_observed` runs the executor on every path, including the ones enforce mode would have refused — `control.py:1571` | `test_T82_observe_executes_what_enforce_would_deny`, `test_T83_an_executor_that_fails_on_a_held_key_still_writes_the_record` | ## Prove it holds in your setup @@ -139,8 +139,8 @@ catalogue, `GUARANTEES` (`verify/guarantees.py:49`). |---|---|---| | "An approval is bound to the exact action; a mutated or replayed one is refused." | `action_hash` — `action.py`; the approval record stores it and `_authorize_and_reserve` compares it — `state.py:665`; single use is the `granted → consumed` transition in the same `BEGIN IMMEDIATE` | `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch`, `test_T4_replaying_the_approval_raises_ApprovalMismatch_with_reason_consumed`, `test_T5_expiry_is_checked_at_consumption_not_only_at_grant` | | "One logical effect happens at most once, across threads, processes and hosts." | `reserve_effect` — `state.py:637`, decided inside the `BEGIN IMMEDIATE` of `_authorize_and_reserve` (`state.py:1196`) against `effect_key TEXT PRIMARY KEY` (`migrations.py:107`; `COLLATE "C"` on Postgres, §4.4) | `test_T3_exactly_one_agent_reserves_and_seven_are_blocked` (8 OS processes, both backends), `test_T3_the_fake_remote_is_called_exactly_once` | -| "An unknown outcome is AMBIGUOUS, never FAILED, and blocks a blind retry." | Only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2054`. Every other exception, timeouts included, yields `AMBIGUOUS`. A retry against an `AMBIGUOUS` key is refused — `effect.py:172`, the one place `plan_reservation` decides it for every store | `test_T1_a_blind_retry_writes_a_blocked_receipt`, `test_T1_the_ambiguous_record_survives_the_blocked_retry`, `test_T1_a_lost_response_leaves_the_effect_ambiguous` | -| "An unknown action, a missing policy or a missing principal is denied." | Unknown action: `Policy.evaluate` — `policy.py:596` — answers `deny` for a name the document does not list. Missing or malformed policy: `Policy.from_file` — `policy.py:753` — raises `PolicyError`, and there is no `Control` without a policy. Missing principal: `_refuse_no_principal` — `control.py:4626` | `test_T6_unknown_action_raises_ActionDenied_with_reason_unknown_action`, `test_missing_policy_file_is_a_policy_error`, `test_malformed_policy_document_is_a_policy_error`, `test_T62_a_declining_provider_with_no_context_is_no_principal` | +| "An unknown outcome is AMBIGUOUS, never FAILED, and blocks a blind retry." | Only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2065`. Every other exception, timeouts included, yields `AMBIGUOUS`. A retry against an `AMBIGUOUS` key is refused — `effect.py:172`, the one place `plan_reservation` decides it for every store | `test_T1_a_blind_retry_writes_a_blocked_receipt`, `test_T1_the_ambiguous_record_survives_the_blocked_retry`, `test_T1_a_lost_response_leaves_the_effect_ambiguous` | +| "An unknown action, a missing policy or a missing principal is denied." | Unknown action: `Policy.evaluate` — `policy.py:655` — answers `deny` for a name the document does not list. Missing or malformed policy: `Policy.from_file` — `policy.py:812` — raises `PolicyError`, and there is no `Control` without a policy. Missing principal: `_refuse_no_principal` — `control.py:4677` | `test_T6_unknown_action_raises_ActionDenied_with_reason_unknown_action`, `test_missing_policy_file_is_a_policy_error`, `test_malformed_policy_document_is_a_policy_error`, `test_T62_a_declining_provider_with_no_context_is_no_principal` | | "With authority on, every principal needs a grant, and delegation cannot widen one." | `NO_AUTHORITY` — the fail-closed default of `Authority.evaluate` (`authority.py:84`); `contained_dimension` — `authority.py:946` — runs from `plan_delegation` (`authority.py:1500`) and from the chain walk in `Authority.evaluate` | `test_T67_a_principal_with_no_grant_is_denied`, `test_t76_each_dimension_violated_alone` | | "Every executed action leaves a portable JSON receipt" | `ReceiptResult` — `receipt.py:240`; `Event` — `receipt.py:306`; the store is authoritative — `append_event` — `state.py:846`; the JSONL export — `JSONLEventSink` — `receipt.py:817` | `test_T11_every_demo_receipt_carries_every_field_in_the_spec`, `test_T11_every_demo_receipt_parses_back_into_a_Receipt` | @@ -152,22 +152,22 @@ catalogue, `GUARANTEES` (`verify/guarantees.py:49`). | "a unique index on the effect key and compare-and-set updates whose row counts are checked" | `reserve_effect` — `postgres.py:740` — `INSERT … ON CONFLICT DO NOTHING` against `effect_key TEXT PRIMARY KEY COLLATE "C"` (`migrations.py:107`) | `test_T3_exactly_one_agent_reserves_and_seven_are_blocked` (8 OS processes, both backends) | | "Same `StateStore` protocol, extended by nothing" | `PostgresStateStore.reserve_effect` — `postgres.py:740` — and every other method implement `v0.1 §5.3`'s frozen protocol; the decisions stay in `plan_reservation` (`effect.py:248`) | `test_T154_postgres_passes_the_store_conformance_suite` | | "graded by the suite written for SQLite" | `ctrlrun.conformance.store.run` — `conformance/store/__init__.py:53` | `test_T140_every_fixture_fails_the_suite_named_for_it` | -| "It will not *knowingly* execute the same logical effect twice, and will never treat an unknown outcome as a failure." | `plan_reservation` — `effect.py:248` (refuse retry on `AMBIGUOUS`) and `_outcome` — `control.py:2054` (only `NotExecuted` → `FAILED`) | `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote`, `test_T1_a_lost_response_leaves_the_effect_ambiguous` | -| "a lost connection during `COMMIT` ... are `AMBIGUOUS`" | `_resolve_lost_insert` — `postgres.py:931`; `_resolve_lost_update` — `postgres.py:1557`; only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2054` | `test_T155_a_connection_killed_during_commit_is_resolved_by_the_re_read`, `test_T155_no_effect_is_ever_recorded_failed_by_a_lost_commit` | +| "It will not *knowingly* execute the same logical effect twice, and will never treat an unknown outcome as a failure." | `plan_reservation` — `effect.py:248` (refuse retry on `AMBIGUOUS`) and `_outcome` — `control.py:2065` (only `NotExecuted` → `FAILED`) | `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote`, `test_T1_a_lost_response_leaves_the_effect_ambiguous` | +| "a lost connection during `COMMIT` ... are `AMBIGUOUS`" | `_resolve_lost_insert` — `postgres.py:931`; `_resolve_lost_update` — `postgres.py:1557`; only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2065` | `test_T155_a_connection_killed_during_commit_is_resolved_by_the_re_read`, `test_T155_no_effect_is_ever_recorded_failed_by_a_lost_commit` | | "the store re-reads the row to find out which" | The six branches, named and logged — `A2_LANDED` — `postgres.py:140` | `test_T155b_a_landed_commit_on_a_transition_is_seen_as_landed`, `test_T155d_a_commit_the_server_never_received_retries_the_insert` | | "A crashed worker's effect stays `AMBIGUOUS` until a human runs `ctrlrun resolve` or a `reconcile` hook asks the remote what happened" | An expired lease is `AMBIGUOUS` and nothing sweeps it — `LEASE_EXPIRED` — `effect.py:172`; who resolved it — `resolved_by` — `effect.py:208`; `resolve` — `cli/main.py:587` | `test_T159_ambiguous_survives_a_restart_and_still_refuses_a_blind_retry`, `test_T160_there_is_no_reaper`, `test_T161_a_human_resolution_records_who` | -| "the only thing besides a human permitted to move a record out of `AMBIGUOUS`" | `Control._reconciled` — `control.py:2702`; `RECONCILED_STATES` — `effect.py` | `test_T13_a_hook_answering_not_executed_moves_the_record_to_failed`, `test_T14_a_hook_answering_committed_refuses_the_retry_as_a_duplicate` | +| "the only thing besides a human permitted to move a record out of `AMBIGUOUS`" | `Control._reconciled` — `control.py:2719`; `RECONCILED_STATES` — `effect.py` | `test_T13_a_hook_answering_not_executed_moves_the_record_to_failed`, `test_T14_a_hook_answering_committed_refuses_the_retry_as_a_duplicate` | | "and only in the direction its answer points" | `"unknown"` is absent from `RECONCILED_STATES` — `effect.py` | `test_T15_a_hook_that_cannot_answer_leaves_the_record_ambiguous` | -| "Unknown action, missing policy, malformed policy, missing principal, missing or mismatched approval and inconsistent state are all `deny`." | `Policy.evaluate` — `policy.py:596`; `Policy.from_file` — `policy.py:753`; `_refuse_no_principal` — `control.py:4626`; `_authorize_and_reserve` — `state.py:1196` | `test_T6_unknown_action_raises_ActionDenied_with_reason_unknown_action`, `test_malformed_policy_document_is_a_policy_error`, `test_T62_a_declining_provider_with_no_context_is_no_principal`, `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch` | -| "No flag makes a consequential action permissive by default" | There is no such option on `Control`, on `@protect`, on the CLI or in the policy schema's closed key sets — `_TOP_LEVEL_KEYS` — `policy.py:123` | `test_T84_mode_is_refused_anywhere_but_the_top_level`, `test_T101b_zero_applicable_guarantees_is_not_a_pass` | -| "With `authority:` on, every principal needs a grant, delegation cannot widen one, and `ctrlrun revoke` cuts a chain with one write." | `Authority.evaluate` — `authority.py:1248`; `contained_dimension` — `authority.py:946`; `Control.revoke` — `control.py:4293` | `test_T67_a_principal_with_no_grant_is_denied`, `test_t76_each_dimension_violated_alone`, `test_t78_a_revoked_parent_denies_its_grandchild` | +| "Unknown action, missing policy, malformed policy, missing principal, missing or mismatched approval and inconsistent state are all `deny`." | `Policy.evaluate` — `policy.py:655`; `Policy.from_file` — `policy.py:812`; `_refuse_no_principal` — `control.py:4677`; `_authorize_and_reserve` — `state.py:1196` | `test_T6_unknown_action_raises_ActionDenied_with_reason_unknown_action`, `test_malformed_policy_document_is_a_policy_error`, `test_T62_a_declining_provider_with_no_context_is_no_principal`, `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch` | +| "No flag makes a consequential action permissive by default" | There is no such option on `Control`, on `@protect`, on the CLI or in the policy schema's closed key sets — `_TOP_LEVEL_KEYS` — `policy.py:127` | `test_T84_mode_is_refused_anywhere_but_the_top_level`, `test_T101b_zero_applicable_guarantees_is_not_a_pass` | +| "With `authority:` on, every principal needs a grant, delegation cannot widen one, and `ctrlrun revoke` cuts a chain with one write." | `Authority.evaluate` — `authority.py:1248`; `contained_dimension` — `authority.py:946`; `Control.revoke` — `control.py:4344` | `test_T67_a_principal_with_no_grant_is_denied`, `test_t76_each_dimension_violated_alone`, `test_t78_a_revoked_parent_denies_its_grandchild` | | "verifies a bearer token against a JWKS or a pinned key" | `JWTIdentityProvider._verified` — `jwt_identity.py:208`; the algorithm comes from the configured list and never from the token | `test_T88_a_valid_token_becomes_a_principal`, `test_T89_every_invalid_token_is_refused_by_cause` | | "maps the verified claims onto a principal" | `_principal` — `jwt_identity.py` — copies only the claims named in `claim_names` | `test_T88_only_the_named_claims_reach_the_principal` | | "`pip install \"ctrlrun[identity]\"`" | `identity = ["pyjwt[crypto]>=2.8"]` in `pyproject.toml`; imported lazily by `_jwt()` — `jwt_identity.py` | `test_T92_constructing_without_the_extra_names_the_install_command`, `test_T92_importing_ctrlrun_pulls_in_no_jwt_module` | | "CTRLRun issues no credential and defines no identity format" | There is no minting, signing or issuing code path in the package: `jwt_identity.py` calls `decode` and never `encode` | `test_the_package_never_encodes_a_token` | -| "every receipt records which policy decided it" | `Policy.policy_hash` — `policy.py:734`, over `_canonical_policy` — `policy.py:963`; carried into the receipt by `_record` — `control.py:4517` | `test_T172_every_receipt_carries_the_hash_and_the_declared_version`, `test_T172_two_policies_sharing_a_version_string_are_told_apart_by_the_hash` | -| "the policy's declared `version:` and a hash of its canonical content" | `version:` is recorded and never authoritative; `policy_hash` is what tells two documents apart — `policy.py:724` | `test_T171_the_declared_version_alone_does_not_change_the_hash`, `test_T171_comments_key_order_and_whitespace_do_not_change_the_hash` | -| "the approval is re-checked against the policy in force at execution" | `Control.execute` — `control.py:1181`; `_spend_unneeded_approval` — `control.py:2642` | `test_T173_the_DENY_row_refuses_and_leaves_the_approval_granted`, `test_T173_the_ALLOW_row_invalidates_the_approval_it_did_not_need` | +| "every receipt records which policy decided it" | `Policy.policy_hash` — `policy.py:793`, over `_canonical_policy` — `policy.py:1041`; carried into the receipt by `_record` — `control.py:4568` | `test_T172_every_receipt_carries_the_hash_and_the_declared_version`, `test_T172_two_policies_sharing_a_version_string_are_told_apart_by_the_hash` | +| "the policy's declared `version:` and a hash of its canonical content" | `version:` is recorded and never authoritative; `policy_hash` is what tells two documents apart — `policy.py:783` | `test_T171_the_declared_version_alone_does_not_change_the_hash`, `test_T171_comments_key_order_and_whitespace_do_not_change_the_hash` | +| "the approval is re-checked against the policy in force at execution" | `Control.execute` — `control.py:1187`; `_spend_unneeded_approval` — `control.py:2659` | `test_T173_the_DENY_row_refuses_and_leaves_the_approval_granted`, `test_T173_the_ALLOW_row_invalidates_the_approval_it_did_not_need` | | "Each receipt carries the hash of the one before it" | `Receipt.chain_hash` — `receipt.py:533`; `prev_hash` — `receipt.py:452`; `GENESIS_HASH` — `receipt.py:125`; `put_receipt` takes the head row's lock first — `postgres.py:2092` | `test_T164_an_altered_receipt_is_content_altered_at_its_seq`, `test_T164_reordering_two_receipts_is_detected_either_way` | | "`ctrlrun receipts --verify-chain` reports it by `seq`" | `verify_chain` — `receipt.py:935`; the six names — `CHAIN_BREAKS` — `receipt.py:872` | `test_the_verify_chain_flag_reports_a_break_by_seq_and_by_name`, `test_verify_chain_reads_a_postgres_store_through_store_url` | | "migrations are automatic at open, forward-only" | `migrate` — `migrations.py:633`, called from both stores' constructors; `HEAD` — `migrations.py:414` | `test_T147_a_v05_database_migrates_and_keeps_every_row`, `test_T150_reopening_does_not_rerun` | @@ -177,8 +177,8 @@ catalogue, `GUARANTEES` (`verify/guarantees.py:49`). | "`WebhookApprovalProvider` sends an approval request to a webhook, such as Slack, and takes the answer back through the same grant calls" | `WebhookApprovalProvider` — `webhook.py:141` — one signed POST on `APPROVAL_REQUESTED`; the inbound answer lands through `grant_approval` / `deny_approval` like the CLI's | `test_T27_the_outbound_post_carries_a_signature_over_the_exact_bytes_sent`, `test_T27_the_payload_carries_what_the_spec_names` | | "one OpenTelemetry span per action, one span event per step" | `OTelEventSink` — `otel.py:45` | `test_T29_one_action_produces_one_span_named_for_the_action`, `test_T29_every_event_becomes_a_span_event_named_by_its_type` | | "argument values stay out of it unless you ask for them" | `OTelEventSink(arguments=...)` — `otel.py:45` | `test_T29_argument_values_are_not_attributes_by_default` | -| "Receipts in a `ctrlrun.policy/v4` document can cite the `controls:` an action satisfies" | `PolicyControl` — `policy.py:327`; `Receipt` — `receipt.py:398` — carries `controls`; attribution only, never a decision | `test_T175_the_receipt_carries_the_union_of_the_action_and_the_matched_rule`, `test_T175_a_control_is_attribution_and_changes_no_decision` | -| "a rule can condition on the `data:` labels present in an action's arguments" | `DataLabel` — `policy.py:568`; `Policy.data_scope` — `policy.py:588`; `data_scope_in` in `v0.1 §3.2`'s grammar with no new operator | `test_T176_the_derived_set_is_the_labels_of_the_arguments_actually_supplied`, `test_T176_the_derived_set_drives_a_decision` | +| "Receipts in a `ctrlrun.policy/v4` document can cite the `controls:` an action satisfies" | `PolicyControl` — `policy.py:360`; `Receipt` — `receipt.py:398` — carries `controls`; attribution only, never a decision | `test_T175_the_receipt_carries_the_union_of_the_action_and_the_matched_rule`, `test_T175_a_control_is_attribution_and_changes_no_decision` | +| "a rule can condition on the `data:` labels present in an action's arguments" | `DataLabel` — `policy.py:627`; `Policy.data_scope` — `policy.py:647`; `data_scope_in` in `v0.1 §3.2`'s grammar with no new operator | `test_T176_the_derived_set_is_the_labels_of_the_arguments_actually_supplied`, `test_T176_the_derived_set_drives_a_decision` | ## What it can't, stated as limits @@ -190,7 +190,7 @@ The README also makes negative claims. They matter as much as the positive ones. | "CTRLRun is not a transaction manager: it rolls nothing back" | There is no compensation, saga or rollback code path in the package; an `AMBIGUOUS` effect is resolved by a human or a reconcile hook and never undone — `RECONCILED_STATES` — `effect.py` | | "The receipt chain detects alteration, and alteration is not authorship." | n/a — a disclaimer, and the scan that keeps it one: `test_T180_the_release_documents_do_not_blur_alteration_and_authorship` | | "erasing the end of the log costs two statements" | No code — this is what the chain does **not** cover, and it is asserted rather than argued: `test_erasing_a_suffix_and_rewinding_the_head_is_two_statements_and_undetected` | -| "CTRLRun does not detect prompt injection" | No code — and that is the point. Nothing in the package reads the agent's instructions: `Policy.evaluate` takes the action's name and arguments (`policy.py:596`) and `Authority` matches a grant against the action, so neither axis has the prompt to inspect. The README's problem table claims containment of the consequence, and this row is the sentence that stops it being read as detection. | `test_T6_an_action_name_is_matched_exactly`, `test_a_condition_naming_an_action_field_is_refused_at_load` | +| "CTRLRun does not detect prompt injection" | No code — and that is the point. Nothing in the package reads the agent's instructions: `Policy.evaluate` takes the action's name and arguments (`policy.py:655`) and `Authority` matches a grant against the action, so neither axis has the prompt to inspect. The README's problem table claims containment of the consequence, and this row is the sentence that stops it being read as detection. | `test_T6_an_action_name_is_matched_exactly`, `test_a_condition_naming_an_action_field_is_refused_at_load` | | "`ctrlrun verify` cannot see your executors" | `docs/verify.md`, "What it does not mean"; `THREAT_MODEL.md`, "Known v0.4 limitations" | | "`ctrlrun scan` … reports the consequential call sites and policy entries CTRLRun is **not** covering" and "has no score, no percentage and no badge" | `ctrlrun/scan/` reads the tree with `ast` and never imports it, resolves no principal, evaluates no policy and opens no store (SPEC-scan §9.2); the limits sentence is emitted on every run including a clean one, and no percentage is computed anywhere | `test_T194_scan_never_imports_the_tree_it_reads`, `test_T205_scan_resolves_no_principal_evaluates_no_policy_and_opens_no_store`, `test_T203_the_limits_sentence_is_in_every_run_including_a_clean_one` | | "`ctrlrun mcp-operator` … It authenticates who answered and records it; it does not check that they were entitled to." | the write tools refuse without a principal and attribute the answer to the verified one; there is no entitlement check, and `docs/SPEC-mcp-operator.md` §10 says so | `test_T184_approve_refuses_without_a_principal`, `test_T184_approve_succeeds_with_one_and_is_attributed`, `test_T183_there_is_no_flag_that_permits_a_remote_bind` | @@ -204,7 +204,7 @@ The README also makes negative claims. They matter as much as the positive ones. | "automatic at open and forward-only, with no flag that opens a database un-migrated. An older binary against a newer schema refuses immediately." | `migrate` — `migrations.py:633` — called from both stores' constructors; `_refuse` — `migrations.py:559` — raises `SchemaMismatch` on a newer `user_version` | `test_T147_a_v05_database_migrates_and_keeps_every_row`, `test_T148_an_older_binary_refuses_a_newer_database`, `test_T152b_no_flag_opens_a_database_without_migrating` | | "an edit, a deletion from the middle or a reordering is detected and named by `seq`" | `verify_chain` — `receipt.py:450` — and the six break names in `CHAIN_BREAKS` | `test_T164_an_altered_receipt_is_content_altered_at_its_seq`, `test_T164_reordering_two_receipts_is_detected_either_way`, `test_the_verify_chain_flag_reports_a_break_by_seq_and_by_name` | | "It detects **alteration**, which is not authorship: receipts are not signed." | No signing code, and a release scan keeps the vocabulary out | `test_T180_the_release_documents_do_not_blur_alteration_and_authorship` | -| "every receipt records the policy that decided it, so a receipt from six months ago says what the rules were" | `Policy.policy_hash` — `policy.py:734` — over the parsed decision inputs, recorded on the receipt | `test_T172_every_receipt_carries_the_hash_and_the_declared_version`, `test_T171_any_decision_input_changes_the_hash`, `test_T171_the_declared_version_alone_does_not_change_the_hash` | +| "every receipt records the policy that decided it, so a receipt from six months ago says what the rules were" | `Policy.policy_hash` — `policy.py:793` — over the parsed decision inputs, recorded on the receipt | `test_T172_every_receipt_carries_the_hash_and_the_declared_version`, `test_T171_any_decision_input_changes_the_hash`, `test_T171_the_declared_version_alone_does_not_change_the_hash` | ## The docs site: Home and Concepts @@ -228,7 +228,7 @@ restating the code; the ones that are new to the site carry their own code and p | `concepts/outcomes-and-ambiguous` | the outcome table; only a human or a reconcile hook moves a record on, and only in the direction the answer points; nothing sweeps; a lost `COMMIT` on Postgres is `AMBIGUOUS` | the matrix row "An unknown outcome is AMBIGUOUS…", the reconciliation rows, "A crashed worker's effect stays `AMBIGUOUS`…" and the Postgres rows above; `test_T160_there_is_no_reaper` | | `concepts/receipts-and-evidence` | the receipt's fields, the JSONL sink, the policy hash and version, the chain and what it does not prove | the matrix row "Every executed action leaves a portable JSON receipt", the receipt-chain and policy-versioning rows above, and `test_T11_every_demo_receipt_carries_every_field_in_the_spec` | | `concepts/authority-and-delegation` | opt-in then fail-closed, no `decision:` on a grant, stricter of the two, containment at creation and at every evaluation, omission rejected, one-write revocation, identity consumed | the authority rows under "Write down what the agent may do" and "What it guarantees" above | -| `concepts/observe-mode` | executes, records `would_have`, one top-level line, counted by `ctrlrun stats`, never asks a human | the observe-mode rows above; `_observed` — `control.py:4569` | +| `concepts/observe-mode` | executes, records `would_have`, one top-level line, counted by `ctrlrun stats`, never asks a human | the observe-mode rows above; `_observed` — `control.py:4620` | | `concepts/fail-closed` | the refusal table, one exception per row | the matrix row "An unknown action, a missing policy or a missing principal is denied.", `ActionDenied` — `errors.py:29`, `DuplicateEffect` — `errors.py:126`, `AmbiguousEffect` — `errors.py:141`, and `test_a_policy_deny_is_denied_the_same_way_as_an_unknown_action` | ## The docs site: Production diff --git a/docs/OWASP-AGENTIC-TOP10.md b/docs/OWASP-AGENTIC-TOP10.md index 69f2a22..c82dd89 100644 --- a/docs/OWASP-AGENTIC-TOP10.md +++ b/docs/OWASP-AGENTIC-TOP10.md @@ -89,6 +89,7 @@ mechanism, not the entry. | **G24** grant refused off its task | A grant may name `tasks:`, the unit of work the authority is for, attenuated by the same `child ⊆ parent` rule as actions, resources and environments. An action bound to a task the grant does not cover is refused by reason and not by exception type. A grant that names no `tasks:` authorises any task, so every grant written before v0.9 keeps its meaning. | `ASI01:2026` (partly), `ASI03:2026` (partly), `ASI10:2026` (partly) | **Task binding limits blast radius; it does not detect a hijack.** The task id is supplied by the caller, so an agent that has been talked into a different goal will often still be operating inside the task it was legitimately given, and the kernel cannot tell a hijacked run from an honest one. What it stops is a credential or a delegation issued for one unit of work being spent on another, which is the shape a stolen grant most often takes. The task never enters the action hash, so binding one changes no existing receipt. | | **G25** a hop narrows or it is refused | When one agent hands work to another across a boundary the kernel does not control, what the receiver holds is a *hop*: a delegation record in the store, narrowed on every dimension the issuing grant constrains, refused by name on the dimension it would widen, and with nothing inherited by omission. An action proposed under a hop is evaluated against that hop's grant alone, with no fallback to any other grant the receiver happens to hold, and what it spends is the issuing agent's budget, so a hop never creates a second root. No flag admits a wider hop. | `ASI03:2026` (partly), `ASI07:2026` (partly), `ASI10:2026` (partly) | This is the multi-agent shape of the containment rule that has bounded delegation since v0.3: the boundary changes nothing about what a receiver may do. What it stops is privilege growing as work moves between agents, and an agent two hops down holding more than the root granted. Two limits. **The kernel decides which grant a hop-borne action is judged by; it cannot compel a receiver to present its hop.** A receiver that holds a grant of its own and proposes the action without naming the hop is judged by that grant, and the receipt records which of the two happened. And **a hop does not cross stores**: agents whose stores differ get a refusal by name, not a weaker check. | | **G26** a hop is named on both sides | Both ends of a hop are in the evidence. The receiver's receipt records the hop it ran under, and the issuer's `DELEGATION_CREATED` event names the hop it created and, when the issuer supplied it, the action that created it, so the same hop id is found from either side's records without the other side's cooperation. Who the receiver is comes from the deployment's `IdentityProvider` and is never read off the payload. | `ASI07:2026` (partly), `ASI03:2026` (partly), `ASI10:2026` (partly) | What crosses the boundary is a reference of two strings to a record in the store, not a claim in a token, so there is nothing to forge, replay or widen in transit and nothing to verify but the record. Two limits. **The channel is not CTRLRun's**: it defines no wire format, no agent card and no transport, so how the reference travels and whether the transport authenticates its peers is the deployment's, and a caller the `IdentityProvider` resolves as the receiving principal *is* that principal. And **a relay's created hop is tied to the action that created it only when the relay supplies `action_id`**; the kernel refuses to infer the link, because an inferred one fails in the evidence direction. | +| **G27** a swapped upstream is denied | An action entry may pin the gateway's upstream: the SHA-256 of its TLS leaf certificate, the certificate itself as the connection's only trust anchor, and the digest of the tool's `tools/list` entry. At decision time the pin is compared against what this process last observed for that upstream, and the action is denied `upstream_mismatch` where it differs and `upstream_unverified` where nothing has been observed, so a pin never switches itself off by being unobserved. A pinned certificate makes a swapped server fail the handshake before the first request byte; a mismatch at startup refuses to start. | `ASI02:2026` (partly), `ASI07:2026` (partly) | The sharp case is an approved action name with a different server, or a different tool schema, underneath it: every grant still matches and the receipt still says `stripe.refund`. This closes that one shape and not the category. The decision-time check attributes and the handshake check prevents: the gateway forwards after deciding, so a swap timed between the two is caught at the handshake, not by the DENY. Three limits. **Only the gateway can pin**, because pinning means holding the connection; the in-process decorator and the ACS hook refuse rather than pretend. **A digest-only pin gets no handshake check**, and the startup line says so. And **`ASI04` keeps its verdict below**: nothing here inspects a package, a model, a registry, a build or a signature chain. | --- @@ -98,7 +99,7 @@ The half that makes the table above credible. One honest sentence each; nothing | Entry | Title | Why not | |---|---|---| -| `ASI04:2026` | Agentic Supply Chain Vulnerabilities | Out of scope. CTRLRun never inspects a package, a model, a tool registry or an MCP server's provenance; it decides actions, and a poisoned dependency reaches it as an ordinary caller. | +| `ASI04:2026` | Agentic Supply Chain Vulnerabilities | Out of scope. CTRLRun never inspects a package, a model, a tool registry or an MCP server's provenance; it decides actions, and a poisoned dependency reaches it as an ordinary caller. Since v0.10, G27 covers one slice and no more: an action entry that pins its upstream's certificate or its tool's schema is denied against a server that is not the pinned one. | | `ASI05:2026` | Unexpected Code Execution | Out of scope. Nothing here sandboxes an interpreter or constrains what a process may run. CTRLRun sits between an agent and one remote effect, not between an agent and its own runtime. | And the four entries where the mapping above is **partial**, with the part that is not covered diff --git a/docs/cookbook/verify-in-github-actions.mdx b/docs/cookbook/verify-in-github-actions.mdx index b2b5b89..7c764c0 100644 --- a/docs/cookbook/verify-in-github-actions.mdx +++ b/docs/cookbook/verify-in-github-actions.mdx @@ -103,11 +103,12 @@ G23 a failing scope provider refuses N/A no action this configuration admits G24 grant refused off its task N/A no authority section 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 (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) -15/15 declared guarantees pass. 11 not applicable: G8, G9, G13, G15, G17, G19, G22, G23, G24, G25, G26. +15/15 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 diff --git a/docs/guides/verify-in-ci.mdx b/docs/guides/verify-in-ci.mdx index 6b40170..2ae0649 100644 --- a/docs/guides/verify-in-ci.mdx +++ b/docs/guides/verify-in-ci.mdx @@ -73,11 +73,12 @@ guarantees pass. G24 grant refused off its task N/A no authority section 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 (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) - 15/15 declared guarantees pass. 11 not applicable: G8, G9, G13, G15, G17, G19, G22, G23, G24, G25, G26. + 15/15 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: an action with no principal is diff --git a/docs/production/index.mdx b/docs/production/index.mdx index 2f29bee..695605d 100644 --- a/docs/production/index.mdx +++ b/docs/production/index.mdx @@ -28,8 +28,8 @@ 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.9.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later. -- **6,005 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). -- **26 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). +- **6,019 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). - **Each receipt carries the hash of the one before it**, so an alteration is detected and named. [Read more](/docs/production/receipt-integrity). diff --git a/docs/reference/api/Condition.mdx b/docs/reference/api/Condition.mdx index 78d1465..8d591f6 100644 --- a/docs/reference/api/Condition.mdx +++ b/docs/reference/api/Condition.mdx @@ -5,7 +5,7 @@ description: "One `_: 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:371` +`ctrlrun.Condition` — class, defined at `src/ctrlrun/policy.py:398` ```python from ctrlrun import Condition diff --git a/docs/reference/api/Control.mdx b/docs/reference/api/Control.mdx index 05e37e5..4786a20 100644 --- a/docs/reference/api/Control.mdx +++ b/docs/reference/api/Control.mdx @@ -5,7 +5,7 @@ description: "Policy, state and evidence composed around a single action (SPEC-v {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.Control` — class, defined at `src/ctrlrun/control.py:717` +`ctrlrun.Control` — class, defined at `src/ctrlrun/control.py:718` ```python from ctrlrun import Control @@ -14,7 +14,7 @@ from ctrlrun import Control ```python class Control - def __init__(policy: Policy, store: StateStore, approvals: ApprovalProvider | None = None, *, clock: Callable[[], datetime] = _utc_now, approval_ttl: timedelta = DEFAULT_APPROVAL_TTL, lease: timedelta = DEFAULT_LEASE, sinks: Sequence[EventSink] = (), suspend_timeout: timedelta = DEFAULT_SUSPEND_TIMEOUT, identity: IdentityProvider | None = None, authority: Authority | None = None, environment: str | None = None, approver_identity: ApproverIdentity | None = None, require_approved_policy: bool = False) + def __init__(policy: Policy, store: StateStore, approvals: ApprovalProvider | None = None, *, clock: Callable[[], datetime] = _utc_now, approval_ttl: timedelta = DEFAULT_APPROVAL_TTL, lease: timedelta = DEFAULT_LEASE, sinks: Sequence[EventSink] = (), suspend_timeout: timedelta = DEFAULT_SUSPEND_TIMEOUT, identity: IdentityProvider | None = None, authority: Authority | None = None, environment: str | None = None, approver_identity: ApproverIdentity | None = None, require_approved_policy: bool = False, upstream: str | None = None) ``` Policy, state and evidence composed around a single action (SPEC-v0.1 §8). diff --git a/docs/reference/api/Decision.mdx b/docs/reference/api/Decision.mdx index c3de473..758e88a 100644 --- a/docs/reference/api/Decision.mdx +++ b/docs/reference/api/Decision.mdx @@ -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:308` +`ctrlrun.Decision` — class, defined at `src/ctrlrun/policy.py:335` ```python from ctrlrun import Decision diff --git a/docs/reference/api/Policy.mdx b/docs/reference/api/Policy.mdx index 8189ebd..1e5ea45 100644 --- a/docs/reference/api/Policy.mdx +++ b/docs/reference/api/Policy.mdx @@ -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:700` +`ctrlrun.Policy` — class, defined at `src/ctrlrun/policy.py:759` ```python from ctrlrun import Policy diff --git a/docs/reference/api/context.mdx b/docs/reference/api/context.mdx index da74139..094bd35 100644 --- a/docs/reference/api/context.mdx +++ b/docs/reference/api/context.mdx @@ -5,7 +5,7 @@ description: "Bind the principal for calls made inside the block." {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.context` — function, defined at `src/ctrlrun/control.py:362` +`ctrlrun.context` — function, defined at `src/ctrlrun/control.py:363` ```python from ctrlrun import context diff --git a/docs/reference/api/idempotency_token.mdx b/docs/reference/api/idempotency_token.mdx index db6d8f2..92d9b36 100644 --- a/docs/reference/api/idempotency_token.mdx +++ b/docs/reference/api/idempotency_token.mdx @@ -5,7 +5,7 @@ description: "The provider idempotency token for the attempt this executor is ru {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.idempotency_token` — function, defined at `src/ctrlrun/control.py:330` +`ctrlrun.idempotency_token` — function, defined at `src/ctrlrun/control.py:331` ```python from ctrlrun import idempotency_token diff --git a/docs/reference/api/parse_conditions.mdx b/docs/reference/api/parse_conditions.mdx index ef816d6..c9eee51 100644 --- a/docs/reference/api/parse_conditions.mdx +++ b/docs/reference/api/parse_conditions.mdx @@ -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:1248` +`ctrlrun.parse_conditions` — function, defined at `src/ctrlrun/policy.py:1326` ```python from ctrlrun import parse_conditions diff --git a/docs/reference/api/protect.mdx b/docs/reference/api/protect.mdx index d121baa..d9844a9 100644 --- a/docs/reference/api/protect.mdx +++ b/docs/reference/api/protect.mdx @@ -5,7 +5,7 @@ description: "Bind a function to an action name: every call becomes a decided, r {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.protect` — function, defined at `src/ctrlrun/control.py:4744` +`ctrlrun.protect` — function, defined at `src/ctrlrun/control.py:4795` ```python from ctrlrun import protect diff --git a/docs/reference/api/with_approval.mdx b/docs/reference/api/with_approval.mdx index 529c276..c4cdc67 100644 --- a/docs/reference/api/with_approval.mdx +++ b/docs/reference/api/with_approval.mdx @@ -5,7 +5,7 @@ description: "Present a granted approval to the calls made inside the block (SPE {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.with_approval` — function, defined at `src/ctrlrun/control.py:385` +`ctrlrun.with_approval` — function, defined at `src/ctrlrun/control.py:386` ```python from ctrlrun import with_approval diff --git a/docs/reference/policy-yaml.mdx b/docs/reference/policy-yaml.mdx index 4ba5143..104265a 100644 --- a/docs/reference/policy-yaml.mdx +++ b/docs/reference/policy-yaml.mdx @@ -59,6 +59,7 @@ Exactly one of `decision` and `rules`, plus the optional keys. | `effect` | template | v2 | no reservation: the action runs unguarded against duplicates, and the gateway names it on the line that starts it | `"refund:{payment_id}"`; every placeholder must resolve from the arguments or the action is refused before it runs | | `resource` | template | v2 | no resource in the hash, and a grant's `resources:` cannot match | `"payment:{payment_id}"`; the decorator's `resource=` wins where both exist | | `mcp` | mapping with one key, `not_executed_on_error` (bool) | v2 | `false` | gateway only: whether an upstream's in-band error means the tool did nothing (`FAILED`) or nobody knows (`AMBIGUOUS`). Say `true` only for an upstream that rejects before acting | +| `upstream` | mapping with up to three keys: `tls_cert_sha256` (list of `sha256:…` strings, the upstream's leaf certificate in DER form), `tls_cert_file` (path to a PEM file, one or more certificates), `tool_schema_sha256` (one `sha256:…` string, the tool's `tools/list` entry canonicalised) | v8 | no pin: the action is authorised against whatever answers at the upstream's name | gateway only. Checked in three places for one rule: at startup, where a mismatch refuses to start and prints the observed hash beside the pinned one; at decision time, where the action is denied `upstream_mismatch` when what this process last observed differs from the pin and `upstream_unverified` when nothing has been observed yet, so a pin never switches itself off by being unobserved; and at the handshake, where `tls_cert_file`'s certificates are the connection's only trust anchors and a swapped server fails before the first request byte. A digest-only pin gets the first two and not the third, and the startup line says so. The in-process decorator and the ACS hook hold no connection and cannot pin: the hook refuses at construction, the decorator denies `upstream_unverified` | | `controls` | list of ids | v4 | none | cited on every receipt for this action, in registry order | | `data` | map of argument name → label, either a bare string or a mapping with the one key `label` | v4 | no labels; `data_scope` is empty | labels the arguments carry, for the `data_scope` condition below | | `approvals_required` | integer, at least 1 | v6 | one approval, exactly as before v6 | how many **distinct verified principals** must answer before the approval is consumable. A second yes from a principal that already answered is recorded, moves that entry's timestamp, and does not move the count. The count is decided inside the store's own write on all three backends, never by a read followed by a write. Above `1` in a deployment that configures no approver identity is a **denial**, not a silent downgrade: the kernel cannot tell two anonymous yeses apart. `ctrlrun approve` records no verified approver, so it never counts toward one, and says so when it is used. Inside the policy hash | @@ -155,6 +156,8 @@ reader would have silently misread is one it refuses instead. | `ctrlrun.policy/v4` | `version`, `controls` at the top; `controls`, `data` on an entry and a rule | cite ids nothing defines, or evaluate a `data_scope` rule against nothing | | `ctrlrun.policy/v5` | `max_attempts` on an entry | bound renewal after `FAILED` on one effect key | | `ctrlrun.policy/v6` | `approver_role` on a control entry; `approvals_required` on an action entry; `break_glass` in the authority section | gate nobody while reporting a deployment as checking entitlement, consume on the first grant where several were required, or read an envelope as an ordinary grant | +| `ctrlrun.policy/v7` | `tasks`, `budgets` on a grant in the authority section | authorise a task the grant does not cover, or spend with no ceiling | +| `ctrlrun.policy/v8` | `upstream` on an entry | authorise the action against any server at all, which is the whole of what the key restricts | That is why the key sets are closed by version: each key is refused by every reader that would misread it. diff --git a/docs/verify.md b/docs/verify.md index 5906b05..1358a4a 100644 --- a/docs/verify.md +++ b/docs/verify.md @@ -59,11 +59,12 @@ G23 a failing scope provider refuses PASS stripe.refund G24 grant refused off its task PASS head-of-support G25 a hop narrows or it is refused PASS head-of-support G26 a hop is named on both sides PASS head-of-support +G27 a swapped upstream is denied N/A no action entry pins an upstream (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) -24/24 declared guarantees pass. 2 not applicable: G13, G15. +24/24 declared guarantees pass. 3 not applicable: G13, G15, G27. ``` It reads the policy document — `$CTRLRUN_CONFIG`, else `./ctrlrun.yaml` — and the authority diff --git a/generated/readiness.full.mdx b/generated/readiness.full.mdx index ca398eb..148d2bb 100644 --- a/generated/readiness.full.mdx +++ b/generated/readiness.full.mdx @@ -1,7 +1,7 @@ {/* generated from the suite, pyproject and the soak (full) — run the generator */} - **Version 0.9.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later. -- **6,005 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). -- **26 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). +- **6,019 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). - **Each receipt carries the hash of the one before it**, so an alteration is detected and named. [Read more](/docs/production/receipt-integrity). diff --git a/generated/readiness.json b/generated/readiness.json index dd2787d..efbacac 100644 --- a/generated/readiness.json +++ b/generated/readiness.json @@ -1,5 +1,5 @@ { - "guarantees": 26, + "guarantees": 27, "released": "0.9.0", "soak": { "actions": 889735, @@ -9,6 +9,6 @@ "positive_control": true, "unexplained": 0 }, - "tests": 6005, + "tests": 6019, "version": "0.9.0" } diff --git a/generated/readiness.mdx b/generated/readiness.mdx index fce776d..a6e6032 100644 --- a/generated/readiness.mdx +++ b/generated/readiness.mdx @@ -1,7 +1,7 @@ {/* generated from the suite, pyproject and the soak (mdx) — run the generator */} - **Version 0.9.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later. -- **6,005 tests**, every version specified before it was written and every requirement mutation-tested. -- **26 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,019 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). - **Each receipt carries the hash of the one before it**, so an alteration is detected and named. diff --git a/generated/readiness.readme.md b/generated/readiness.readme.md index b499e95..e8d11b7 100644 --- a/generated/readiness.readme.md +++ b/generated/readiness.readme.md @@ -1,7 +1,7 @@ - **Version 0.9.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later. -- **6,005 tests**, every version specified before it was written and every requirement mutation-tested. -- **26 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,019 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). - **Each receipt carries the hash of the one before it**, so an alteration is detected and named.