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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!-- Fork the repo and open this PR from a branch in your fork. See CONTRIBUTING.md. -->

## What & why

<!-- What does this change to the protocol do, and why? Link any issue. -->

## AI assistance disclosure (required)

<!-- AI-assisted PRs are welcome but get stricter review. Be specific. -->

- [ ] 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`.
14 changes: 14 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
41 changes: 36 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
152 changes: 152 additions & 0 deletions conformance.py
Original file line number Diff line number Diff line change
@@ -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.")
1 change: 1 addition & 0 deletions ctk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading