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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
80 changes: 40 additions & 40 deletions docs/CLAIMS.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/OWASP-AGENTIC-TOP10.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

---

Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion docs/cookbook/verify-in-github-actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion docs/guides/verify-in-ci.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/production/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/Condition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "One `<argument>_<op>: operand` test against an action's arguments

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

`ctrlrun.Condition` — class, defined at `src/ctrlrun/policy.py:371`
`ctrlrun.Condition` — class, defined at `src/ctrlrun/policy.py:398`

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

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

`ctrlrun.Decision` — class, defined at `src/ctrlrun/policy.py:308`
`ctrlrun.Decision` — class, defined at `src/ctrlrun/policy.py:335`

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

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

`ctrlrun.Policy` — class, defined at `src/ctrlrun/policy.py:700`
`ctrlrun.Policy` — class, defined at `src/ctrlrun/policy.py:759`

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

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

`ctrlrun.parse_conditions` — function, defined at `src/ctrlrun/policy.py:1248`
`ctrlrun.parse_conditions` — function, defined at `src/ctrlrun/policy.py:1326`

```python
from ctrlrun import parse_conditions
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/protect.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/with_approval.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/policy-yaml.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down
Loading