Skip to content
Open
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
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,44 @@ reference implements it. See [`spec.md` §2.1](spec.md) for the version matrix.

## [Unreleased]

### Added (0.4 — draft, adoption clauses; additive on the 0.3 preimage, not yet reference-backed)
The document moves to **0.4 (draft)**. 0.4 is an **adoption-first** track: every
clause is additive (it changes no hashed or signed bytes and MAY be adopted by a
0.3 implementation), and the one breaking item considered for 0.4 — folding
request headers/body into the `action_fingerprint` preimage — is **deferred** to
keep 0.4 free of a re-integration tax. The reference still implements **0.3**;
these clauses are spec-led (*draft — not yet in reference*).
- §2.2 — **Broker interface (the PEP contract)**: consolidates the Broker's
transport-agnostic obligations (reconstruct from the authorized action only,
refuse what it cannot reconstruct, verify a token per §9.1 if required, execute
at most once per authorization, report its outcome) into one contract a third
party can implement against, plus a **separated-gateway** request/response
profile (`schema/broker-gateway.json`) that distinguishes a Broker refusal from
an upstream result. Conformance line added to §10.
- §7.2 — **Approval lifecycle & routing metadata**: optional `reason`,
`routing_group`/`required_approvers`, and `expires_at` on the approval record
(expiry **fails closed** → `deny`). Advisory only — set by the Authorizer/policy,
never the Agent; MUST NOT affect the §7 resolution guards.
- §7.3 — **Approval notification & callback protocol**: takes the human decision
out of the local console onto any surface (chat/web/ticketing) with the Agent
out of the loop. Outbound notification + signed single-use decision callback
(`schema/approval-callback.json`); the approval principal MUST be separate from
the Agent and an unverifiable/replayed callback leaves the approval `pending`
(fail-closed). The callback carries no action, so the §7 P1/P2 guards still bind
the action at release time. Conformance line added to §10.
- §8.4 — **Receipt context (optional, unsigned)**: a `context` object *outside*
the signed payload for correlating receipts to operational identity (agent /
session / trace / principal) without a breaking receipt change. NOT
tamper-evident; ignored by §8.1 verification; MUST NOT carry
authorization-relevant data. `schema/receipt.json` gains an optional `context`.
- §10 — a **0.4 — additive (adoption)** conformance block; §11 — security notes
for approval-callback authenticity/replay, Broker idempotency, and unsigned
receipt context.
- New schemas `schema/approval-callback.json`, `schema/broker-gateway.json` and
examples `examples/approval-callback.json`, `examples/broker-gateway.json`, all
wired into `validate.py`. No CTK `hashing`/`decisions`/`resolve`/`chain`/`token`
vector changes — `conformance.py` is unaffected (reference 0.3 ≤ spec 0.4).

