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
89 changes: 79 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,91 @@ reference implements it. See [`spec.md` §2.1](spec.md) for the version matrix.

## [0.3-draft] — Unreleased

0.3 has two tracks: a set of **additive hardening clauses** that tighten
obligations **without changing any hashed or signed bytes** (so they MAY be
adopted on the 0.2 preimage), and **one deferred breaking item** (folding the URL
query into the `action_fingerprint` preimage) held for a later draft.

### Repositioned
- Abstract recast around **intent-bound action authorization**: delego is a
**deterministic pre-action authorization layer for AI agents** (a Policy
Decision Point) that answers `allow` / `needs_approval` / `deny` per action as a
pure function of (action, policy, audit ledger, time), bound to the action
fingerprint. Names the gap as **OWASP ASI03 (Excessive Agency)**.
- §1 / README gain a one-line definition.
- §2 names the **PDP/PEP split** explicitly (Authorizer = PDP, Broker = PEP; only
the Broker touches a credential) and maps `allow` / `needs_approval` / `deny`
onto the `ALLOW` / `REQUIRE_APPROVAL` / `DENY` triad of agent authorization
fabrics.
- §11 adds: **"'Firewall' is an analogy, not the model"** — delego is an
action-authorization layer (a PDP), not a network firewall. "Firewall" is
retained only as a loose analogy.

### Added (0.3, draft — additive hardening on the 0.2 preimage)
- §4.2 — a **NORMATIVE Broker query obligation**: a Broker MUST NOT transmit any
query parameter not derivable from the authorized action, MUST reconstruct the
outgoing URL from the authorized `host`/`path`/`params` (not the agent-supplied
raw query), and MUST refuse an action whose URL it cannot reconstruct. Changes
no hashed bytes. Conformance line added to §10.
- §5.1 — **policy validation (NORMATIVE)**: an Authorizer MUST validate its policy
against `schema/policy.json` and **fail closed** on an invalid policy, including
rejecting unknown `match`/`constraints` keys (`additionalProperties: false`); it
MUST NOT silently skip an unknown constraint. Conformance line added to §10.
- §7.1 — **Authorization properties (NORMATIVE)**, four testable invariants:
(P1) exact-action binding / substitution-proof — fingerprint mismatch ⇒ `deny`
regardless of status, checked **before** status; (P2) exact-intent binding;
(P3) single-use — `approved → consumed` committed before the credential is
released, atomic, and terminal (`consumed`/`denied` never reopened);
(P4) no cross-approval reuse. Conformance line added to §10.
- §8.3 — **head-anchoring as the REQUIRED truncation/rollback defense**: defines
the external head anchor `(seq, entry_hash)`; an Authorizer advertising
rollback-resistance MUST maintain it; an Auditor with an anchor MUST reject a
mismatched head; an Auditor with **no** anchor MUST report that truncation
cannot be ruled out rather than an unqualified "valid". The §8.1 caveat now
points to §8.3.
- §5 / §11 — the `rate_limit` **consistency class** is stated: **exact** only
under a serialized single-writer ledger, **best-effort** under concurrent
writers; an implementation MUST document which it provides.
- §9 — the **authorization token** is recast as an **OPTIONAL PROFILE** (a
portable PDP→PEP decision artifact), not the protocol's load-bearing control.
Adds a `cns` (consumption nonce) claim binding the token to one credential
release (single-use inheritance); tightens `aud` (exact match), issuance
(TTL ≤ 60 s, unique `jti`, only for `allow`/released approval), and replay
(Broker retains `jti` until `exp`, refuses a consumed `cns`). New **§9.2
Revocation** (expiry primary; optional revocation list) and **§9.3 Interop**
(PDP→PEP artifact; composes with an agent-passport / identity layer via the
optional `sub` claim; maps onto `ALLOW` / `REQUIRE_APPROVAL` / `DENY`).
- `schema/authorization-token.json` — adds `cns` (string, **required**) and an
optional `sub` (string); the example token and its markdown are updated to match.

