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 capabilities.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,15 @@ capabilities:

- id: authority-containment
name: Authority and delegation
description: With authority on, every principal needs a grant, and delegation cannot widen one.
description: Every principal needs a grant, delegation cannot widen one, and a grant bounds the total.
guarantee: true
ways_in:
decorator: true
gateway: true
adapter: true
since: v0.3
page: docs/concepts/authority-and-delegation
claim: provably a subset of its parent on every dimension, at creation and again at every evaluation
claim: 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

- id: receipts
name: Receipts
Expand Down
4 changes: 4 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@
"docs/reference/api/WebhookApprovalProvider",
"docs/reference/api/acs-AcsControlHook",
"docs/reference/api/action_hash",
"docs/reference/api/authority-Budget",
"docs/reference/api/banner",
"docs/reference/api/canonical_bytes",
"docs/reference/api/canonicalize",
Expand All @@ -310,6 +311,9 @@
"docs/reference/api/parse_conditions",
"docs/reference/api/postgres-PostgresStateStore",
"docs/reference/api/protect",
"docs/reference/api/state-Charge",
"docs/reference/api/state-Consumption",
"docs/reference/api/state-check_charges",
"docs/reference/api/transport-HTTPConnection",
"docs/reference/api/transport-HTTPSConnection",
"docs/reference/api/transport-Transport",
Expand Down
15 changes: 4 additions & 11 deletions docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ the same decisions without an install, or read the full transcript in the
An unknown action, a missing policy or a missing principal is denied. Since v0.1.
</Card>
<Card title="Authority and delegation" href="/docs/concepts/authority-and-delegation">
With authority on, every principal needs a grant, and delegation cannot widen one. Since v0.3.
Every principal needs a grant, delegation cannot widen one, and a grant bounds the total. Since v0.3.
</Card>
<Card title="Receipts" href="/docs/concepts/receipts-and-evidence">
Every executed action leaves a portable JSON receipt of who, what and outcome. Since v0.1.
Expand Down Expand Up @@ -218,9 +218,9 @@ the framework's own interrupt, and a framework with no such primitive does not n
## Where it stands

{/* generated from the suite, pyproject and the soak (mdx) — run the generator */}
- **Version 0.8.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later.
- **5,653 tests**, every version specified before it was written and every requirement mutation-tested.
- **21 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.
- **Version 0.9.0**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later.
- **6,019 tests**, every version specified before it was written and every requirement mutation-tested.
- **24 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 All @@ -233,13 +233,6 @@ the framework's own interrupt, and a framework with no such primitive does not n
- No sector packs. (the policy templates are starting points, not a product)
{/* end generated */}

**Not yet:**

- No external security audit. (optional, and no release waits for one)
- No third-party review of the kernel. (every review so far was run inside this project)
- No sector packs. (the policy templates are starting points, not a product)
{/* end generated */}

## Start here

<Columns cols={3}>
Expand Down
131 changes: 66 additions & 65 deletions docs/CLAIMS.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion docs/OWASP-AGENTIC-TOP10.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ mechanism, not the entry.
| **G19** one principal counts once | An action entry may require several approvals. The threshold counts **distinct verified principals**: 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. | `ASI09:2026` (partly), `ASI03:2026` (partly) | M-of-N is worth nothing if one person can be N of it. What this closes is a miscount: two processes answering at the same instant produce two approvers or one, never a threshold reached twice, and the test that proves it opens the window between the count's read and its write rather than starting two processes and hoping. A threshold above one in a deployment that verifies nobody is a denial, not a silent downgrade to one approval. It does not make several humans independent of each other, and it does not know whether they discussed it. |
| **G20** revoked before its exp: no | Where a deployment configures a revocation feed, Security Event Tokens are consumed and a credential the issuer revoked is refused **at resolution**, before its `exp`, as an `IdentityError`. The match is against the token's own `iss`, `sub` and `jti` and never against the principal's agent name. | `ASI03:2026` (partly), `ASI10:2026` (partly) | Until v0.8 a verified token was valid until it expired, so a compromised credential stayed good for the rest of its lifetime and short lifetimes were the whole of the answer. Two things this closes less than it sounds, and both are stated wherever the feature is described. A revoked credential leaves a **log line and no receipt**: resolution happens before an action exists, where an expired credential leaves a full receipt. And a feed is worth what its source is worth: whoever can write it can refuse the operator's own agents, which is a denial of service against them and is fail-closed. What they cannot do is admit a principal the issuer revoked, because the feed is only ever consulted to refuse. |
| **G21** unapproved policy decides no | Where a deployment requires it, a decision is made only where a committed effect records that this exact policy hash was approved, and the policy in force must send its own change to a human. Otherwise every action is denied `policy_unapproved`, with an `ACTION_DENIED` event and a `DENIED` receipt. A policy change is an ordinary action, so the approval path applies to it in full. | `ASI03:2026` (partly), `ASI01:2026` (partly), `ASI10:2026` (partly) | The policy is the one file that decides every other decision, and until v0.8 it was changed by editing it. The property is exactly **a policy change that no verified principal other than the proposer approved decides nothing** — not "a policy cannot be changed by whoever holds the file". An administrator with write access can still widen *who* may approve the next change; what they cannot manufacture is the approving principal, whose credential is verified by the provider configured in code. An approval binds a hash and not an ordering, so any hash ever approved stays approved and a superseded policy can be restored with nothing in the evidence saying so. |
| **G22** held budget refuses next reserve | A grant may carry a consequence budget: a metric, a limit and a rolling window. The sum is consumed **when the effect is reserved, in the same transaction**, and an action that would take it past the limit is refused `budget_exhausted` with an `ACTION_DENIED` event and a `DENIED` receipt. Every grant in the delegation chain is charged, so a child cannot spend its parent's budget over again. | `ASI08:2026` (partly), `ASI01:2026` (partly), `ASI10:2026` (partly) | Until v0.9 the authority model bounded **one action** and never an aggregate: `amount_lte: 5000` says nothing about the thousand actions that each pass it, so an agent acting entirely within its permissions could empty an account one permitted refund at a time. Three limits stated wherever the feature is described. A budget **cannot recall an action already in flight**: the window rolls forward and a reservation taken a second before it rolls is committed regardless. An `AMBIGUOUS` effect **holds** its charge until a human resolves it, which is deliberate and which means an agent that can manufacture ambiguity can exhaust a budget it cannot spend, a denial of service against its own principal that is fail-closed. And a budget counts a **metric an operator named** — an action argument — not a consequence: nothing here ranks, scores or classifies what an action means. |
| **G23** a failing scope provider refuses | Where a deployment configures one, a scope provider answers *is this record this principal's?* and the kernel matches the answer against the action's resource, strictly **before** the reservation. Two distinct refusals, never conflated: `scope_unavailable` when the provider raises, answers the wrong shape, or answers something the canonicalizer refuses; `out_of_scope` when it answered and the record is not covered. Only the **hash** of the answer reaches the receipt. | `ASI06:2026` (partly), `ASI01:2026` (partly), `ASI02:2026` (partly) | This is the bite on an identifier an attacker chose, which the `ASI06` row below said nothing had: a grant permits `records.read` on `customer:*`, and until v0.9 nothing had an opinion about *whose* record `customer:90210` is. Two limits. **A scope provider is worth what its source is worth**: it is the operator's own code answering from the operator's own system of record, and a poisoned source answers wrongly with the kernel none the wiser. And whether a deployment configures one is a fact about its code, which `ctrlrun verify` cannot read: verify grades the mechanism against a provider it supplies. The gateway and the ACS hook cannot name a provider at all. |
| **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. |

---

Expand All @@ -102,7 +105,7 @@ stated here rather than left implied:

| Entry | Title | Covered | Not covered |
|---|---|---|---|
| `ASI06:2026` | Memory & Context Poisoning | G6 and G1 constrain what an agent acting on a poisoned context can *do*: the action must still be named in the policy, so a belief an attacker planted cannot reach a tool the agent was never entitled to use, and an approval granted for one action cannot be spent on another. This is the same downstream constraint that makes `ASI01` partial, and it is here for the same reason. | CTRLRun never reads a model's memory, its context or its prompt, so it neither detects nor prevents the poisoning. And the shape poisoning most often takes is the one the kernel has least to say about: **corrupted arguments to an action the agent is entitled to take** — the right operation against the wrong record. Policy conditions, resource patterns and v0.6 data scope bite on part of that; nothing bites on an identifier an attacker chose. |
| `ASI06:2026` | Memory & Context Poisoning | G6 and G1 constrain what an agent acting on a poisoned context can *do*: the action must still be named in the policy, so a belief an attacker planted cannot reach a tool the agent was never entitled to use, and an approval granted for one action cannot be spent on another. This is the same downstream constraint that makes `ASI01` partial, and it is here for the same reason. | CTRLRun never reads a model's memory, its context or its prompt, so it neither detects nor prevents the poisoning. And the shape poisoning most often takes is the one the kernel has least to say about: **corrupted arguments to an action the agent is entitled to take** — the right operation against the wrong record. Policy conditions, resource patterns and v0.6 data scope bite on part of that, and since v0.9 **G23 bites on the identifier itself**: where a deployment configures a scope provider, the kernel asks its system of record whether `customer:90210` is this principal's before reserving anything. That closes the sharpest version of this and not the category. The provider is the operator's own code reading the operator's own data, so a poisoned source answers wrongly with the kernel none the wiser, and a deployment that configures no provider is exactly where it was. |
| `ASI01:2026` | Agent Goal Hijack | G1 and G6 constrain what a hijacked agent can *do*: it still meets the policy, and it still cannot present an approval granted for a different action. | CTRLRun does not detect or prevent the hijack. It never sees the prompt, the plan or the reasoning, so an agent whose goal was replaced proposes actions exactly as a healthy one would — and every action inside its policy and its grants will run. |
| `ASI09:2026` | Human-Agent Trust Exploitation | G1 and G2 close the shape where an approval a human gave for one action is spent on another, or spent twice. | CTRLRun does not authenticate the *approver*, does not model separation of duties, and has no opinion on whether the human was misled into approving. A human persuaded to approve the right action for the wrong reason gets a valid approval, and the receipt records it as one. |

Expand Down
Loading