### Fixed (spec prose — status bookkeeping caught up with the 0.3.0 query-fold)
- `spec.md` had not been updated when the reference shipped the §4.2 query-fold
(delego 0.3.0, CTK regenerated in #9) and contradicted itself: §2.1's version
Expand Down
38 changes: 22 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Contributions welcome](https://img.shields.io/badge/contributions-welcome-green.svg)](CONTRIBUTING.md)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Spec](https://img.shields.io/badge/spec-v0.3-blue.svg)](spec.md)
[![Spec](https://img.shields.io/badge/spec-v0.4--draft-blue.svg)](spec.md)

delego is a **deterministic pre-action authorization layer for AI agents**: it
sits between an agent that proposes actions and the credential broker that
Expand Down Expand Up @@ -60,7 +60,7 @@ byte-for-byte.
| Component | What it is |
|-----------|------------|
| **[Specification](spec.md)** | This document — the protocol. |
| **[Schemas](schema/)** | JSON Schemas for the policy, the audit receipt, and the authorization token. |
| **[Schemas](schema/)** | JSON Schemas for the policy, the audit receipt, the authorization token, and the 0.4-draft approval-callback and separated-gateway contracts. |
| **[Conformance Test Kit](ctk/README.md)** | Language-agnostic vectors any implementation can check itself against. |
| **[delego](https://github.com/Delego-Dev/delego)** | The reference implementation (Python) — policy engine, CLI, and MCP server. |

Expand All @@ -76,20 +76,26 @@ reproduce them; see [§10 Conformance](spec.md#10-conformance).

## Status & versioning

**v0.3 — frozen.** The spec/protocol is versioned `0.x` (the reference *package*
is `0.x.y`). The reference implements **0.3** (delego ≥ 0.3.0; the §9 token
profile since 0.3.3); each prior protocol version has a standalone document of
record in [`versions/`](versions/README.md) ([0.1](versions/spec-v0.1.md),
[0.2](versions/spec-v0.2.md)). 0.3 has two tracks: **additive hardening clauses**
— the §4.2 Broker query obligation (≤ 0.2 preimage), 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; and one
**breaking** change — folding the canonicalized URL query into the
`action_fingerprint` preimage (§4.2), reference-backed since delego 0.3.0 with
the `hashing` CTK vectors regenerated on the 0.3 preimage. 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)).
**v0.4 — draft.** The spec/protocol is versioned `0.x` (the reference *package*
is `0.x.y`). **0.1–0.3 are reference-backed** — the reference implements **0.3**
(delego ≥ 0.3.0; the §9 token profile since 0.3.3); each prior protocol version
has a standalone document of record in [`versions/`](versions/README.md)
([0.1](versions/spec-v0.1.md), [0.2](versions/spec-v0.2.md)). 0.3 added **additive
hardening** (the §4.2 Broker query obligation, policy-schema validation §5.1, the
authorization properties P1–P4 §7.1, head-anchoring §8.3, the authorization-token
profile §9) plus one **breaking** change (folding the canonicalized URL query into
the `action_fingerprint` preimage, §4.2, reference-backed since delego 0.3.0).

**0.4 (draft) is adoption-first** and **entirely additive** on the 0.3 preimage
(*draft — not yet in the reference*): a Broker-interface / separated-gateway
contract (§2.2), approval lifecycle & routing metadata (§7.2), an approval
**notification & callback protocol** that takes the human decision out of the
local console (§7.3), and an optional **unsigned** receipt `context` for
correlation (§8.4). The one breaking item considered for 0.4 — folding request
headers/body into the fingerprint — is **deferred** to avoid a re-integration
tax. 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)).

## Contributing

Expand Down
23 changes: 23 additions & 0 deletions examples/approval-callback.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"_note": "Illustrative (spec.md §7.3, draft 0.4). The notification delivers a parked approval to a human surface; the decision callback resolves it. Validated against schema/approval-callback.json#/$defs/notification and #/$defs/decision. Signature is a placeholder.",
"notification": {
"approval_id": "apr_4c9183f7606f",
"instruction": "place a small order",
"action_summary": "POST api.example.com/orders",
"outcome": "needs_approval",
"reason": "amount exceeds the auto-allow threshold",
"routing_group": "payments-approvers",
"required_approvers": 1,
"nonce": "01JBQK9Z6X8N3M2P0R5T7V9W3A",
"exp": 1759000600
},
"decision": {
"approval_id": "apr_4c9183f7606f",
"decision": "approve",
"nonce": "01JBQK9Z6X8N3M2P0R5T7V9W3A",
"ts": "2026-06-11T12:00:30Z",
"kid": "approval-2026-06",
"decision_note": "verified the recipient out of band",
"signature": "PLACEHOLDER_approval_principal_signature_over_approval_id_decision_nonce_ts"
}
}
20 changes: 20 additions & 0 deletions examples/broker-gateway.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"_note": "Illustrative (spec.md §2.2, draft 0.4). The request carries the authorized place-order action and its bindings (fpr/iht from ctk/vectors/hashing.json); the response reports one execution attempt. Validated against schema/broker-gateway.json#/$defs/request and #/$defs/response.",
"request": {
"action": {
"method": "POST",
"url": "https://api.example.com/orders",
"params": { "amount": 2400, "currency": "USD", "destination": "internal" }
},
"intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3",
"action_fingerprint": "4327df2637072bf058622d1f8baea6e431726f7332a50cd12ec970d6e43c2fd2",
"authorization_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.PLACEHOLDER.PLACEHOLDER"
},
"response": {
"executed": true,
"upstream": {
"status": 201,
"result": { "order_id": "ord_8f21" }
}
}
}
40 changes: 40 additions & 0 deletions schema/approval-callback.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/Delego-Dev/specification/blob/main/schema/approval-callback.json",
"title": "delego approval notification & decision callback",
"description": "Draft (0.4): the outbound notification that delivers a parked approval to a human surface, and the inbound signed decision callback that resolves it. See spec.md §7.3. Additive — no hashed or signed wire bytes change.",
"$defs": {
"notification": {
"type": "object",
"additionalProperties": false,
"description": "Outbound event emitted when an action is parked for approval. MUST NOT carry a credential or any secret.",
"required": ["approval_id", "instruction", "action_summary", "outcome", "nonce", "exp"],
"properties": {
"approval_id": { "type": "string", "description": "Identifier of the parked approval this notification is about." },
"instruction": { "type": "string", "description": "The human-readable instruction the action was proposed under (plaintext of intent_hash)." },
"action_summary": { "type": "string", "description": "Human-readable summary of the authorized action (e.g. 'POST api.example.com/orders')." },
"outcome": { "const": "needs_approval" },
"nonce": { "type": "string", "description": "Single-use value the matching decision callback MUST echo (replay defence)." },
"exp": { "type": "integer", "minimum": 0, "description": "Epoch seconds after which a decision callback for this notification is a no-op (the approval has expired → deny)." },
"reason": { "type": "string", "description": "Why approval is required (spec.md §7.2)." },
"routing_group": { "type": "string", "description": "Who SHOULD decide (spec.md §7.2). Advisory; set by the Authorizer/policy, never the Agent." },
"required_approvers": { "type": "integer", "minimum": 1, "description": "How many distinct approvals are required before status becomes approved (spec.md §7.2)." }
}
},
"decision": {
"type": "object",
"additionalProperties": false,
"description": "Inbound callback that resolves a parked approval. Authenticated by an approval key the Agent does not possess (spec.md §7.3). Carries NO action — the §7 fingerprint/intent guards still bind the action at release time.",
"required": ["approval_id", "decision", "nonce", "ts", "signature"],
"properties": {
"approval_id": { "type": "string", "description": "The parked approval being resolved." },
"decision": { "enum": ["approve", "deny"] },
"nonce": { "type": "string", "description": "MUST match the notification's nonce; single-use." },
"ts": { "type": "string", "description": "ISO-8601 UTC timestamp of the human decision." },
"signature": { "type": "string", "description": "Authentication over (approval_id, decision, nonce, ts) by the approval principal (human-approval trust domain, §2). An unverifiable signature MUST leave the approval pending." },
"kid": { "type": "string", "description": "OPTIONAL approval-key id, so a verifier can select the key and support rotation." },
"decision_note": { "type": "string", "description": "OPTIONAL human note recorded for audit (spec.md §7.2). MUST NOT alter the binding." }
}
}
}
}
58 changes: 58 additions & 0 deletions schema/broker-gateway.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/Delego-Dev/specification/blob/main/schema/broker-gateway.json",
"title": "delego separated-gateway request & response",
"description": "Draft (0.4): the JSON contract between an Authorizer-side caller and a separated Broker (PEP). See spec.md §2.2. The Broker reconstructs the request from the authorized action only and reports a single execution attempt, distinguishing a refusal from an upstream result. Additive — no hashed or signed wire bytes change.",
"$defs": {
"request": {
"type": "object",
"additionalProperties": false,
"description": "Authorizer-side → Broker. Carries the authorized action and its bindings; the Broker MUST reconstruct the outgoing request from these fields only (spec.md §2.2).",
"required": ["action", "intent_hash", "action_fingerprint"],
"properties": {
"action": {
"type": "object",
"additionalProperties": false,
"required": ["method", "url"],
"properties": {
"method": { "type": "string", "description": "Uppercase HTTP method, or the action verb for a non-HTTP transport." },
"url": { "type": "string", "description": "The authorized target (host/path/query as fingerprinted). The Broker refuses a #fragment (spec.md §4.2)." },
"params": { "type": "object", "description": "The authorized, decision-relevant fields (spec.md §4)." }
}
},
"intent_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"action_fingerprint": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"authorization_token": { "type": "string", "description": "OPTIONAL compact JWS (spec.md §9). If present, a token-requiring Broker MUST verify it per §9.1 before injecting a credential." }
}
},
"response": {
"type": "object",
"additionalProperties": false,
"description": "Broker → Authorizer-side. Reports a single execution attempt. 'refused' MUST be distinguished from an executed upstream result so the Authorizer records the right receipt (spec.md §2.2, §8).",
"required": ["executed"],
"properties": {
"executed": { "type": "boolean", "description": "True iff the Broker injected a credential and forwarded the request upstream." },
"refused": {
"type": "object",
"additionalProperties": false,
"description": "Present iff the Broker would not or could not enforce. Mutually exclusive with an upstream result.",
"required": ["code", "message"],
"properties": {
"code": { "enum": ["reconstruction_failed", "fragment_present", "token_invalid", "unsupported", "already_consumed"] },
"message": { "type": "string" }
}
},
"upstream": {
"type": "object",
"additionalProperties": false,
"description": "Present iff executed is true: what the Service returned.",
"properties": {
"status": { "type": "integer", "description": "Upstream status code (HTTP) or transport-equivalent." },
"result": { "description": "Opaque upstream result body, echoed for the caller." },
"error": { "type": "string", "description": "Set when the execution attempt itself failed (e.g. a timeout); distinct from a Broker refusal." }
}
}
}
}
}
}
4 changes: 4 additions & 0 deletions schema/receipt.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
"type": "string",
"pattern": "^[0-9a-f]+$",
"description": "Ed25519 signature over UTF-8(entry_hash), hex-encoded."
},
"context": {
"type": "object",
"description": "OPTIONAL, UNSIGNED correlation metadata (spec.md §8.4, draft 0.4). NOT covered by entry_hash/signature and ignored by §8.1 verification; MUST NOT carry authorization-relevant data. Correlation only, e.g. agent_id / session_id / trace_id / principal."
}
}
}
Loading