### Added (0.3, draft — CTK)
- `ctk/vectors/resolve.json` — two new **wired** cases for §7.1: **P1**
(fingerprint mismatch refused even when `status = approved`) and **P3** (a
`denied` approval is not resurrected). The current reference already enforces
both, so they are replayed by `conformance.py`.
- `ctk/vectors/policy_invalid.json` and `ctk/vectors/broker_query.json` — new
**pending** vectors documenting the §5.1 and §4.2 behaviour the reference does
not yet expose; **not wired** into `conformance.py`. They activate (get wired)
at reference ≥ 0.2.3.

### Deferred (0.3, draft — breaking, NOT in 0.3)
- §4.2 — folding the URL **query string** into the `action_fingerprint` preimage.
This **changes the preimage** and is therefore a **breaking** change (§8.2); it
is intentionally **DEFERRED** to a future draft and recorded as a
forward-looking note only. It would bump the protocol version and ship with
regenerated `hashing` vectors when the reference implements it. (Earlier
0.3-draft notes listed this as an Added item; it is now correctly tracked as
deferred, and the interim defense is the additive §4.2 Broker obligation above.)

### Fixed
- §8.1 corrected: hash-chaining does **not** detect truncation of the most recent
receipts (a truncated prefix verifies clean), nor a holder of the local signing
key. Added a normative caveat requiring external head anchoring for rollback
detection — the prior "deleting any receipt breaks a check" claim was wrong.
key. The normative requirement to anchor the head externally now lives in §8.3;
the prior "deleting any receipt breaks a check" claim was wrong.
- Version notation normalised to the `0.x` scheme everywhere (spec/protocol is
`0.x`; the reference *package* is `0.x.y`). `conformance.py` now parses a
`0.x`; the reference *package* is `0.x.y`). `conformance.py` parses a
two-component spec version.

### Added (0.3, draft — not yet in reference)
- §4.2 — the URL **query string** is folded into the `action_fingerprint`
preimage, closing the confused-deputy gap where two requests differing only in
their query share a fingerprint. A breaking change to the preimage; ships with
updated `hashing` vectors when the reference implements it.
- §9 retagged as the 0.3 frontier (signed authorization token; unchanged content).

