diff --git a/docs.mdx b/docs.mdx index 8e702a7..08271f6 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.11.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14. -- **6,221 tests**, every version specified before it was written and every requirement mutation-tested. +- **6,232 tests**, every version specified before it was written and every requirement mutation-tested. - **32 guarantees you can check in your own setup**, with `ctrlrun verify` against your policy, on your store's backend, in a scratch store it creates. - **One host: a file.** SQLite, no server, no ops. **Many hosts: Postgres**, the same guarantees, graded by the same suite. - **Soaked for 20m 0s on postgres**: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. Nothing here establishes what only accumulates over days. [What it does not establish](https://ctrlrun.dev/docs/production/soak). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5b21a36..9636190 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -275,14 +275,30 @@ cycle nobody had recorded, `jwt_identity` ⇄ `revocation`, where `revocation.py handler. One copy was always right and the direction was wrong: the class now lives in `revocation.py`, which `jwt_identity.py` already imports, and `jwt_identity` re-exports it. -**One layering cycle remains, named in the test rather than left to a reader.** `authority.py` -imports `Condition` and `parse_conditions` from `policy.py` deliberately, per the exception -above, and `policy.py` reaches back from `_canonical_authority` and `hash_with_authority`. That -edge cannot be removed by relocation: `_from_section` *constructs* an `Authority` and -`canonical_grants` *consumes* one, so neither moves below `policy.py`, and moving the two callers -up to `control.py` would change what `policy_hash` is taken over, which is evidence in every -receipt rather than an implementation detail. `RECORDED_LAYERING_CYCLES` holds that one pair and -nothing else, so a second one fails the suite. +**And then the last one went too, from the side that was actually loose.** `policy <-> authority` +looked unfixable, and the reason given was true but incomplete: `_from_section` *constructs* an +`Authority` and `canonical_grants` *consumes* one, so neither moves below `policy.py`, and moving +the two callers up to `control.py` would change what `policy_hash` is taken over, which is +evidence in every receipt rather than an implementation detail. All of that holds. It only +describes one side of the cycle. + +The other side was `authority.py` importing eight names from `policy.py`, and not one of them is +`Policy`: the supported schemas, the strict YAML loader, the condition grammar, type-strict +equality, key validation. That is the policy **document grammar**, and it is shared vocabulary +rather than either axis's property. It now lives in `grammar.py`, below both, so `authority.py` +does not import `policy.py` at all. + +The v0.3 exception above is not repealed by this, it is honoured more exactly. `SPEC-v0.3.md` +§4.5 requires the two axes to share **one** condition evaluator, because a second would be a +second place for `True` to start comparing equal to `1`. That one evaluator is now owned by +neither axis instead of by one of them. + +Nothing moved but an address: every block was moved verbatim and `policy.py` re-exports all +thirty-four names, so `SPEC-v0.1.md` §8's frozen `__init__` block and `SPEC-v0.3.md` §8's +`from .policy import Condition, parse_conditions` both stay literally true. + +**`RECORDED_LAYERING_CYCLES` is now empty, and it is meant to stay empty.** A new entry is a +decision rather than a fix, and it carries the reason the edge cannot be relocated. ## 7. What changes after v0.1 (and what doesn't) diff --git a/docs/CLAIMS.md b/docs/CLAIMS.md index a7f25c2..b6cb5df 100644 --- a/docs/CLAIMS.md +++ b/docs/CLAIMS.md @@ -26,7 +26,7 @@ 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:1318` — 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:657` — passes only the action's **name and arguments** to `_ActionPolicy.evaluate` (`policy.py:657`), 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:253`) rather than silently matching nothing. | `test_T6_an_action_name_is_matched_exactly`, `test_a_condition_naming_an_action_field_is_refused_at_load` | +| "Autonomy belongs to the action, not the agent." | `Policy.evaluate(action)` — `policy.py:449` — passes only the action's **name and arguments** to `_ActionPolicy.evaluate` (`policy.py:449`), 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:449`) 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:250`. Exactly as approved: the approval is bound to `action_hash` and consumed with the reservation — `_authorize_and_reserve` — `state.py:1341`. Or not at all: a refusal raises before the executor — `Control.execute` — `control.py:1318`. Says so instead of guessing: only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2223` — and everything else is `AMBIGUOUS`. A receipt: `Receipt` — `receipt.py:256`. **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:1535` — 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` | @@ -53,13 +53,13 @@ by its quoted claim, and `tests/test_docs_audit.py` fails if a named row is not | "A lost reply is `AMBIGUOUS`, never `FAILED`, and a retry against an `AMBIGUOUS` effect is refused." | Only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2223`; `plan_reservation` — `effect.py:250` | `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:643`; `PostgresStateStore.reserve_effect` — `postgres.py:769` | `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:1341` | `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:646` | `test_T6_unknown_action_is_denied_with_reason_unknown_action` | +| "An action the policy does not list is denied" | `Policy.evaluate` — `policy.py:449` | `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:1318` | `test_T74_a_denial_leaves_no_pending_approval_request` | -| "Neither axis reads the agent's instructions" | `Policy.evaluate` — `policy.py:646` — sees the action's name and arguments; `Authority.evaluate` — `authority.py:1296` — 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` | +| "Neither axis reads the agent's instructions" | `Policy.evaluate` — `policy.py:449` — sees the action's name and arguments; `Authority.evaluate` — `authority.py:1300` — 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:81` | `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:1341` — 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:2223`; `NotExecuted` — `errors.py:159` | `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:784`; `prev_hash`, `GENESIS_HASH` for the first — `receipt.py:131` | `test_T172_every_receipt_carries_the_hash_and_the_declared_version`, `test_T164_an_altered_receipt_is_content_altered_at_its_seq` | +| "the hash of the policy that decided it, chained to the receipt before it" | `Policy.policy_hash` — `policy.py:513`; `prev_hash`, `GENESIS_HASH` for the first — `receipt.py:131` | `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 @@ -71,7 +71,7 @@ by its quoted claim, and `tests/test_docs_audit.py` fails if a named row is not | "No agent changes" | `INTERCEPTED_METHOD` is `tools/call` and every other method is relayed unchanged — `gateway/mcp.py:42` | `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:399`; `serve` — `gateway/__init__.py:45` | `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:970`; `McpOptions` — `policy.py:591` | `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:699`; `McpOptions` — `policy.py:394` | `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:95` | `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:131`, 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:161` | `test_the_startup_block_names_the_environment_identity_and_authority`, `test_the_startup_block_says_so_when_there_is_no_authority_section` | @@ -85,22 +85,22 @@ 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:390`) with the operators `eq`, `neq`, `in`, `lt`, `lte`, `gt`, `gte` — `_OPERATORS` — `policy.py:131` | `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:646` | `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` — `decision.py:41` — is exactly `allow`, `approve`, `deny`; rules match first-wins over `Condition` (`grammar.py:190`) with the operators `eq`, `neq`, `in`, `lt`, `lte`, `gt`, `gte` — `_OPERATORS` — `grammar.py:113` | `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:449` | `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:81` | `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:646` — 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:1288`; `Control._authority_result` — `control.py:1128` | `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:449` — 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:1292`; `Control._authority_result` — `control.py:1128` | `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:86`), reached for reads and for actions with no effect key alike | `test_T67_an_action_the_policy_allows_outright_still_needs_a_grant` | +| "every principal needs a grant and no grant means denied" | `NO_AUTHORITY` — the fail-closed default of `Authority.evaluate` (`authority.py:90`), 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:4165`; `Authority.plan_delegation` — `authority.py:1549`; `ctrlrun delegate` — `cli/main.py:1551` | `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:986` — runs from `plan_delegation` (`authority.py:1549`) **and** from the chain walk in `Authority.evaluate` (`authority.py:1296`) | `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:575` — is evaluated inside `reserve_effect`'s own transaction on all three backends, and `_charges_for` — `authority.py:1404` — 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:986`; the subject half is `_subject_contained` (`authority.py:1065`) | `test_t81_omission_is_not_unlimited`, `test_T73b_a_subject_addressed_to_every_principal_is_refused`, `test_t76_each_dimension_violated_alone` | +| "narrow it at runtime with `ctrlrun delegate`" | `Control.delegate` — `control.py:4165`; `Authority.plan_delegation` — `authority.py:1553`; `ctrlrun delegate` — `cli/main.py:1551` | `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:990` — runs from `plan_delegation` (`authority.py:1553`) **and** from the chain walk in `Authority.evaluate` (`authority.py:1300`) | `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:575` — is evaluated inside `reserve_effect`'s own transaction on all three backends, and `_charges_for` — `authority.py:1408` — 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:990`; the subject half is `_subject_contained` (`authority.py:1069`) | `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:4505` — writes one row — `revoke_delegation` — `state.py:841` 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:769`; `Control._observed` — `control.py:1708`; `_WouldHave` — `receipt.py:350`; `ReceiptResult.OBSERVED` — `receipt.py:262` | `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:769` | `test_T84_mode_is_refused_anywhere_but_the_top_level` | +| "`mode: observe` … records what *would* have been blocked, without blocking anything" | `_parse_mode` — `policy.py:498`; `Control._observed` — `control.py:1708`; `_WouldHave` — `receipt.py:350`; `ReceiptResult.OBSERVED` — `receipt.py:262` | `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:498` | `test_T84_mode_is_refused_anywhere_but_the_top_level` | | "`ctrlrun stats` gives you the numbers" | `stats` — `cli/main.py:1355`; 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:1708` | `test_T82_observe_executes_what_enforce_would_deny`, `test_T83_an_executor_that_fails_on_a_held_key_still_writes_the_record` | @@ -140,8 +140,8 @@ catalogue, `GUARANTEES` (`verify/guarantees.py:55`). | "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:671`; 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:643`, decided inside the `BEGIN IMMEDIATE` of `_authorize_and_reserve` (`state.py:1341`) against `effect_key TEXT PRIMARY KEY` (`migrations.py:109`; `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:2223`. Every other exception, timeouts included, yields `AMBIGUOUS`. A retry against an `AMBIGUOUS` key is refused — `effect.py:174`, 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:646` — answers `deny` for a name the document does not list. Missing or malformed policy: `Policy.from_file` — `policy.py:814` — raises `PolicyError`, and there is no `Control` without a policy. Missing principal: `_refuse_no_principal` — `control.py:4851` | `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:86`); `contained_dimension` — `authority.py:986` — runs from `plan_delegation` (`authority.py:1549`) and from the chain walk in `Authority.evaluate` | `test_T67_a_principal_with_no_grant_is_denied`, `test_t76_each_dimension_violated_alone` | +| "An unknown action, a missing policy or a missing principal is denied." | Unknown action: `Policy.evaluate` — `policy.py:449` — answers `deny` for a name the document does not list. Missing or malformed policy: `Policy.from_file` — `policy.py:532` — raises `PolicyError`, and there is no `Control` without a policy. Missing principal: `_refuse_no_principal` — `control.py:4851` | `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:90`); `contained_dimension` — `authority.py:990` — runs from `plan_delegation` (`authority.py:1553`) 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:246`; `Event` — `receipt.py:312`; the store is authoritative — `append_event` — `state.py:852`; the JSONL export — `JSONLEventSink` — `receipt.py:938` | `test_T11_every_demo_receipt_carries_every_field_in_the_spec`, `test_T11_every_demo_receipt_parses_back_into_a_Receipt` | ## What it guarantees @@ -158,15 +158,15 @@ catalogue, `GUARANTEES` (`verify/guarantees.py:55`). | "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:174`; who resolved it — `resolved_by` — `effect.py:210`; `resolve` — `cli/main.py:1024` | `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:2877`; `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:646`; `Policy.from_file` — `policy.py:803`; `_refuse_no_principal` — `control.py:4851`; `_authorize_and_reserve` — `state.py:1341` | `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:135` | `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:1288`; `contained_dimension` — `authority.py:986`; `Control.revoke` — `control.py:4505` | `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:449`; `Policy.from_file` — `policy.py:532`; `_refuse_no_principal` — `control.py:4851`; `_authorize_and_reserve` — `state.py:1341` | `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:93` | `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:1292`; `contained_dimension` — `authority.py:990`; `Control.revoke` — `control.py:4505` | `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:215`; 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:784`, over `_canonical_policy` — `policy.py:1032`; carried into the receipt by `_record` — `control.py:4742` | `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:774` | `test_T171_the_declared_version_alone_does_not_change_the_hash`, `test_T171_comments_key_order_and_whitespace_do_not_change_the_hash` | +| "every receipt records which policy decided it" | `Policy.policy_hash` — `policy.py:513`, over `_canonical_policy` — `policy.py:761`; carried into the receipt by `_record` — `control.py:4742` | `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:503` | `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:1318`; `_spend_unneeded_approval` — `control.py:2817` | `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:539`; `prev_hash` — `receipt.py:458`; `GENESIS_HASH` — `receipt.py:131`; `put_receipt` takes the head row's lock first — `postgres.py:2121` | `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:1056`; the six names — `CHAIN_BREAKS` — `receipt.py:993` | `test_the_verify_chain_flag_reports_a_break_by_seq_and_by_name`, `test_verify_chain_reads_a_postgres_store_through_store_url` | @@ -177,8 +177,8 @@ catalogue, `GUARANTEES` (`verify/guarantees.py:55`). | "`WebhookApprovalProvider` sends an approval request to a webhook, such as Slack, and takes the answer back through the same grant calls" | `WebhookApprovalProvider` — `webhook.py:143` — 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:47` | `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:47` | `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:351`; `Receipt` — `receipt.py:404` — 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:618`; `Policy.data_scope` — `policy.py:638`; `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:271`; `Receipt` — `receipt.py:404` — 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:421`; `Policy.data_scope` — `policy.py:441`; `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:646`) 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:449`) 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. **`--coverage` opens a store and still computes none**: `ctrlrun/coverage.py` reports a list with a reason per entry, carries no `score`, `percentage` or `ratio` field, and does not move the exit code (SPEC-v0.11 §7, rule 4) | `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`, `test_T560_the_report_is_a_list_and_never_a_score`, `test_T563_the_coverage_flag_does_not_move_the_exit_code` | | "`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:723` — called from both stores' constructors; `_refuse` — `migrations.py:649` — 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:456` — 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:784` — 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:513` — 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 diff --git a/docs/production/index.mdx b/docs/production/index.mdx index c4355fc..ad555ed 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.11.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14. -- **6,221 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). +- **6,232 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). - **32 guarantees you can check in your own setup**, with `ctrlrun verify` against your policy, on your store's backend, in a scratch store it creates. [Read more](/docs/security/verify-guarantees). - **One host: a file.** SQLite, no server, no ops. **Many hosts: Postgres**, the same guarantees, graded by the same suite. [Read more](/docs/production/postgres). - **Soaked for 20m 0s on postgres**: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. Nothing here establishes what only accumulates over days. [Read more](/docs/production/soak). diff --git a/docs/reference/api/Authority.mdx b/docs/reference/api/Authority.mdx index 5b56b53..f9f80f9 100644 --- a/docs/reference/api/Authority.mdx +++ b/docs/reference/api/Authority.mdx @@ -5,7 +5,7 @@ description: "The `authority:` section, loaded and evaluable (SPEC-v0.3 §4)." {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.Authority` — class, defined at `src/ctrlrun/authority.py:1231` +`ctrlrun.Authority` — class, defined at `src/ctrlrun/authority.py:1235` ```python from ctrlrun import Authority diff --git a/docs/reference/api/AuthorityResult.mdx b/docs/reference/api/AuthorityResult.mdx index 8b01c52..61e98a1 100644 --- a/docs/reference/api/AuthorityResult.mdx +++ b/docs/reference/api/AuthorityResult.mdx @@ -5,7 +5,7 @@ description: "What the authority axis decided, and which grant it decided on (§ {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.AuthorityResult` — class, defined at `src/ctrlrun/authority.py:623` +`ctrlrun.AuthorityResult` — class, defined at `src/ctrlrun/authority.py:627` ```python from ctrlrun import AuthorityResult diff --git a/docs/reference/api/Condition.mdx b/docs/reference/api/Condition.mdx index cf1dcf3..06d21dc 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:389` +`ctrlrun.Condition` — class, defined at `src/ctrlrun/grammar.py:189` ```python from ctrlrun import Condition diff --git a/docs/reference/api/Delegation.mdx b/docs/reference/api/Delegation.mdx index 343a59e..5c83105 100644 --- a/docs/reference/api/Delegation.mdx +++ b/docs/reference/api/Delegation.mdx @@ -5,7 +5,7 @@ description: "A grant created at runtime by a principal who already holds one (S {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.Delegation` — class, defined at `src/ctrlrun/authority.py:583` +`ctrlrun.Delegation` — class, defined at `src/ctrlrun/authority.py:587` ```python from ctrlrun import Delegation diff --git a/docs/reference/api/Grant.mdx b/docs/reference/api/Grant.mdx index bf292fc..fb1cd70 100644 --- a/docs/reference/api/Grant.mdx +++ b/docs/reference/api/Grant.mdx @@ -5,7 +5,7 @@ description: "One permission: this subject may propose these actions, under thes {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.Grant` — class, defined at `src/ctrlrun/authority.py:433` +`ctrlrun.Grant` — class, defined at `src/ctrlrun/authority.py:437` ```python from ctrlrun import Grant diff --git a/docs/reference/api/Policy.mdx b/docs/reference/api/Policy.mdx index 2d1d2f6..0abb2f1 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:750` +`ctrlrun.Policy` — class, defined at `src/ctrlrun/policy.py:479` ```python from ctrlrun import Policy diff --git a/docs/reference/api/Subject.mdx b/docs/reference/api/Subject.mdx index 1f4bd67..afbc8f7 100644 --- a/docs/reference/api/Subject.mdx +++ b/docs/reference/api/Subject.mdx @@ -5,7 +5,7 @@ description: "Who a grant is addressed to: an agent pattern, a user pattern, or {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.Subject` — class, defined at `src/ctrlrun/authority.py:337` +`ctrlrun.Subject` — class, defined at `src/ctrlrun/authority.py:341` ```python from ctrlrun import Subject diff --git a/docs/reference/api/authority-Budget.mdx b/docs/reference/api/authority-Budget.mdx index 6938919..25fd82a 100644 --- a/docs/reference/api/authority-Budget.mdx +++ b/docs/reference/api/authority-Budget.mdx @@ -5,7 +5,7 @@ description: "How much, over what, in how long (SPEC-v0.9 §2.2)." {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} -`ctrlrun.authority.Budget` — class, defined at `src/ctrlrun/authority.py:383` +`ctrlrun.authority.Budget` — class, defined at `src/ctrlrun/authority.py:387` ```python from ctrlrun.authority import Budget diff --git a/docs/reference/api/parse_conditions.mdx b/docs/reference/api/parse_conditions.mdx index 31316cb..4a0691d 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:1317` +`ctrlrun.parse_conditions` — function, defined at `src/ctrlrun/grammar.py:433` ```python from ctrlrun import parse_conditions diff --git a/generated/readiness.full.mdx b/generated/readiness.full.mdx index b14d7bf..7ec9139 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.11.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14. -- **6,221 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). +- **6,232 tests**, every version specified before it was written and every requirement mutation-tested. [Read more](/docs/how-this-is-built). - **32 guarantees you can check in your own setup**, with `ctrlrun verify` against your policy, on your store's backend, in a scratch store it creates. [Read more](/docs/security/verify-guarantees). - **One host: a file.** SQLite, no server, no ops. **Many hosts: Postgres**, the same guarantees, graded by the same suite. [Read more](/docs/production/postgres). - **Soaked for 20m 0s on postgres**: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. Nothing here establishes what only accumulates over days. [Read more](/docs/production/soak). diff --git a/generated/readiness.json b/generated/readiness.json index 012afe1..4eb42e5 100644 --- a/generated/readiness.json +++ b/generated/readiness.json @@ -18,6 +18,6 @@ "positive_control": true, "unexplained": 0 }, - "tests": 6221, + "tests": 6232, "version": "0.11.0" } diff --git a/generated/readiness.mdx b/generated/readiness.mdx index 322708b..c84c27b 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.11.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14. -- **6,221 tests**, every version specified before it was written and every requirement mutation-tested. +- **6,232 tests**, every version specified before it was written and every requirement mutation-tested. - **32 guarantees you can check in your own setup**, with `ctrlrun verify` against your policy, on your store's backend, in a scratch store it creates. - **One host: a file.** SQLite, no server, no ops. **Many hosts: Postgres**, the same guarantees, graded by the same suite. - **Soaked for 20m 0s on postgres**: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. Nothing here establishes what only accumulates over days. [What it does not establish](https://ctrlrun.dev/docs/production/soak). diff --git a/generated/readiness.readme.md b/generated/readiness.readme.md index ba89b4b..ddde915 100644 --- a/generated/readiness.readme.md +++ b/generated/readiness.readme.md @@ -1,6 +1,6 @@ - **Version 0.11.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14. -- **6,221 tests**, every version specified before it was written and every requirement mutation-tested. +- **6,232 tests**, every version specified before it was written and every requirement mutation-tested. - **32 guarantees you can check in your own setup**, with `ctrlrun verify` against your policy, on your store's backend, in a scratch store it creates. - **One host: a file.** SQLite, no server, no ops. **Many hosts: Postgres**, the same guarantees, graded by the same suite. - **Soaked for 20m 0s on postgres**: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. Nothing here establishes what only accumulates over days. [What it does not establish](https://ctrlrun.dev/docs/production/soak).