From 8e1c787bc0ca2666da564e0f682ac5b4f1b95e36 Mon Sep 17 00:00:00 2001 From: Koishore Roy Date: Thu, 4 Jun 2026 01:42:03 +0530 Subject: [PATCH 1/2] Spec 0.3.0-draft: version tiers, 0.2 hardening, spec-leads-code governance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the protocol in versioned tiers so the specification leads the reference implementation rather than trailing it. Protocol versions (new §2.1): - 0.1.0 — baseline, reference-complete, CTK-backed. - 0.2.0 — approval & audit hardening, now reference-backed and CTK-tested: §7 approvals bound to intent_hash and made single-use (full resolution algorithm + status lifecycle); §5/§8 approved-action receipts carry their parking rule so rate_limit counts them, and an unevaluable rate_limit denies; §8.1 a malformed/partial receipt is a verification failure, not a crash. - 0.3.0 (draft — not yet in reference): §4.2 query-string-bound fingerprint (closes the confused-deputy gap where the query is excluded), §9 token. Conformance & governance: - ctk/vectors/resolve.json — authoritative §7 resolution vectors (fingerprint guard, intent guard, single-use replay); ctk/README documents them. - conformance.py + a CI job replay every CTK vector against the installed reference and assert reference __protocol_version__ <= spec version, so the code can never silently drift ahead of the spec. - CONTRIBUTING: "the spec leads the reference"; fork-and-PR; and an AI-assisted-contributions policy (disclose, stricter review, human accountable). - PR template with the AI disclosure and a normative-change checklist. - CHANGELOG added; §6 determinism note names evaluation time as an input. The reference reproduces all hashing, decision, resolve, and chain vectors. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/PULL_REQUEST_TEMPLATE.md | 35 +++++++ .github/workflows/validate.yml | 14 +++ CHANGELOG.md | 49 +++++++++ CONTRIBUTING.md | 41 +++++++- conformance.py | 152 +++++++++++++++++++++++++++ ctk/README.md | 1 + ctk/vectors/resolve.json | 173 +++++++++++++++++++++++++++++++ spec.md | 146 +++++++++++++++++++++++--- 8 files changed, 591 insertions(+), 20 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CHANGELOG.md create mode 100644 conformance.py create mode 100644 ctk/vectors/resolve.json diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f88d931 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,35 @@ + + +## What & why + + + +## AI assistance disclosure (required) + + + +- [ ] No AI assistance. +- [ ] AI-assisted. Tool(s) and how used: ______ +- [ ] AI-generated, human-reviewed. I have read every line and am accountable for it. + +## Kind of change + +- [ ] Editorial only (wording, examples, links) — no normative change. +- [ ] **Normative** change (canonicalization, hashing, policy/decision, audit + chain, approval binding, or the authorization token). + +## Checklist + +- [ ] Forked the repo; this PR comes from a branch in my fork. +- [ ] `python validate.py` is green (examples/vectors validate against the schemas). +- [ ] `python conformance.py` is green (the reference reproduces every CTK vector). + +## For a normative change (additionally) + +- [ ] Updated or added **CTK vectors, regenerated from the reference** (not hand-edited). +- [ ] Updated the **§2.1 version matrix** and tagged new clauses *(since 0.x)* or + *(0.x, draft — not yet in reference)*. +- [ ] The spec **leads** the reference: new behaviour is specified before/independent + of code shipping it; the reference's `__protocol_version__` stays ≤ this spec's version. +- [ ] If the receipt fields or canonicalization changed: bumped the spec version + and the schema version together (§8.2), and updated `CHANGELOG.md`. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c8557d3..be47b0b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -19,3 +19,17 @@ jobs: - run: pip install jsonschema pyyaml - name: Validate examples + CTK vectors against the schemas run: python validate.py + + conformance: + # The spec leads the reference: replay every CTK vector against the installed + # reference and assert its protocol version never exceeds this spec's version. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + # Install the reference. Once published this becomes `pip install delego`. + - run: pip install "delego @ git+https://github.com/Delego-Dev/delego@main" + - name: Replay CTK vectors against the reference (fail on drift) + run: python conformance.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a5abcac --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog — delego wire specification + +All notable changes to the protocol are documented here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The specification is +versioned independently of the reference implementation and **leads** it: a +behaviour is specified (marked *draft — not yet in reference*) before the +reference implements it. See [`spec.md` §2.1](spec.md) for the version matrix. + +## [0.3.0-draft] — Unreleased + +### 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. +- §7 — approvals are bound to the `intent_hash` as well as the + `action_fingerprint`, and are **single-use** (an approval releases its action + at most once; a replayed release is denied). Full resolution algorithm and the + approval status lifecycle (`pending → approved → consumed`, `denied`) specified. +- §5 / §8 — an approved action's `execution`/`allow` receipt carries the rule it + was parked under, so `rate_limit` counts it; an unevaluable `rate_limit` denies. +- §8.1 — a malformed or partial receipt is a verification *failure*, not an error + that aborts the walk. +- `ctk/vectors/resolve.json` — authoritative vectors for the §7 resolution rules + (fingerprint guard, intent guard, single-use replay). +- `conformance.py` + a CI job that replays every CTK vector against the reference + and asserts the spec leads it. + +### Changed +- §6 — the determinism requirement now names *evaluation time* as an input + (the `rate_limit` window), rather than implying a time-independent function. +- Document version → 0.3.0-draft. + +## [0.1.0-draft] — initial specification + +### Added +- §3 canonical JSON; §4 intent hash + action fingerprint; §5–§6 deterministic + policy & decision; §7 fingerprint-bound approval (confused-deputy guard); + §8 append-only, hash-linked, Ed25519-signed audit chain + verification; + §9 authorization-token draft. +- JSON Schemas (`schema/`), CTK vectors (`ctk/`), and `validate.py`. + +[0.3.0-draft]: https://github.com/Delego-Dev/specification +[0.1.0-draft]: https://github.com/Delego-Dev/specification/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a89029e..0fb720b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,15 +6,46 @@ truth for the wire format; the reference implementation lives at ## How to propose a change -1. **Open an issue** describing the problem or gap before a large change. -2. For wording, examples, or clarifications, open a pull request directly. -3. A change to a **NORMATIVE** section (canonicalization, hashing, policy +1. **Fork the repository** and work on a branch in your fork. Open a pull request + from the fork; direct pushes to this repo are not accepted. +2. **Open an issue** describing the problem or gap before a large change. +3. For wording, examples, or clarifications, open a pull request directly. +4. A change to a **NORMATIVE** section (canonicalization, hashing, policy evaluation, the receipt/audit chain, or the authorization token) MUST: - keep the spec consistent with the [Conformance Test Kit](ctk/README.md), and - update or add CTK vectors, regenerated from the reference implementation, so the prose and the vectors never drift. -4. Keep `validate.py` green — examples and vectors must validate against the - [schemas](schema/). CI runs it on every push and PR. +5. Keep `validate.py` **and** `conformance.py` green — examples/vectors must + validate against the [schemas](schema/), and the reference must reproduce + every CTK vector. CI runs both on every push and PR. +6. Fill in the pull-request template completely, including the AI-assistance + disclosure (see below). + +## The spec leads the reference + +This is the source of truth: **normative behaviour is specified here first**, then +implemented. A new behaviour lands in the spec marked *draft — not yet in +reference* (see the §2.1 version matrix), and becomes reference-backed only once +the implementation reproduces its CTK vectors. The reference's +`__protocol_version__` MUST always be ≤ this document's version; `conformance.py` +enforces it. Do not document behaviour here to *match* code that already shipped +ahead of the spec — that is the failure mode this rule exists to prevent. + +## AI-assisted contributions + +AI coding assistants are welcome tools, but AI-generated or AI-assisted +contributions to a security protocol carry extra risk, so: + +- **Disclose it.** The PR template has a required field for whether and how AI was + used. Be honest and specific. +- **Expect stricter review.** AI-assisted PRs — especially ones touching NORMATIVE + sections, the threat model, or the CTK — receive closer scrutiny and may take + longer to merge. Unreviewed, bulk-generated PRs will be closed. +- **You are accountable.** The human author is responsible for every line: its + correctness, that the CTK vectors were regenerated (not hand-edited to pass), + and that no invariant or security property was weakened. "The model wrote it" + is not a defence. +- **Process is the same — fork, template, green CI.** No fast path for AI output. ## Versioning diff --git a/conformance.py b/conformance.py new file mode 100644 index 0000000..28ffee5 --- /dev/null +++ b/conformance.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Replay the Conformance Test Kit against the reference implementation. + +This is the gate that keeps the **spec ahead of the code**: it reproduces every +CTK vector with the installed ``delego`` reference and asserts the reference's +protocol version never exceeds this document's version. CI fails on any drift. + +Run locally or in CI: python conformance.py +Requires: the ``delego`` reference (``pip install delego`` or from git), pyyaml. +""" +from __future__ import annotations + +import json +import re +import shutil +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +VEC = ROOT / "ctk" / "vectors" +fails = 0 + + +def fail(msg: str) -> None: + global fails + fails += 1 + print(f" FAIL {msg}") + + +def ok(msg: str) -> None: + print(f" ok {msg}") + + +try: + import delego + from delego import ProposedAction, build_firewall + from delego.config import Paths +except ImportError: # pragma: no cover + print("delego is not installed; install the reference to run conformance.") + print(" pip install delego # or: pip install git+https://github.com/Delego-Dev/delego") + sys.exit(1) + + +def spec_version() -> tuple[int, ...]: + text = (ROOT / "spec.md").read_text(encoding="utf-8") + m = re.search(r"\*\*Version:\*\*\s*([0-9]+\.[0-9]+\.[0-9]+)", text) + return tuple(int(x) for x in m.group(1).split(".")) + + +def ver(s: str) -> tuple[int, ...]: + return tuple(int(x) for x in s.split(".")) + + +def build(policy_name: str = "examples/policy.example.yaml"): + home = Path(tempfile.mkdtemp()) + shutil.copy(ROOT / policy_name, home / "policy.yaml") + return build_firewall(Paths.resolve(home)) + + +def action(a: dict) -> ProposedAction: + return ProposedAction(a["instruction"], a["method"], a["url"], a.get("params", {})) + + +# --- spec leads the reference ------------------------------------------------ # +print("protocol version:") +ref = getattr(delego, "__protocol_version__", None) +if ref is None: + fail("reference does not expose __protocol_version__") +elif ver(ref) > spec_version(): + fail(f"reference protocol {ref} EXCEEDS spec {'.'.join(map(str, spec_version()))} — spec must lead") +else: + ok(f"reference {ref} <= spec {'.'.join(map(str, spec_version()))}") + +# --- §4 hashing -------------------------------------------------------------- # +print("hashing (§4):") +for e in json.loads((VEC / "hashing.json").read_text()): + a = action(e["action"]) + if a.intent_hash == e["intent_hash"] and a.fingerprint == e["action_fingerprint"]: + ok(e["action"]["url"]) + else: + fail(f"hash mismatch for {e['action']['url']}") + +# --- §5–§6 decisions --------------------------------------------------------- # +print("decisions (§5–§6):") +for e in json.loads((VEC / "decisions.json").read_text()): + fw = build() + got = list(fw.policy.evaluate(action(e["action"]), fw.audit)) + if got == [e["outcome"], e["rule"], e["reasons"]]: + ok(f"{e['outcome']:14} {e['action']['url']}") + else: + fail(f"decision {got} != {[e['outcome'], e['rule'], e['reasons']]}") + +# --- §7 resolve / approval lifecycle (0.2) ----------------------------------- # +print("resolve (§7, 0.2):") +for e in json.loads((VEC / "resolve.json").read_text()): + fw = build() + approval_id = "apr_vector" + if e["approval"] is not None: + rec = { + "id": approval_id, + "status": e["approval"]["status"], + "action_fingerprint": e["approval"]["action_fingerprint"], + "intent_hash": e["approval"]["intent_hash"], + "rule": e["approval"].get("rule"), + "instruction": e["presented_action"]["instruction"], + "summary": "", + "approver": "human", + "created_at": None, + "decided_at": None, + } + fw.approvals._append(rec) # seed the store with the vector's parked approval + d = fw.resolve(approval_id, action(e["presented_action"])) + want = e["expected"] + if d.outcome == want["outcome"] and any(want["reason_contains"] in r for r in d.reasons): + ok(f"{d.outcome:14} {e['name']}") + else: + fail(f"{e['name']}: got ({d.outcome}, {d.reasons}) want {want}") + +# --- §8.1 chain verification ------------------------------------------------- # +print("chain verification (§8.1):") + + +def verify_chain(jsonl: str): + from cryptography.hazmat.primitives import serialization + from delego.audit import AuditLog + + home = Path(tempfile.mkdtemp()) + shutil.copy(VEC / "signing_key.pub", home / "signing_key.pub") + log = AuditLog(home / "audit.log.jsonl", home / "signing_key.pem", home / "signing_key.pub") + shutil.copy(VEC / jsonl, log.path) + log._pub = serialization.load_pem_public_key((home / "signing_key.pub").read_bytes()) + log._load_keys = lambda: None + return log.verify() + + +valid, _ = verify_chain("chain.jsonl") +exp = json.loads((VEC / "chain.expected.json").read_text()) +ok("chain.jsonl valid") if valid == exp["valid"] else fail("chain.jsonl validity mismatch") + +tvalid, tprobs = verify_chain("chain.tampered.jsonl") +texp = json.loads((VEC / "chain.tampered.expected.json").read_text()) +if tvalid == texp["valid"] and tprobs == texp["problems"]: + ok("chain.tampered.jsonl fails as expected") +else: + fail(f"tampered chain: got valid={tvalid} problems={tprobs}") + +print() +if fails: + print(f"{fails} conformance failure(s) — reference does not match the spec's CTK.") + sys.exit(1) +print("reference reproduces every CTK vector; spec leads the reference.") diff --git a/ctk/README.md b/ctk/README.md index 708de76..6b7ebf0 100644 --- a/ctk/README.md +++ b/ctk/README.md @@ -13,6 +13,7 @@ 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. | The policy used for the decision and chain vectors is [`../examples/policy.example.yaml`](../examples/policy.example.yaml). The public diff --git a/ctk/vectors/resolve.json b/ctk/vectors/resolve.json new file mode 100644 index 0000000..362d61b --- /dev/null +++ b/ctk/vectors/resolve.json @@ -0,0 +1,173 @@ +[ + { + "name": "unknown approval id is denied", + "approval": null, + "presented_action": { + "instruction": "pay my electricity bill", + "method": "POST", + "url": "https://api.examplebank.in/transfer", + "params": { + "amount": 2400, + "currency": "INR", + "beneficiary_type": "domestic" + } + }, + "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "expected": { + "outcome": "deny", + "reason_contains": "unknown approval id" + } + }, + { + "name": "fingerprint mismatch is refused (before status)", + "approval": { + "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "status": "pending", + "rule": "small-domestic-transfer" + }, + "presented_action": { + "instruction": "pay my electricity bill", + "method": "POST", + "url": "https://api.examplebank.in/transfer", + "params": { + "amount": 2400, + "currency": "INR", + "beneficiary_type": "domestic", + "to": "attacker" + } + }, + "presented_fingerprint": "f8e933c3ea285c35d9b99839a59d217e6b1f5e433f198c3438555db3210b3499", + "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "expected": { + "outcome": "deny", + "reason_contains": "approval/action mismatch: this approval was issued for a different action (possible confused-deputy / substituted action)" + } + }, + { + "name": "intent mismatch is refused", + "approval": { + "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "status": "approved", + "rule": "small-domestic-transfer" + }, + "presented_action": { + "instruction": "send the deposit to my landlord", + "method": "POST", + "url": "https://api.examplebank.in/transfer", + "params": { + "amount": 2400, + "currency": "INR", + "beneficiary_type": "domestic" + } + }, + "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "presented_intent_hash": "03fa5b7e3b8e6205bf83be28fa423e311676533157af96923d3bf6651087b37d", + "expected": { + "outcome": "deny", + "reason_contains": "approval/intent mismatch: this approval was issued for a different instruction" + } + }, + { + "name": "matching action while pending awaits approval", + "approval": { + "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "status": "pending", + "rule": "small-domestic-transfer" + }, + "presented_action": { + "instruction": "pay my electricity bill", + "method": "POST", + "url": "https://api.examplebank.in/transfer", + "params": { + "amount": 2400, + "currency": "INR", + "beneficiary_type": "domestic" + } + }, + "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "expected": { + "outcome": "needs_approval", + "reason_contains": "awaiting human approval" + } + }, + { + "name": "human-denied action is refused", + "approval": { + "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "status": "denied", + "rule": "small-domestic-transfer" + }, + "presented_action": { + "instruction": "pay my electricity bill", + "method": "POST", + "url": "https://api.examplebank.in/transfer", + "params": { + "amount": 2400, + "currency": "INR", + "beneficiary_type": "domestic" + } + }, + "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "expected": { + "outcome": "deny", + "reason_contains": "human denied this action" + } + }, + { + "name": "approved action is released once", + "approval": { + "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "status": "approved", + "rule": "small-domestic-transfer" + }, + "presented_action": { + "instruction": "pay my electricity bill", + "method": "POST", + "url": "https://api.examplebank.in/transfer", + "params": { + "amount": 2400, + "currency": "INR", + "beneficiary_type": "domestic" + } + }, + "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "expected": { + "outcome": "allow", + "reason_contains": "human approved by 'human'" + } + }, + { + "name": "replay of a consumed approval is refused", + "approval": { + "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "status": "consumed", + "rule": "small-domestic-transfer" + }, + "presented_action": { + "instruction": "pay my electricity bill", + "method": "POST", + "url": "https://api.examplebank.in/transfer", + "params": { + "amount": 2400, + "currency": "INR", + "beneficiary_type": "domestic" + } + }, + "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", + "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "expected": { + "outcome": "deny", + "reason_contains": "approval already used: this single-use approval has already released its action" + } + } +] diff --git a/spec.md b/spec.md index 4c40ec9..d6b8949 100644 --- a/spec.md +++ b/spec.md @@ -1,6 +1,6 @@ # delego Wire Specification -**Version:** 0.1.0 (draft) · **Status:** Draft · **License:** Apache-2.0 +**Version:** 0.3.0 (draft) · **Status:** Draft · **License:** Apache-2.0 This document specifies the **delego protocol**: how an *action proposed by an agent* is authorized — deterministically, with no LLM in the decision path — @@ -37,6 +37,25 @@ model, or any non-deterministic input, when rendering a decision (§6). The Agen is assumed compromisable; the protocol's job is to bound blast radius and prove provenance, not to infer the human's intent semantically. +## 2.1 Protocol versions + +delego is versioned so the **specification can lead the reference +implementation**. A behaviour is specified here *first* — marked *draft, not yet +in reference* — and becomes reference-backed only once the implementation +reproduces its Conformance Test Kit vectors (§10). The reference's protocol +version (`delego.__protocol_version__`) therefore **MUST** always be ≤ the version +of this document. + +| Version | Status | Adds | +|---------|--------|------| +| **0.1.0** | reference-complete, CTK-backed | Canonical JSON (§3); intent hash + action fingerprint (§4); deterministic policy & decision, first-match-wins, fail-closed (§5–§6); fingerprint-bound approval / confused-deputy guard (§7); append-only, hash-linked, Ed25519-signed audit chain + verification (§8). | +| **0.2.0** | reference-complete, CTK-backed | Approval & audit hardening. Approvals are additionally bound to the `intent_hash` and made **single-use** (§7); an approved action's `execution` receipt carries the rule it was parked under, so `rate_limit` counts it (§5, §8); verification treats a malformed or partial receipt as a *failure* rather than aborting the walk (§8.1). | +| **0.3.0** | **draft — not yet in reference** | Query-string-bound fingerprint (§4.2); signed authorization token (§9, §9.1). | + +This document is at **0.3.0 (draft)**; the reference implements **0.2.0**. Clauses +introduced after 0.1.0 are tagged inline — *(since 0.2)* for reference-backed +behaviour, *(0.3, draft)* for the not-yet-implemented frontier. + ## 3. Canonicalization (NORMATIVE) Every value that is hashed or signed is first serialized with **canonical JSON**: @@ -112,6 +131,36 @@ transfer {…, to:"attacker"} (one param added) See [`ctk/vectors/hashing.json`](ctk/vectors/hashing.json) for the full set. +### 4.2 Query string *(0.3, draft — not yet in reference)* + +Through 0.2 the fingerprint covers `method`, `host`, `path`, and the +agent-declared `params`; the URL's **query string is not part of the +fingerprint**. Two requests that differ only in their query (e.g. +`/transfer?to=me` vs `/transfer?to=attacker`) therefore share one fingerprint — +a confused-deputy gap if decision-relevant data rides the query. + +Until 0.3, an Authorizer and Broker **MUST** ensure no decision-relevant value is +taken from the query string: a Broker **MUST NOT** forward query parameters that +are not represented in `params`. + +In **0.3** the fingerprint folds the canonicalized query into its preimage: + +``` +query = the URL query parsed into a list of [name, value] pairs, + sorted lexicographically by (name, value) +action_fingerprint = sha256(canonical_json({ + "host": host, + "method": uppercase(method), + "params": params, + "path": path, + "query": query +})) +``` + +This changes the fingerprint preimage and is therefore a **breaking** change +(§8.2); it bumps the protocol version and ships with updated `hashing` CTK vectors +when the reference implements it. + ## 5. Policy A policy is a document (YAML or JSON) conforming to @@ -143,7 +192,13 @@ an empty match matches nothing. value MUST be ≤ `max`. - `allow_list` — `{ field, in: [...] }`. The named field's value MUST be a member. - `rate_limit` — `{ max, per: minute|hour|day }`. Counts prior `allow` receipts - for the same rule within the window (§8); denies at or above `max`. + for the same rule within the window (§8); denies at or above `max`. A + `rate_limit` the Authorizer cannot evaluate (no audit ledger available) **MUST** + `deny`, never silently pass. *(since 0.2)* The `allow` receipt of an action + released through human approval (§7) carries the rule the approval was parked + under, so such executions are counted too. The cap is evaluated at **decision** + time; an action parked for approval is counted only once released, so several + actions parked before any release MAY each execute even past `max`. > **Glob note (v0.1).** Path globbing is coarse: `**` and `*` are treated alike > and both span `/`. Per-segment globbing is a planned refinement; a conformant @@ -161,23 +216,61 @@ Evaluation order is fixed: The result is `{ outcome, rule, reasons }` where `outcome` ∈ `{allow, deny, needs_approval}`, `rule` is the matched rule name (or null), and `reasons` is a -list of human-readable strings. Evaluation **MUST** be a pure function of -(action, policy, audit ledger) — identical inputs yield identical outputs. +list of human-readable strings. Evaluation **MUST** be a pure, deterministic +function of (action, policy, audit ledger, evaluation time) — there is no +stochastic or hidden input. (The `rate_limit` constraint is the only one that +reads the evaluation time, to size its window; all others depend on action and +policy alone.) Authoritative decision vectors: [`ctk/vectors/decisions.json`](ctk/vectors/decisions.json). ## 7. Approval binding (the confused-deputy guard) -When the outcome is `needs_approval`, the Authorizer parks the action bound to -its exact `action_fingerprint` and returns an `approval_id`. A human decides -out-of-band. +When the outcome is `needs_approval`, the Authorizer parks the action — bound to +its exact `action_fingerprint` **and** `intent_hash` — and returns an +`approval_id`. A human decides out-of-band. An approval moves through a status +lifecycle: + +``` +pending ──approve──▶ approved ──release──▶ consumed + └─────deny──────▶ denied +``` + +`approved` and `denied` are set by the human decision; `consumed` is set by the +Authorizer when the approval releases its action (below). `denied` and `consumed` +are terminal. When the Agent later presents an action to *release* an approval, the Authorizer -**MUST** recompute the presented action's `action_fingerprint` and compare it to -the fingerprint the approval was issued for. **If they differ, the outcome MUST -be `deny`** — the approval does not transfer to a different action. This holds -**before** the approval's status is considered: a substituted action is refused -whether or not a human has approved the original. +**MUST** evaluate, in this order: + +1. **Fingerprint guard.** Recompute the presented action's `action_fingerprint` + and compare it to the one the approval was issued for. **If they differ, the + outcome MUST be `deny`** — the approval does not transfer to a different + action. This check is made **before** the approval's status is considered: a + substituted action is refused whether or not a human has approved the original. + +2. **Intent guard** *(since 0.2)*. Recompute the presented action's `intent_hash` + and compare it to the approval's. **If they differ, the outcome MUST be + `deny`** — the approval does not transfer to a different claimed instruction, + so an approval granted for one stated purpose cannot be re-pointed at another. + +3. **Status.** Then resolve by the approval's status: + - `pending` → outcome `needs_approval` (no human decision yet); + - `denied` → outcome `deny`; + - `consumed` → outcome `deny` *(since 0.2)* — see single-use, below; + - `approved` → release the action: the Authorizer **MUST** transition the + approval to `consumed`, then execute, emitting an `execution`/`allow` receipt + that carries the rule the approval was parked under (§5, §8). + +**Single-use** *(since 0.2)*. An approval releases its action **at most once**. +The transition to `consumed` **MUST** happen no later than execution, so a +*replayed* release of an already-consumed approval is refused by the status check +above. One human "yes" authorises exactly one execution; a redirected Agent +cannot replay it to run the same action again. + +Every refusal in this section **MUST** be recorded as an `execution`/`deny` +receipt (§8). Authoritative vectors: +[`ctk/vectors/resolve.json`](ctk/vectors/resolve.json). ## 8. Receipt & audit chain (NORMATIVE) @@ -220,6 +313,12 @@ An Auditor verifies a chain by walking it in `seq` order. For each receipt it The chain is valid iff every check passes for every receipt. Editing, reordering, inserting, or deleting any receipt breaks at least one check. +*(since 0.2)* A **structurally invalid** receipt — one not parseable as JSON, or +missing any of the eleven signed fields — **MUST** be treated as a verification +failure at that position (and the chain link across it as broken), not as an error +that aborts the walk. A verifier that throws on a malformed receipt is one an +attacker can silence by corrupting a single byte. + Authoritative vectors: a valid chain ([`ctk/vectors/chain.jsonl`](ctk/vectors/chain.jsonl), expected [`chain.expected.json`](ctk/vectors/chain.expected.json)) and a tampered chain that MUST fail at `seq 0` ([`chain.tampered.jsonl`](ctk/vectors/chain.tampered.jsonl)), @@ -231,10 +330,10 @@ The set of payload fields is part of the wire format. Any change to it is a **breaking** change: it MUST bump this specification's version and the receipt `schema` version together, or previously-signed chains stop verifying. -## 9. Authorization Token (NORMATIVE) +## 9. Authorization Token (NORMATIVE) *(0.3, draft — not yet in reference)* > **Status: draft, not yet implemented in the reference.** This section defines -> the v0.3 protocol that closes the gap where a Broker would inject a credential +> the 0.3 protocol that closes the gap where a Broker would inject a credential > for *any* in-scope request. It converges with > [RFC 8693](https://www.rfc-editor.org/rfc/rfc8693) (token exchange). @@ -280,11 +379,28 @@ See [`examples/authorization-token.md`](examples/authorization-token.md) and ## 10. Conformance +An implementation declares the highest protocol version (§2.1) it implements, and +**MUST** satisfy every clause at or below that version and reproduce that version's +CTK vectors. The reference implements **0.2.0**. + +**0.1.0** - A conformant **Authorizer** MUST implement §3–§8 and reproduce the CTK `hashing` and `decisions` vectors, and produce chains that verify per §8.1. - A conformant **Auditor** MUST implement §8.1 and agree with the CTK `chain` expectations (valid and tampered). -- A conformant **Broker** that participates in §9 MUST implement §9.1. + +**0.2.0** (additionally) +- An **Authorizer** MUST implement the §7 intent guard and single-use semantics + and reproduce the CTK `resolve` vectors; MUST attribute an approved action's + `allow` receipt to its parking rule (§5, §8); MUST `deny` a `rate_limit` it + cannot evaluate (§5). +- An **Auditor** MUST treat a malformed or partial receipt as a verification + failure, not an error (§8.1). + +**0.3.0** (draft — not yet in reference) +- An **Authorizer** binds the query string into the fingerprint (§4.2) and MAY + mint authorization tokens (§9). +- A **Broker** that participates in §9 MUST implement §9.1. ## 11. Security considerations From 9c7265fad77f725785cfd8ebd3843ed2e781e746 Mon Sep 17 00:00:00 2001 From: Koishore Roy Date: Thu, 4 Jun 2026 01:59:43 +0530 Subject: [PATCH 2/2] Genericize the example and regenerate all CTK vectors (drop BFSI framing) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the banking (BFSI) example with a neutral generic HTTP/JSON API across the spec prose and the whole Conformance Test Kit, so the protocol's examples don't read as bank-specific. - examples/policy.example.yaml: api.example.com, place-order (POST /orders, USD amount cap + destination allow_list); kept byte-identical to the reference. - spec.md §4.1 worked-example hashes, §4.2 query example, §5 policy block: generic. - CTK regenerated from the reference: hashing, decisions, resolve, and a fresh-key chain (+ tampered) — new signing_key.pub, expected files updated. - authorization-token example: generic place-order fpr/iht + rule. validate.py and conformance.py both green against the reference. Co-Authored-By: Claude Opus 4.8 (1M context) --- ctk/vectors/chain.jsonl | 12 +-- ctk/vectors/chain.tampered.jsonl | 12 +-- ctk/vectors/decisions.json | 34 ++++----- ctk/vectors/hashing.json | 46 +++++------ ctk/vectors/resolve.json | 122 +++++++++++++++--------------- ctk/vectors/signing_key.pub | 2 +- examples/authorization-token.json | 9 ++- examples/authorization-token.md | 8 +- examples/policy.example.yaml | 32 ++++---- spec.md | 26 +++---- 10 files changed, 153 insertions(+), 150 deletions(-) diff --git a/ctk/vectors/chain.jsonl b/ctk/vectors/chain.jsonl index 973628d..6186867 100644 --- a/ctk/vectors/chain.jsonl +++ b/ctk/vectors/chain.jsonl @@ -1,6 +1,6 @@ -{"seq": 0, "ts": "2026-06-03T19:32:40.406187+00:00", "phase": "execution", "outcome": "allow", "rule": "read-accounts", "reasons": ["rate_limit: 1/50 per hour", "executed via broker"], "intent_hash": "cb091de0cf51909850329b23d6552563700889909c2d0db240dc9614720fc224", "action_fingerprint": "8dbf1c7409bc66eda1be5679163ebc455e15078226d655d8b7e406c49688e2dd", "action_summary": "GET api.examplebank.in/accounts/me", "approval_id": null, "prev_hash": "GENESIS", "entry_hash": "f262318db692c5434efed0154d46a9cdfc17a941f4b808e9b6a1fa8bdac3bd6b", "signature": "7d9fbf6c7ca3a173412f2b4bc1da621527fc28402184e2da950914be38e6b65c4d20fe3367d9ba03f327c076e35b8fe65ef9496a0c5cca1a7e3834bfa406b90f"} -{"seq": 1, "ts": "2026-06-03T19:32:40.407798+00:00", "phase": "decision", "outcome": "deny", "rule": "no-access-control-changes", "reasons": ["forbidden: Agents may never modify permissions or sharing"], "intent_hash": "f9d78fae596e4299cf995cbdfb346330d0a0647b650aaeeb971bfcf8a33830e3", "action_fingerprint": "b7b09bb5b2034e06ac7c42d6ec1f2622686b9046d1f358a7172d166c62e93d63", "action_summary": "POST api.examplebank.in/accounts/me/permissions {\"grant\":\"x@y.com\"}", "approval_id": null, "prev_hash": "f262318db692c5434efed0154d46a9cdfc17a941f4b808e9b6a1fa8bdac3bd6b", "entry_hash": "6ac9fe9e63f27bd8a8ee54cd117658301e0a772de9659634dce55c1c7772f96d", "signature": "094bbf67656e8fe9417c4e3e5511937f94d3cabf24e1e12d590391a94ccb79949b0c5db215a5071ae887c5f6f7ec2cdbc3177c107d4322471ba806392e236f02"} -{"seq": 2, "ts": "2026-06-03T19:32:40.408274+00:00", "phase": "decision", "outcome": "deny", "rule": "small-domestic-transfer", "reasons": ["amount: 50000 exceeds max 5000 INR"], "intent_hash": "1d85d0c70086992901292e1972a25a2ad517d110e077fdd99605556aa2f4e12a", "action_fingerprint": "08498e6012a839a030406a89e64b7d22ae357889ff4a4a07fc26210b98921e8d", "action_summary": "POST api.examplebank.in/transfer {\"amount\":50000,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\"}", "approval_id": null, "prev_hash": "6ac9fe9e63f27bd8a8ee54cd117658301e0a772de9659634dce55c1c7772f96d", "entry_hash": "6de26d0d1882a68331a8a92ae8df2efa2c7980a6b95925f487f3c1f87700f30c", "signature": "d4b04bbb06575bbcbcfe22a6525ab3cce6ff4a614e07abdc2c5a66da2a81305379f79c99ccc127a3e22cabdd6da699a00512aff29bd74eecbdf43661cb54f806"} -{"seq": 3, "ts": "2026-06-03T19:32:40.408880+00:00", "phase": "decision", "outcome": "needs_approval", "rule": "small-domestic-transfer", "reasons": ["amount: 2400 within max 5000", "allow_list: beneficiary_type='domestic' permitted"], "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", "action_summary": "POST api.examplebank.in/transfer {\"amount\":2400,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\"}", "approval_id": "apr_d136a402c69d", "prev_hash": "6de26d0d1882a68331a8a92ae8df2efa2c7980a6b95925f487f3c1f87700f30c", "entry_hash": "3c017c620ee3e0bf8295a14d98caa2b6cbd111262e63bd96ca46ba58e5d4a52e", "signature": "87c9693fe31fbcb78c614796e2acf345fdb59e60d913a88b7f1f5e9f9bda8a822b365f32aadf691a4f538e53b75235f2215d9bb8571f7316842fe8ba35c3e104"} -{"seq": 4, "ts": "2026-06-03T19:32:40.409440+00:00", "phase": "execution", "outcome": "deny", "rule": null, "reasons": ["approval/action mismatch: this approval was issued for a different action (possible confused-deputy / substituted action)"], "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", "action_fingerprint": "f8e933c3ea285c35d9b99839a59d217e6b1f5e433f198c3438555db3210b3499", "action_summary": "POST api.examplebank.in/transfer {\"amount\":2400,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\",\"to\":\"attacker\"}", "approval_id": "apr_d136a402c69d", "prev_hash": "3c017c620ee3e0bf8295a14d98caa2b6cbd111262e63bd96ca46ba58e5d4a52e", "entry_hash": "a62ff6e830fb88912e7bf06aaf7bb30685e19671730219f46c04b6d8178bdfdd", "signature": "f05a6a2165bb678962375156a1cc737e1ddcb839ad84bc378ecc5c4a8719105a2a06e2ae4b96621462a12807b2b6e9ae844edcf5a971c15d2da54359f29a5e0b"} -{"seq": 5, "ts": "2026-06-03T19:32:40.410272+00:00", "phase": "execution", "outcome": "allow", "rule": null, "reasons": ["human approved by 'koishore'", "executed via broker"], "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", "action_summary": "POST api.examplebank.in/transfer {\"amount\":2400,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\"}", "approval_id": "apr_d136a402c69d", "prev_hash": "a62ff6e830fb88912e7bf06aaf7bb30685e19671730219f46c04b6d8178bdfdd", "entry_hash": "c6c58e2d1832bef095a33823ba080e9e9a294b3635705bf824211319112ce68f", "signature": "0bd684e8259a12d24752ef9f1f324aabe10cc2ba6d5e628be023686db18fb6484a3f5d1348f153bd4ce185eb1ded88d9d6297d3e4dbf089147bbb68f76f05003"} +{"seq": 0, "ts": "2026-06-03T20:21:56.023090+00:00", "phase": "execution", "outcome": "allow", "rule": "read-accounts", "reasons": ["rate_limit: 1/50 per hour", "executed via broker"], "intent_hash": "ec949034e985a92f3bcd9f9ab8313a80005157698f748f2b8df6163c04af4619", "action_fingerprint": "497e02606ea157ca8ca885cbbd33d1a8a70c40fdaf6f15c5154d858f870b8b61", "action_summary": "GET api.example.com/accounts/me", "approval_id": null, "prev_hash": "GENESIS", "entry_hash": "3e541bb3eab15ec751285bfa1fd1ebd768dd3e69fb61af9e6f9c9b8609c764b1", "signature": "3c26e9d49ac5469983cbd5db1168f130dae8e328015dce368af0613dcd123ba0de0f53041bc025cc42b1ce02b0565681fa9182bd667057417a9362f6e1dd1e06"} +{"seq": 1, "ts": "2026-06-03T20:21:56.023622+00:00", "phase": "decision", "outcome": "deny", "rule": "no-access-control-changes", "reasons": ["forbidden: Agents may never modify permissions or sharing"], "intent_hash": "35f3a4ea90a20026e1515ce4d575628971fcf79c231fd8a4dbc8ec3be21e9823", "action_fingerprint": "a7336f1b76b045ce2b952e04d3a26ef1698d97b63f46449f252c28dcfe21477b", "action_summary": "POST api.example.com/accounts/me/permissions {\"grant\":\"teammate@example.com\"}", "approval_id": null, "prev_hash": "3e541bb3eab15ec751285bfa1fd1ebd768dd3e69fb61af9e6f9c9b8609c764b1", "entry_hash": "4ed0386fffcecff3e91a9d5c960fdac76a789f62e0a9971bdd18c79606677b49", "signature": "e1f37f86203e75c4d3111a9d1e1aac5e7b42316d222b11edcf4e947fb0b8628b9600869415194dd4f02c474a48785af7b8920f13cb01b0381cd4a2634e9c6f04"} +{"seq": 2, "ts": "2026-06-03T20:21:56.024050+00:00", "phase": "decision", "outcome": "deny", "rule": "place-order", "reasons": ["amount: 50000 exceeds max 5000 USD"], "intent_hash": "32e749345831d5d51ff7ac99e98d97e9a8ed3f175f0898466b2f9e9d68031f1f", "action_fingerprint": "d18d95bdaf33086b598dc20e44f01e9b92f196e8e37e4c638c09ef5a854d230d", "action_summary": "POST api.example.com/orders {\"amount\":50000,\"currency\":\"USD\",\"destination\":\"internal\"}", "approval_id": null, "prev_hash": "4ed0386fffcecff3e91a9d5c960fdac76a789f62e0a9971bdd18c79606677b49", "entry_hash": "06904b8cac30280b555ef3a36db7a94bbdb3f505fccf0db9336cffd1452de542", "signature": "c0046e469f7ade9e15f158fdd67af8693b86643f7833848b1bdaba9e436c2047f8017266eca6297ebe10557cfb075f5ff30050edab18bae9a80e0144602eeb04"} +{"seq": 3, "ts": "2026-06-03T20:21:56.024659+00:00", "phase": "decision", "outcome": "needs_approval", "rule": "place-order", "reasons": ["amount: 2400 within max 5000", "allow_list: destination='internal' permitted"], "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", "action_summary": "POST api.example.com/orders {\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\"}", "approval_id": "apr_70137ae08979", "prev_hash": "06904b8cac30280b555ef3a36db7a94bbdb3f505fccf0db9336cffd1452de542", "entry_hash": "9eb2eb608bea5b9e10f9d3f4245ba761640e46ca0db25009681a04eb4d631d70", "signature": "82302948f9d527015797519abb9bc8c0b3e3d2d0ad0a9a5edf12bf8b035ab8eb4e3109580713c5a4dc01e0de887e9b749b461fe0aa1e36b0e9389bfb5581d60b"} +{"seq": 4, "ts": "2026-06-03T20:21:56.025209+00:00", "phase": "execution", "outcome": "deny", "rule": null, "reasons": ["approval/action mismatch: this approval was issued for a different action (possible confused-deputy / substituted action)"], "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "action_fingerprint": "dabddc8fc7e8fb30bdec6fb796a336b7897d4a2a12ae386727e2110d7e0e9572", "action_summary": "POST api.example.com/orders {\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\",\"recipient\":\"attacker\"}", "approval_id": "apr_70137ae08979", "prev_hash": "9eb2eb608bea5b9e10f9d3f4245ba761640e46ca0db25009681a04eb4d631d70", "entry_hash": "7f5076f32d550517270828e2c1e03ea8cd1fbb46d6f366226245e161c579eb68", "signature": "d8468ce1b2d320f01c79854654142f758b034653fad5ddcbc0fbafb5a3a45287967e3c8a052b027e462ddb389cfd134a552329e8da43c030fd4dee4276320f06"} +{"seq": 5, "ts": "2026-06-03T20:21:56.026262+00:00", "phase": "execution", "outcome": "allow", "rule": "place-order", "reasons": ["human approved by 'koishore'", "executed via broker"], "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", "action_summary": "POST api.example.com/orders {\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\"}", "approval_id": "apr_70137ae08979", "prev_hash": "7f5076f32d550517270828e2c1e03ea8cd1fbb46d6f366226245e161c579eb68", "entry_hash": "d9db4d30169d2c3cf4ddf270b051ea2885eba00be1f698e8cd38224f343f986b", "signature": "f610be4bf7da3ab3590a59af523628efd8f6f308d0ac49d5ffe7d5f7dbc0e1caf6c325cce1bedbec7875bc9949eb14901663230cf82c97758462b49030094d0d"} diff --git a/ctk/vectors/chain.tampered.jsonl b/ctk/vectors/chain.tampered.jsonl index 31b3e78..6df9ce9 100644 --- a/ctk/vectors/chain.tampered.jsonl +++ b/ctk/vectors/chain.tampered.jsonl @@ -1,6 +1,6 @@ -{"seq": 0, "ts": "2026-06-03T19:32:40.406187+00:00", "phase": "execution", "outcome": "allow", "rule": "read-accounts", "reasons": ["rate_limit: 1/50 per hour", "executed via broker"], "intent_hash": "cb091de0cf51909850329b23d6552563700889909c2d0db240dc9614720fc224", "action_fingerprint": "8dbf1c7409bc66eda1be5679163ebc455e15078226d655d8b7e406c49688e2dd", "action_summary": "GET api.examplebank.in/accounts/victim", "approval_id": null, "prev_hash": "GENESIS", "entry_hash": "f262318db692c5434efed0154d46a9cdfc17a941f4b808e9b6a1fa8bdac3bd6b", "signature": "7d9fbf6c7ca3a173412f2b4bc1da621527fc28402184e2da950914be38e6b65c4d20fe3367d9ba03f327c076e35b8fe65ef9496a0c5cca1a7e3834bfa406b90f"} -{"seq": 1, "ts": "2026-06-03T19:32:40.407798+00:00", "phase": "decision", "outcome": "deny", "rule": "no-access-control-changes", "reasons": ["forbidden: Agents may never modify permissions or sharing"], "intent_hash": "f9d78fae596e4299cf995cbdfb346330d0a0647b650aaeeb971bfcf8a33830e3", "action_fingerprint": "b7b09bb5b2034e06ac7c42d6ec1f2622686b9046d1f358a7172d166c62e93d63", "action_summary": "POST api.examplebank.in/accounts/me/permissions {\"grant\":\"x@y.com\"}", "approval_id": null, "prev_hash": "f262318db692c5434efed0154d46a9cdfc17a941f4b808e9b6a1fa8bdac3bd6b", "entry_hash": "6ac9fe9e63f27bd8a8ee54cd117658301e0a772de9659634dce55c1c7772f96d", "signature": "094bbf67656e8fe9417c4e3e5511937f94d3cabf24e1e12d590391a94ccb79949b0c5db215a5071ae887c5f6f7ec2cdbc3177c107d4322471ba806392e236f02"} -{"seq": 2, "ts": "2026-06-03T19:32:40.408274+00:00", "phase": "decision", "outcome": "deny", "rule": "small-domestic-transfer", "reasons": ["amount: 50000 exceeds max 5000 INR"], "intent_hash": "1d85d0c70086992901292e1972a25a2ad517d110e077fdd99605556aa2f4e12a", "action_fingerprint": "08498e6012a839a030406a89e64b7d22ae357889ff4a4a07fc26210b98921e8d", "action_summary": "POST api.examplebank.in/transfer {\"amount\":50000,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\"}", "approval_id": null, "prev_hash": "6ac9fe9e63f27bd8a8ee54cd117658301e0a772de9659634dce55c1c7772f96d", "entry_hash": "6de26d0d1882a68331a8a92ae8df2efa2c7980a6b95925f487f3c1f87700f30c", "signature": "d4b04bbb06575bbcbcfe22a6525ab3cce6ff4a614e07abdc2c5a66da2a81305379f79c99ccc127a3e22cabdd6da699a00512aff29bd74eecbdf43661cb54f806"} -{"seq": 3, "ts": "2026-06-03T19:32:40.408880+00:00", "phase": "decision", "outcome": "needs_approval", "rule": "small-domestic-transfer", "reasons": ["amount: 2400 within max 5000", "allow_list: beneficiary_type='domestic' permitted"], "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", "action_summary": "POST api.examplebank.in/transfer {\"amount\":2400,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\"}", "approval_id": "apr_d136a402c69d", "prev_hash": "6de26d0d1882a68331a8a92ae8df2efa2c7980a6b95925f487f3c1f87700f30c", "entry_hash": "3c017c620ee3e0bf8295a14d98caa2b6cbd111262e63bd96ca46ba58e5d4a52e", "signature": "87c9693fe31fbcb78c614796e2acf345fdb59e60d913a88b7f1f5e9f9bda8a822b365f32aadf691a4f538e53b75235f2215d9bb8571f7316842fe8ba35c3e104"} -{"seq": 4, "ts": "2026-06-03T19:32:40.409440+00:00", "phase": "execution", "outcome": "deny", "rule": null, "reasons": ["approval/action mismatch: this approval was issued for a different action (possible confused-deputy / substituted action)"], "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", "action_fingerprint": "f8e933c3ea285c35d9b99839a59d217e6b1f5e433f198c3438555db3210b3499", "action_summary": "POST api.examplebank.in/transfer {\"amount\":2400,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\",\"to\":\"attacker\"}", "approval_id": "apr_d136a402c69d", "prev_hash": "3c017c620ee3e0bf8295a14d98caa2b6cbd111262e63bd96ca46ba58e5d4a52e", "entry_hash": "a62ff6e830fb88912e7bf06aaf7bb30685e19671730219f46c04b6d8178bdfdd", "signature": "f05a6a2165bb678962375156a1cc737e1ddcb839ad84bc378ecc5c4a8719105a2a06e2ae4b96621462a12807b2b6e9ae844edcf5a971c15d2da54359f29a5e0b"} -{"seq": 5, "ts": "2026-06-03T19:32:40.410272+00:00", "phase": "execution", "outcome": "allow", "rule": null, "reasons": ["human approved by 'koishore'", "executed via broker"], "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", "action_summary": "POST api.examplebank.in/transfer {\"amount\":2400,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\"}", "approval_id": "apr_d136a402c69d", "prev_hash": "a62ff6e830fb88912e7bf06aaf7bb30685e19671730219f46c04b6d8178bdfdd", "entry_hash": "c6c58e2d1832bef095a33823ba080e9e9a294b3635705bf824211319112ce68f", "signature": "0bd684e8259a12d24752ef9f1f324aabe10cc2ba6d5e628be023686db18fb6484a3f5d1348f153bd4ce185eb1ded88d9d6297d3e4dbf089147bbb68f76f05003"} +{"seq": 0, "ts": "2026-06-03T20:21:56.023090+00:00", "phase": "execution", "outcome": "allow", "rule": "read-accounts", "reasons": ["rate_limit: 1/50 per hour", "executed via broker"], "intent_hash": "ec949034e985a92f3bcd9f9ab8313a80005157698f748f2b8df6163c04af4619", "action_fingerprint": "497e02606ea157ca8ca885cbbd33d1a8a70c40fdaf6f15c5154d858f870b8b61", "action_summary": "GET api.example.com/accounts/victim", "approval_id": null, "prev_hash": "GENESIS", "entry_hash": "3e541bb3eab15ec751285bfa1fd1ebd768dd3e69fb61af9e6f9c9b8609c764b1", "signature": "3c26e9d49ac5469983cbd5db1168f130dae8e328015dce368af0613dcd123ba0de0f53041bc025cc42b1ce02b0565681fa9182bd667057417a9362f6e1dd1e06"} +{"seq": 1, "ts": "2026-06-03T20:21:56.023622+00:00", "phase": "decision", "outcome": "deny", "rule": "no-access-control-changes", "reasons": ["forbidden: Agents may never modify permissions or sharing"], "intent_hash": "35f3a4ea90a20026e1515ce4d575628971fcf79c231fd8a4dbc8ec3be21e9823", "action_fingerprint": "a7336f1b76b045ce2b952e04d3a26ef1698d97b63f46449f252c28dcfe21477b", "action_summary": "POST api.example.com/accounts/me/permissions {\"grant\":\"teammate@example.com\"}", "approval_id": null, "prev_hash": "3e541bb3eab15ec751285bfa1fd1ebd768dd3e69fb61af9e6f9c9b8609c764b1", "entry_hash": "4ed0386fffcecff3e91a9d5c960fdac76a789f62e0a9971bdd18c79606677b49", "signature": "e1f37f86203e75c4d3111a9d1e1aac5e7b42316d222b11edcf4e947fb0b8628b9600869415194dd4f02c474a48785af7b8920f13cb01b0381cd4a2634e9c6f04"} +{"seq": 2, "ts": "2026-06-03T20:21:56.024050+00:00", "phase": "decision", "outcome": "deny", "rule": "place-order", "reasons": ["amount: 50000 exceeds max 5000 USD"], "intent_hash": "32e749345831d5d51ff7ac99e98d97e9a8ed3f175f0898466b2f9e9d68031f1f", "action_fingerprint": "d18d95bdaf33086b598dc20e44f01e9b92f196e8e37e4c638c09ef5a854d230d", "action_summary": "POST api.example.com/orders {\"amount\":50000,\"currency\":\"USD\",\"destination\":\"internal\"}", "approval_id": null, "prev_hash": "4ed0386fffcecff3e91a9d5c960fdac76a789f62e0a9971bdd18c79606677b49", "entry_hash": "06904b8cac30280b555ef3a36db7a94bbdb3f505fccf0db9336cffd1452de542", "signature": "c0046e469f7ade9e15f158fdd67af8693b86643f7833848b1bdaba9e436c2047f8017266eca6297ebe10557cfb075f5ff30050edab18bae9a80e0144602eeb04"} +{"seq": 3, "ts": "2026-06-03T20:21:56.024659+00:00", "phase": "decision", "outcome": "needs_approval", "rule": "place-order", "reasons": ["amount: 2400 within max 5000", "allow_list: destination='internal' permitted"], "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", "action_summary": "POST api.example.com/orders {\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\"}", "approval_id": "apr_70137ae08979", "prev_hash": "06904b8cac30280b555ef3a36db7a94bbdb3f505fccf0db9336cffd1452de542", "entry_hash": "9eb2eb608bea5b9e10f9d3f4245ba761640e46ca0db25009681a04eb4d631d70", "signature": "82302948f9d527015797519abb9bc8c0b3e3d2d0ad0a9a5edf12bf8b035ab8eb4e3109580713c5a4dc01e0de887e9b749b461fe0aa1e36b0e9389bfb5581d60b"} +{"seq": 4, "ts": "2026-06-03T20:21:56.025209+00:00", "phase": "execution", "outcome": "deny", "rule": null, "reasons": ["approval/action mismatch: this approval was issued for a different action (possible confused-deputy / substituted action)"], "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "action_fingerprint": "dabddc8fc7e8fb30bdec6fb796a336b7897d4a2a12ae386727e2110d7e0e9572", "action_summary": "POST api.example.com/orders {\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\",\"recipient\":\"attacker\"}", "approval_id": "apr_70137ae08979", "prev_hash": "9eb2eb608bea5b9e10f9d3f4245ba761640e46ca0db25009681a04eb4d631d70", "entry_hash": "7f5076f32d550517270828e2c1e03ea8cd1fbb46d6f366226245e161c579eb68", "signature": "d8468ce1b2d320f01c79854654142f758b034653fad5ddcbc0fbafb5a3a45287967e3c8a052b027e462ddb389cfd134a552329e8da43c030fd4dee4276320f06"} +{"seq": 5, "ts": "2026-06-03T20:21:56.026262+00:00", "phase": "execution", "outcome": "allow", "rule": "place-order", "reasons": ["human approved by 'koishore'", "executed via broker"], "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", "action_summary": "POST api.example.com/orders {\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\"}", "approval_id": "apr_70137ae08979", "prev_hash": "7f5076f32d550517270828e2c1e03ea8cd1fbb46d6f366226245e161c579eb68", "entry_hash": "d9db4d30169d2c3cf4ddf270b051ea2885eba00be1f698e8cd38224f343f986b", "signature": "f610be4bf7da3ab3590a59af523628efd8f6f308d0ac49d5ffe7d5f7dbc0e1caf6c325cce1bedbec7875bc9949eb14901663230cf82c97758462b49030094d0d"} diff --git a/ctk/vectors/decisions.json b/ctk/vectors/decisions.json index a7e0ed1..9794173 100644 --- a/ctk/vectors/decisions.json +++ b/ctk/vectors/decisions.json @@ -1,9 +1,9 @@ [ { "action": { - "instruction": "check my balance", + "instruction": "read my account details", "method": "GET", - "url": "https://api.examplebank.in/accounts/me", + "url": "https://api.example.com/accounts/me", "params": {} }, "outcome": "allow", @@ -14,11 +14,11 @@ }, { "action": { - "instruction": "share my statements", + "instruction": "share my account with a teammate", "method": "POST", - "url": "https://api.examplebank.in/accounts/me/permissions", + "url": "https://api.example.com/accounts/me/permissions", "params": { - "grant": "x@y.com" + "grant": "teammate@example.com" } }, "outcome": "deny", @@ -29,37 +29,37 @@ }, { "action": { - "instruction": "pay the contractor", + "instruction": "place a large order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 50000, - "currency": "INR", - "beneficiary_type": "domestic" + "currency": "USD", + "destination": "internal" } }, "outcome": "deny", - "rule": "small-domestic-transfer", + "rule": "place-order", "reasons": [ - "amount: 50000 exceeds max 5000 INR" + "amount: 50000 exceeds max 5000 USD" ] }, { "action": { - "instruction": "pay my electricity bill", + "instruction": "place a small order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic" + "currency": "USD", + "destination": "internal" } }, "outcome": "needs_approval", - "rule": "small-domestic-transfer", + "rule": "place-order", "reasons": [ "amount: 2400 within max 5000", - "allow_list: beneficiary_type='domestic' permitted" + "allow_list: destination='internal' permitted" ] } ] diff --git a/ctk/vectors/hashing.json b/ctk/vectors/hashing.json index c349b00..f886955 100644 --- a/ctk/vectors/hashing.json +++ b/ctk/vectors/hashing.json @@ -1,47 +1,47 @@ [ { "action": { - "instruction": "check my balance", + "instruction": "read my account details", "method": "GET", - "url": "https://api.examplebank.in/accounts/me", + "url": "https://api.example.com/accounts/me", "params": {} }, - "intent_canonical_json": "{\"instruction\":\"check my balance\"}", - "intent_hash": "cb091de0cf51909850329b23d6552563700889909c2d0db240dc9614720fc224", - "fingerprint_canonical_json": "{\"host\":\"api.examplebank.in\",\"method\":\"GET\",\"params\":{},\"path\":\"/accounts/me\"}", - "action_fingerprint": "8dbf1c7409bc66eda1be5679163ebc455e15078226d655d8b7e406c49688e2dd" + "intent_canonical_json": "{\"instruction\":\"read my account details\"}", + "intent_hash": "ec949034e985a92f3bcd9f9ab8313a80005157698f748f2b8df6163c04af4619", + "fingerprint_canonical_json": "{\"host\":\"api.example.com\",\"method\":\"GET\",\"params\":{},\"path\":\"/accounts/me\"}", + "action_fingerprint": "497e02606ea157ca8ca885cbbd33d1a8a70c40fdaf6f15c5154d858f870b8b61" }, { "action": { - "instruction": "pay my electricity bill", + "instruction": "place a small order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic" + "currency": "USD", + "destination": "internal" } }, - "intent_canonical_json": "{\"instruction\":\"pay my electricity bill\"}", - "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", - "fingerprint_canonical_json": "{\"host\":\"api.examplebank.in\",\"method\":\"POST\",\"params\":{\"amount\":2400,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\"},\"path\":\"/transfer\"}", - "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e" + "intent_canonical_json": "{\"instruction\":\"place a small order\"}", + "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", + "fingerprint_canonical_json": "{\"host\":\"api.example.com\",\"method\":\"POST\",\"params\":{\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\"},\"path\":\"/orders\"}", + "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394" }, { "action": { - "instruction": "pay my electricity bill", + "instruction": "place a small order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic", - "to": "attacker" + "currency": "USD", + "destination": "internal", + "recipient": "attacker" } }, - "intent_canonical_json": "{\"instruction\":\"pay my electricity bill\"}", - "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", - "fingerprint_canonical_json": "{\"host\":\"api.examplebank.in\",\"method\":\"POST\",\"params\":{\"amount\":2400,\"beneficiary_type\":\"domestic\",\"currency\":\"INR\",\"to\":\"attacker\"},\"path\":\"/transfer\"}", - "action_fingerprint": "f8e933c3ea285c35d9b99839a59d217e6b1f5e433f198c3438555db3210b3499" + "intent_canonical_json": "{\"instruction\":\"place a small order\"}", + "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", + "fingerprint_canonical_json": "{\"host\":\"api.example.com\",\"method\":\"POST\",\"params\":{\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\",\"recipient\":\"attacker\"},\"path\":\"/orders\"}", + "action_fingerprint": "dabddc8fc7e8fb30bdec6fb796a336b7897d4a2a12ae386727e2110d7e0e9572" } ] diff --git a/ctk/vectors/resolve.json b/ctk/vectors/resolve.json index 362d61b..4bdae4d 100644 --- a/ctk/vectors/resolve.json +++ b/ctk/vectors/resolve.json @@ -3,17 +3,17 @@ "name": "unknown approval id is denied", "approval": null, "presented_action": { - "instruction": "pay my electricity bill", + "instruction": "place a small order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic" + "currency": "USD", + "destination": "internal" } }, - "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "presented_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "presented_intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "expected": { "outcome": "deny", "reason_contains": "unknown approval id" @@ -22,24 +22,24 @@ { "name": "fingerprint mismatch is refused (before status)", "approval": { - "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "status": "pending", - "rule": "small-domestic-transfer" + "rule": "place-order" }, "presented_action": { - "instruction": "pay my electricity bill", + "instruction": "place a small order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic", - "to": "attacker" + "currency": "USD", + "destination": "internal", + "recipient": "attacker" } }, - "presented_fingerprint": "f8e933c3ea285c35d9b99839a59d217e6b1f5e433f198c3438555db3210b3499", - "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "presented_fingerprint": "dabddc8fc7e8fb30bdec6fb796a336b7897d4a2a12ae386727e2110d7e0e9572", + "presented_intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "expected": { "outcome": "deny", "reason_contains": "approval/action mismatch: this approval was issued for a different action (possible confused-deputy / substituted action)" @@ -48,23 +48,23 @@ { "name": "intent mismatch is refused", "approval": { - "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "status": "approved", - "rule": "small-domestic-transfer" + "rule": "place-order" }, "presented_action": { - "instruction": "send the deposit to my landlord", + "instruction": "place a different order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic" + "currency": "USD", + "destination": "internal" } }, - "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "presented_intent_hash": "03fa5b7e3b8e6205bf83be28fa423e311676533157af96923d3bf6651087b37d", + "presented_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "presented_intent_hash": "70b7c5af667d7dc104bd9baaf5c26ebdabeebe69272ee013a6306e208a09c4b3", "expected": { "outcome": "deny", "reason_contains": "approval/intent mismatch: this approval was issued for a different instruction" @@ -73,23 +73,23 @@ { "name": "matching action while pending awaits approval", "approval": { - "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "status": "pending", - "rule": "small-domestic-transfer" + "rule": "place-order" }, "presented_action": { - "instruction": "pay my electricity bill", + "instruction": "place a small order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic" + "currency": "USD", + "destination": "internal" } }, - "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "presented_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "presented_intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "expected": { "outcome": "needs_approval", "reason_contains": "awaiting human approval" @@ -98,23 +98,23 @@ { "name": "human-denied action is refused", "approval": { - "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "status": "denied", - "rule": "small-domestic-transfer" + "rule": "place-order" }, "presented_action": { - "instruction": "pay my electricity bill", + "instruction": "place a small order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic" + "currency": "USD", + "destination": "internal" } }, - "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "presented_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "presented_intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "expected": { "outcome": "deny", "reason_contains": "human denied this action" @@ -123,23 +123,23 @@ { "name": "approved action is released once", "approval": { - "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "status": "approved", - "rule": "small-domestic-transfer" + "rule": "place-order" }, "presented_action": { - "instruction": "pay my electricity bill", + "instruction": "place a small order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic" + "currency": "USD", + "destination": "internal" } }, - "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "presented_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "presented_intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "expected": { "outcome": "allow", "reason_contains": "human approved by 'human'" @@ -148,23 +148,23 @@ { "name": "replay of a consumed approval is refused", "approval": { - "action_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "status": "consumed", - "rule": "small-domestic-transfer" + "rule": "place-order" }, "presented_action": { - "instruction": "pay my electricity bill", + "instruction": "place a small order", "method": "POST", - "url": "https://api.examplebank.in/transfer", + "url": "https://api.example.com/orders", "params": { "amount": 2400, - "currency": "INR", - "beneficiary_type": "domestic" + "currency": "USD", + "destination": "internal" } }, - "presented_fingerprint": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "presented_intent_hash": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "presented_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "presented_intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "expected": { "outcome": "deny", "reason_contains": "approval already used: this single-use approval has already released its action" diff --git a/ctk/vectors/signing_key.pub b/ctk/vectors/signing_key.pub index cf1a1af..d84546c 100644 --- a/ctk/vectors/signing_key.pub +++ b/ctk/vectors/signing_key.pub @@ -1,3 +1,3 @@ -----BEGIN PUBLIC KEY----- -MCowBQYDK2VwAyEAzwFCHrIcEoKk/1UENje0lKB/ICBnG357cKPmBnuR8Xw= +MCowBQYDK2VwAyEA2rcFObBUFzdrsYz1UV0nj8EdZPkHQrQZw3o7W3qhXlU= -----END PUBLIC KEY----- diff --git a/examples/authorization-token.json b/examples/authorization-token.json index c254db6..089cfef 100644 --- a/examples/authorization-token.json +++ b/examples/authorization-token.json @@ -4,8 +4,11 @@ "iat": 1759000000, "exp": 1759000045, "jti": "01JBQK9Z6X8N3M2P0R5T7V9W1Y", - "fpr": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "iht": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "fpr": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "iht": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "apr": "apr_4c9183f7606f", - "pol": { "version": 1, "rule": "small-domestic-transfer" } + "pol": { + "version": 1, + "rule": "place-order" + } } diff --git a/examples/authorization-token.md b/examples/authorization-token.md index 49fd003..6b9d902 100644 --- a/examples/authorization-token.md +++ b/examples/authorization-token.md @@ -22,14 +22,14 @@ each segment base64url-encoded) signed with `alg = EdDSA` (Ed25519). "iat": 1759000000, "exp": 1759000045, "jti": "01JBQK9Z6X8N3M2P0R5T7V9W1Y", - "fpr": "79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e", - "iht": "cbcc6dab3a0bceb9acacf333d48ddb24d68d09a02f676d8f15dfd36e4c2b3205", + "fpr": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "iht": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", "apr": "apr_4c9183f7606f", - "pol": { "version": 1, "rule": "small-domestic-transfer" } + "pol": { "version": 1, "rule": "place-order" } } ``` -`fpr` and `iht` are the small-domestic-transfer's `action_fingerprint` and +`fpr` and `iht` are the place-order's `action_fingerprint` and `intent_hash` from [`../ctk/vectors/hashing.json`](../ctk/vectors/hashing.json). **Compact form** (illustrative): diff --git a/examples/policy.example.yaml b/examples/policy.example.yaml index fc092ba..edfd7a5 100644 --- a/examples/policy.example.yaml +++ b/examples/policy.example.yaml @@ -1,11 +1,11 @@ -# Example policy — a BFSI-flavoured starting point. +# Example policy — a generic starting point for an HTTP/JSON API. # # Evaluation order: forbidden (hard deny) -> rules (first match wins) -> default. # A matched rule whose constraints fail becomes a DENY (fail-closed). # # `match` supports: method (str or list), host, path (glob), path_contains. # `params` referenced by constraints are the fields the agent declares on the -# action (e.g. amount, currency, beneficiary_type). +# action (e.g. amount, currency, destination). version: 1 default: deny # anything not explicitly allowed is refused @@ -16,11 +16,11 @@ forbidden: match: path_contains: /permissions - - name: no-withdrawals - description: Agents may never withdraw funds + - name: no-deletes + description: Agents may never delete resources match: method: POST - path: /withdraw + path: /delete rules: - name: read-accounts @@ -28,28 +28,28 @@ rules: decision: allow match: method: GET - host: api.examplebank.in + host: api.example.com path: /accounts/** constraints: rate_limit: { max: 50, per: hour } - - name: read-transactions - description: Read own transactions (read-only, rate-limited) + - name: read-orders + description: Read own orders (read-only, rate-limited) decision: allow match: method: GET - host: api.examplebank.in - path: /transactions/** + host: api.example.com + path: /orders/** constraints: rate_limit: { max: 50, per: hour } - - name: small-domestic-transfer - description: Small domestic INR transfer — always needs human approval + - name: place-order + description: Place a small order — always needs human approval decision: needs_approval match: method: POST - host: api.examplebank.in - path: /transfer + host: api.example.com + path: /orders constraints: - amount: { field: amount, max: 5000, currency: INR } - allow_list: { field: beneficiary_type, in: [domestic] } + amount: { field: amount, max: 5000, currency: USD } + allow_list: { field: destination, in: [internal] } diff --git a/spec.md b/spec.md index d6b8949..351bd5d 100644 --- a/spec.md +++ b/spec.md @@ -108,25 +108,25 @@ action_fingerprint = sha256(canonical_json({ ### 4.1 Worked example (authoritative) -For `GET https://api.examplebank.in/accounts/me`, instruction `"check my balance"`, +For `GET https://api.example.com/accounts/me`, instruction `"read my account details"`, `params = {}`: ``` canonical_json for fingerprint: -{"host":"api.examplebank.in","method":"GET","params":{},"path":"/accounts/me"} +{"host":"api.example.com","method":"GET","params":{},"path":"/accounts/me"} -intent_hash = cb091de0cf51909850329b23d6552563700889909c2d0db240dc9614720fc224 -action_fingerprint = 8dbf1c7409bc66eda1be5679163ebc455e15078226d655d8b7e406c49688e2dd +intent_hash = ec949034e985a92f3bcd9f9ab8313a80005157698f748f2b8df6163c04af4619 +action_fingerprint = 497e02606ea157ca8ca885cbbd33d1a8a70c40fdaf6f15c5154d858f870b8b61 ``` Adding one parameter changes the fingerprint completely — this is what makes the confused-deputy guard (§7) work: ``` -transfer {amount:2400,currency:INR,beneficiary_type:domestic} - → 79311ec97086b7c5107825dd198beb3c87339802d631f178ef5384d3f7c57a9e -transfer {…, to:"attacker"} (one param added) - → f8e933c3ea285c35d9b99839a59d217e6b1f5e433f198c3438555db3210b3499 +order {amount:2400,currency:USD,destination:internal} + → c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394 +order {…, recipient:"attacker"} (one param added) + → dabddc8fc7e8fb30bdec6fb796a336b7897d4a2a12ae386727e2110d7e0e9572 ``` See [`ctk/vectors/hashing.json`](ctk/vectors/hashing.json) for the full set. @@ -136,7 +136,7 @@ See [`ctk/vectors/hashing.json`](ctk/vectors/hashing.json) for the full set. Through 0.2 the fingerprint covers `method`, `host`, `path`, and the agent-declared `params`; the URL's **query string is not part of the fingerprint**. Two requests that differ only in their query (e.g. -`/transfer?to=me` vs `/transfer?to=attacker`) therefore share one fingerprint — +`/orders?to=me` vs `/orders?to=attacker`) therefore share one fingerprint — a confused-deputy gap if decision-relevant data rides the query. Until 0.3, an Authorizer and Broker **MUST** ensure no decision-relevant value is @@ -173,12 +173,12 @@ forbidden: # hard denials, evaluated first - name: no-access-control-changes match: { path_contains: /permissions } rules: # first match wins - - name: small-domestic-transfer + - name: place-order decision: needs_approval - match: { method: POST, host: api.examplebank.in, path: /transfer } + match: { method: POST, host: api.example.com, path: /orders } constraints: - amount: { field: amount, max: 5000, currency: INR } - allow_list: { field: beneficiary_type, in: [domestic] } + amount: { field: amount, max: 5000, currency: USD } + allow_list: { field: destination, in: [internal] } ``` A **rule** has a `name`, a `decision` (`allow` or `needs_approval`), a `match`,