### Added (0.2, now reference-backed)
- §2.1 — a **Protocol versions** matrix (0.1 / 0.2 / 0.3) and the rule that the
reference's `__protocol_version__` MUST be ≤ this document's version.
Expand Down
36 changes: 24 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Spec](https://img.shields.io/badge/spec-v0.3--draft-orange.svg)](spec.md)

The open specification for **delego** — a deterministic **authorization & audit
protocol for AI-agent actions**. It defines how an action proposed by an agent
is authorized (with no LLM in the decision path) *before* any credential is used,
bound to the originating human instruction, and recorded in a tamper-evident,
signed audit chain.
delego is a **deterministic pre-action authorization layer for AI agents**: it
sits between an agent that proposes actions and the credential broker that
executes them, and answers, for each action, `allow` / `needs_approval` / `deny`
— with no language model in the decision path, before any credential is used,
bound to the originating human instruction and the exact action, and recorded in
a tamper-evident, signed audit chain. It addresses the agent-authorization gap
catalogued as **OWASP ASI03 (Excessive Agency)**.

This repository is the source of truth for the protocol. The reference
implementation is **[delego](https://github.com/Delego-Dev/delego)**.
Expand All @@ -28,10 +30,14 @@ implementation is **[delego](https://github.com/Delego-Dev/delego)**.
Credential brokers ensure an agent never holds a raw secret — but they can't
tell whether *this specific action* is the thing the human authorized. So a
prompt injection can redirect an in-scope, validly-credentialed action: the
**confused deputy**. delego authorizes the *action* against a deterministic
policy before any credential is used, binds it to the human instruction,
requires human approval for sensitive actions, and writes a signed, hash-chained
audit trail.
**confused deputy**. delego is the **Policy Decision Point (PDP)** that authorizes
the *action* against a deterministic policy before any credential is used, binds
it to the human instruction, requires human approval for sensitive actions, and
writes a signed, hash-chained audit trail; the credential broker is the **Policy
Enforcement Point (PEP)** and the only component that ever touches a credential.
("Firewall" is sometimes used as a loose analogy for this — but delego is an
action-authorization layer, not a network firewall; see
[§11](spec.md#11-security-considerations).)

This specification exists so that independent **authorizers**, **brokers**, and
**auditors** — written by different people, in different languages — agree
Expand All @@ -45,8 +51,9 @@ byte-for-byte.
- [Proposed Action & hashing](spec.md#4-proposed-action)
- [Policy & decisions](spec.md#5-policy)
- [Approval binding — the confused-deputy guard](spec.md#7-approval-binding-the-confused-deputy-guard)
- [Authorization properties (P1–P4)](spec.md#71-authorization-properties-normative-03-draft--additive)
- [Receipt & audit chain](spec.md#8-receipt--audit-chain-normative)
- [Authorization Token](spec.md#9-authorization-token-normative)
- [Authorization Token](spec.md#9-authorization-token-optional-profile)

## Ecosystem

Expand All @@ -70,8 +77,13 @@ reproduce them; see [§10 Conformance](spec.md#10-conformance).
## Status & versioning

**v0.3 — draft.** The spec/protocol is versioned `0.x` (the reference *package*
is `0.x.y`). 0.1–0.2 are reference-backed; 0.3 (query-string fingerprint §4.2,
authorization token §9) is specified but not yet in the reference. See the
is `0.x.y`). 0.1–0.2 are reference-backed. 0.3 adds **additive hardening clauses**
— the §4.2 Broker query obligation, policy-schema validation (§5.1), the
authorization properties P1–P4 (§7.1), head-anchoring (§8.3), and the
authorization-token profile (§9) — which tighten obligations **without changing
any hashed or signed bytes** and so MAY be adopted on the 0.2 preimage. One item
is **deferred**: folding the URL query into the `action_fingerprint` preimage
(§4.2) is a breaking change held for a later draft. See the
[§2.1 version matrix](spec.md#21-protocol-versions). A breaking change to the
receipt fields bumps the version (see [§8.2](spec.md#82-schema-versioning)).

Expand Down
20 changes: 18 additions & 2 deletions ctk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,27 @@ conformant implementation MUST reproduce it.
| [`vectors/decisions.json`](vectors/decisions.json) | Load the example policy, evaluate each action (spec §5–§6), and match `outcome` / `rule` / `reasons`. |
| [`vectors/chain.jsonl`](vectors/chain.jsonl) + [`vectors/chain.expected.json`](vectors/chain.expected.json) | Verify the chain (spec §8.1) using [`vectors/signing_key.pub`](vectors/signing_key.pub); it MUST be valid with the listed `seqs`. |
| [`vectors/chain.tampered.jsonl`](vectors/chain.tampered.jsonl) + [`vectors/chain.tampered.expected.json`](vectors/chain.tampered.expected.json) | The same chain with `seq 0` edited and not re-signed; verification MUST fail with a content-hash mismatch at `seq 0`. |
| [`vectors/resolve.json`](vectors/resolve.json) *(0.2)* | For each case, given a parked `approval` (its `action_fingerprint`, `intent_hash`, `status`, `rule`) and a `presented_action`, apply the §7 resolution rules (fingerprint guard → intent guard → status) and match `expected.outcome`; the emitted reason MUST contain `expected.reason_contains`. Exercises the confused-deputy guard, the intent guard, and single-use replay refusal. |
| [`vectors/resolve.json`](vectors/resolve.json) *(0.2; + 0.3 §7.1 P1/P3)* | For each case, given a parked `approval` (its `action_fingerprint`, `intent_hash`, `status`, `rule`) and a `presented_action`, apply the §7 resolution rules (fingerprint guard → intent guard → status) and match `expected.outcome`; the emitted reason MUST contain `expected.reason_contains`. Exercises the confused-deputy guard, the intent guard, and single-use replay refusal — and now the §7.1 authorization properties **P1** (fingerprint mismatch refused even when `status = approved`) and **P3** (a `denied` approval is not resurrected). The P1/P3 cases carry a `"property"` tag; they are already enforced by the reference and are wired into `conformance.py`. |

The policy used for the decision and chain vectors is
[`../examples/policy.example.yaml`](../examples/policy.example.yaml). The public
key for verifying the chain signatures is `vectors/signing_key.pub` (the private
key is never published).

## Pending vectors (documented, not yet wired)

These vectors describe **0.3 additive behaviour that the current reference
(protocol 0.2) does not yet expose**, so `conformance.py` does **not** replay them
— wiring them now would fail against the unfixed reference. They are committed as
documentation and **activate (get wired into `conformance.py`) when the reference
is ≥ 0.2.3**. Each file carries its own `_status`, `_activate_at_reference`,
`_spec`, and `_how_to_apply` keys.

| File | What it will check | Activate at |
|------|--------------------|-------------|
| [`vectors/policy_invalid.json`](vectors/policy_invalid.json) | Policy-schema validation and **fail-closed** loading (spec §5.1): each policy is invalid (unknown `match`/`constraints` key, unknown `decision`, missing `default`); a conformant Authorizer MUST reject it and MUST NOT silently skip an unknown constraint. | reference ≥ 0.2.3 |
| [`vectors/broker_query.json`](vectors/broker_query.json) | The Broker (PEP) **query obligation** (spec §4.2): the Broker MUST reconstruct the outgoing URL from the authorized `host`/`path`/`params`, MUST NOT forward the agent-supplied query verbatim, and MUST refuse an action whose URL it cannot reconstruct. (This is the additive obligation; folding the query into the fingerprint is a deferred breaking change and is **not** tested here.) | reference ≥ 0.2.3 |

## Schema validation

The example and the receipt vectors also validate against the
Expand All @@ -29,4 +43,6 @@ runs it on every change.
## Regenerating

The vectors are fixtures produced by the reference implementation. A port in any
language passes the CTK when it reproduces every value above.
language passes the CTK when it reproduces every value above. The pending vectors
above become fixtures (and get wired into `conformance.py`) once the reference
implements the corresponding §4.2 / §5.1 behaviour at ≥ 0.2.3.
58 changes: 58 additions & 0 deletions ctk/vectors/broker_query.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"_about": "CTK vectors for spec §4.2 — the Broker (PEP) query obligation. These exercise NOT-YET-RELEASED behaviour: query reconstruction at the Broker is a 0.3 additive obligation that the current reference (protocol 0.2) does not yet expose a Broker for, so these vectors are PENDING and are NOT wired into conformance.py. Activate (wire into conformance.py) when a conformant Broker is available at reference >= 0.2.3.",
"_status": "pending",
"_activate_at_reference": ">=0.2.3",
"_spec": "§4.2 (Broker query obligation, additive) and §10 (0.3 additive hardening)",
"_how_to_apply": "For each case, the Broker has authorized the action described by `authorized.host`/`authorized.path`/`authorized.params`. The Agent presents `agent_supplied_url`. A conformant Broker MUST reconstruct the outgoing URL from the authorized host/path/params and MUST NOT forward the agent-supplied query verbatim. `expected.outgoing_url` is the URL the Broker MUST send (null when it MUST refuse). `expected.action` is `reconstruct` or `refuse`.",
"_note_deferred": "Folding the query into the action_fingerprint preimage is a DEFERRED breaking change (§4.2) and is NOT covered here; these vectors test only the additive Broker obligation, which changes no hashed bytes.",
"cases": [
{
"name": "agent-supplied query is dropped; URL reconstructed from authorized fields",
"property": "no-verbatim-query-forward",
"authorized": {
"method": "GET",
"host": "api.example.com",
"path": "/orders",
"params": {}
},
"agent_supplied_url": "https://api.example.com/orders?to=attacker&limit=100",
"expected": {
"action": "reconstruct",
"outgoing_url": "https://api.example.com/orders",
"reason_contains": "query not derivable from authorized action"
}
},
{
"name": "decision-relevant value rides the query and is not in params: dropped",
"property": "no-decision-relevant-from-query",
"authorized": {
"method": "POST",
"host": "api.example.com",
"path": "/orders",
"params": { "amount": 2400, "currency": "USD", "destination": "internal" }
},
"agent_supplied_url": "https://api.example.com/orders?destination=attacker",
"expected": {
"action": "reconstruct",
"outgoing_url": "https://api.example.com/orders",
"reason_contains": "query parameter not represented in params"
}
},
{
"name": "Broker that cannot reconstruct the URL refuses the action",
"property": "refuse-when-irreconstructible",
"authorized": {
"method": "GET",
"host": null,
"path": null,
"params": {}
},
"agent_supplied_url": "https://api.example.com/orders?to=me",
"expected": {
"action": "refuse",
"outgoing_url": null,
"reason_contains": "cannot reconstruct URL"
}
}
]
}
Loading
Loading