# Delegus Specification v0.2 — FINAL (frozen)

**Status:** Frozen. Errata applied — 1: revocation-read / replay-write ordering (§5.3, §5.4, §7) and acceptance test (§15); 2: historical verification clock (§5.4, §6.4). This is the sole normative implementation source for v0.2. It consolidates the v0.2 draft, the Freeze Resolutions, the Threat Model, and the Final Freeze Recommendations. Earlier drafts are decision history only; engineers and conformance tests MUST NOT depend on them. Changes after this point require v0.3 or an explicit numbered erratum.

**Acceptance criterion:** An engineer at Company B who has never seen Company A's agent can integrate Delegus in an afternoon and safely accept or reject its action with one API call.

**Normative language:** MUST / SHOULD / MAY per RFC 2119.

**Licence (non-normative):** This specification text is licensed under the Creative Commons Attribution 4.0 International licence (CC BY 4.0, https://creativecommons.org/licenses/by/4.0/). Attribution: "Delegus Specification v0.2, © Delegus (delegus.ai)". The reference engine (`@delegus/core`, including the conformance vectors) and the SDK (`@delegus/sdk`) are licensed separately under Apache-2.0. This statement is metadata: it changes no normative text, no profile document and no trust-configuration descriptor.

---

## 0. Constitution

**OPEN (published, versioned, reproducible by anyone):** Grant semantics · Proof format · protocol verification rules (§5.2) · Receipt format · Outcome Attestation format · portable identity model.

**DELEGUS (the service):** issuer trust · live verification (replay, revocation freshness) · evidence preservation · decision signing · adapter network · outcome graph · risk models · economic assurance.

Design constraints:
1. An ordinary backend engineer can implement the protocol layer from this document with a standard JOSE library.
2. Given the same inputs and the same evidence snapshot, the protocol result is identical everywhere. The Delegus decision is deterministic given inputs + snapshot + trust configuration.
3. Fail closed. Anything unrecognized, missing, ambiguous, or unavailable is DENY.
4. No per-transaction state except replay protection and the receipt/outcome ledger.
5. Someone must be able to verify a Delegus artifact without calling Delegus.

---

## 1. Terminology

| Term | Meaning |
|---|---|
| **Principal** | The organization on whose behalf an agent acts. Identified by a DID. Verified by domain control in v0. |
| **Agent** | Software holding an Ed25519 private key. Identified by `did:key`. |
| **Relying Party (RP)** | The party receiving an action and calling `/verify`. Registered; identified by a DID. |
| **Grant** | A Principal's signed statement of what an Agent may do, for whom, until when. |
| **Proof** | A short-lived Agent signature binding its key to one request, one RP, one Grant. |
| **Action** | The RP's description of what the Agent attempts, in the constraint vocabulary. |
| **Decision** | ALLOW or DENY with a machine-readable reason. |
| **Receipt** | The Decision plus its evidence snapshot, signed by Delegus. |
| **Outcome Attestation** | An append-only event reported after a decision. |

---

## 2. Canonical domain and identifiers

### 2.0 Delegus domain as cryptographic infrastructure

The canonical domain is **`delegus.ai`**. No persistent production artifact (Grant, Receipt, status-list credential, managed identity) may be issued until Delegus controls this domain. Frozen identifiers:

```
did:web:delegus.ai                      Delegus root DID (receipt and status-list signing keys)
did:web:delegus.ai:org:<slug>           managed Principal identities
did:web:delegus.ai:rp:<slug>            managed Relying Party identities
https://delegus.ai/ns/grant/v1          Grant JSON-LD context
https://status.delegus.ai/...           published status-list credentials
did:web:delegus.ai#receipt-<n>          receipt signing key identifiers
```

- Development and test environments MUST use explicitly non-production identifiers (e.g. `did:web:dev.delegus.ai`) that are visually and cryptographically distinct from production. Test signing keys MUST never appear in the production DID document.
- Production credentials MUST NOT be issued against a temporary domain with intent to migrate. Changing the canonical domain after launch is a protocol migration, not a branding change.
- `https://delegus.ai/.well-known/did.json` is production infrastructure with the same availability and TLS discipline as `/verify`. Receipt keys are rotated with overlapping validity and are never removed, only retired.

### 2.1 Principal — managed by default, open by design
- DID method in v0: `did:web`. Two sources, equal in the protocol:
  - **Managed:** `did:web:delegus.ai:org:<slug>`, created by `delegus.org.create()`.
  - **Customer-hosted:** `did:web:acme.com`, accepted at onboarding.
- DID documents are resolved at onboarding and refreshed asynchronously at most every 5 minutes. The engine evaluates against the cached snapshot; the receipt records that snapshot's hash.
- Migration from managed to customer-hosted is a new DID; existing Grants remain valid to `validUntil`.
- Key type Ed25519, verification method `Multikey`, max 5 keys. Key states `active | retired | compromised` are Delegus trust data, not DID-document data.
- **Domain verification:** DNS TXT at `_delegus.<domain>` or `https://<domain>/.well-known/delegus.json` containing a Delegus-issued token. Re-verified every 24 h. Failure sets the Principal `unverified`.
- Reserved for adapters (not v0): Entra, Google, SPIFFE, enterprise PKI, other VC issuers. Adapters MUST materialize their verification state before it reaches the hot path.

### 2.2 Agent
- `did:key`, Ed25519 (`did:key:z6Mk...`). The key is the identifier; no resolution.
- Generated locally by the SDK. `POST /agents` registers the public key for inventory and the agent-level kill switch (`POST /agents/{did}/disable` revokes every Grant naming that Agent). Registration is **not** consulted by `/verify`; the Grant's signature is the binding.
- SDK signer interface MUST be pluggable: in-memory (dev default), KMS, HSM/TPM.

### 2.3 Relying Party
- Registered at onboarding; `did:web:delegus.ai:rp:<slug>` or customer-hosted `did:web`.
- Authenticates to `/verify` with an API key that resolves to the RP DID — the audience for Proofs and the reporter identity for Outcome Attestations.

---

## 3. The Grant

### 3.0 Semantic model and serialization profiles

The Grant is defined by the fields and semantics in §3.2–3.3, independent of encoding. A serialization profile maps those fields to bytes and MUST NOT add or remove semantics.

| Profile | Status |
|---|---|
| **VC 2.0 + compact JWS** (VC-JOSE-COSE) | v0 — the only profile the engine accepts |
| VC 2.0 + SD-JWT | reserved |
| EUDI `dc+sd-jwt` | reserved (eIDAS "powers and mandates to represent legal persons") |
| COSE | reserved |

### 3.1 JOSE header (v0 profile)

| # | Field | Value | Rule |
|---|---|---|---|
| 1 | `alg` | `EdDSA` | MUST. No other algorithm accepted. |
| 2 | `typ` | `vc+jwt` | MUST. |
| 3 | `kid` | Principal key DID URL, e.g. `did:web:acme.com#key-1` | MUST. |

### 3.2 Fields

| # | Field | Type | Rule |
|---|---|---|---|
| 4 | `@context` | array | MUST be exactly `["https://www.w3.org/ns/credentials/v2", "https://delegus.ai/ns/grant/v1"]`. |
| 5 | `type` | array | MUST be exactly `["VerifiableCredential", "DelegusGrant"]`. |
| 6 | `id` | string | `urn:delegus:grant:<ULID>`. |
| 7 | `issuer` | string | Principal DID. MUST equal the DID portion of `kid`. |
| 8 | `validFrom` | RFC 3339 UTC | MUST. |
| 9 | `validUntil` | RFC 3339 UTC | MUST. `validUntil − validFrom ≤ 90 days`. |
| 10 | `credentialSubject.id` | string | Agent DID. |
| 11 | `credentialSubject.principal` | string | Root Principal DID. v0: MUST equal `issuer`. Reserved for chains. |
| 12 | `credentialSubject.authority` | array of Capability | 1–16 entries. |
| 13 | `credentialSubject.audience` | array of RP DIDs | OPTIONAL. Absent = any registered RP. |
| 14 | `credentialStatus` | BitstringStatusListEntry | MUST. `statusPurpose: "revocation"`, `statusListIndex`, `statusListCredential`. |
| 15 | `parentGrant` | string | RESERVED. MUST be absent in v0 → DENY `UNSUPPORTED_FEATURE`. Future: child authority ⊆ parent authority. |
| 16–20 | `iss`, `sub`, `jti`, `nbf`, `exp` | registered claims | MUST equal `issuer`, `credentialSubject.id`, `id`, `validFrom`, `validUntil`. |

Encoded size ≤ 8 KB.

### 3.3 Capability and constraint vocabulary (v0)

```json
{ "action": "commerce:purchase", "constraints": { "maxAmount": 50000, "currency": "USD" }, "resources": ["*"] }
```

- `action` — `<namespace>:<verb>`. v0 defines exactly two:
  - `commerce:purchase` — `maxAmount` (integer, minor units), `currency` (ISO 4217, single), OPTIONAL `merchantCategory` (array of strings).
  - `api:call` — `methods` (array of uppercase HTTP methods), `resources` (URI globs; `*` and `**` only).
- `constraints` — keys MUST all be defined for that action type in this spec version. **Unknown key → DENY `UNKNOWN_CONSTRAINT`.**
- `resources` — OPTIONAL glob array matched against `action.resource`. Absent = `["*"]`.
- Authorization: at least one Capability matches; a Capability matches iff `action` is equal and every constraint is satisfied. Per-action limits only; no cumulative limits. All amounts are integers in minor units; no floats anywhere in the protocol.

### 3.4 Example payload

```json
{
  "@context": ["https://www.w3.org/ns/credentials/v2", "https://delegus.ai/ns/grant/v1"],
  "type": ["VerifiableCredential", "DelegusGrant"],
  "id": "urn:delegus:grant:01J9X4QK7M3Y2Z8N6P5R4T3V2W",
  "issuer": "did:web:acme.com",
  "validFrom": "2026-09-09T00:00:00Z",
  "validUntil": "2026-10-01T00:00:00Z",
  "credentialSubject": {
    "id": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
    "principal": "did:web:acme.com",
    "authority": [
      { "action": "commerce:purchase", "constraints": { "maxAmount": 50000, "currency": "USD" } }
    ]
  },
  "credentialStatus": {
    "id": "https://status.delegus.ai/acme.com/1#4127",
    "type": "BitstringStatusListEntry",
    "statusPurpose": "revocation",
    "statusListIndex": "4127",
    "statusListCredential": "https://status.delegus.ai/acme.com/1"
  },
  "iss": "did:web:acme.com",
  "sub": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
  "jti": "urn:delegus:grant:01J9X4QK7M3Y2Z8N6P5R4T3V2W",
  "nbf": 1788912000,
  "exp": 1790812800
}
```

---

## 4. The Proof

A compact JWS signed by the Agent key, modeled on DPoP (RFC 9449), binding the key to one request, one RP, one Grant.

### 4.1 Header

| Field | Value |
|---|---|
| `alg` | `EdDSA` (MUST) |
| `typ` | `delegus-proof+jwt` (MUST) |
| `kid` | Agent DID URL, e.g. `did:key:z6Mk...#z6Mk...` (MUST) |

### 4.2 Claims

| Field | Rule |
|---|---|
| `iss` | Agent DID. MUST equal Grant `sub`. |
| `aud` | RP DID. MUST equal the DID of the RP calling `/verify`. |
| `grant` | MUST equal Grant `id`. |
| `jti` | Security nonce. MUST contain ≥ 128 cryptographically random bits from a CSPRNG. SDKs SHOULD generate 16 random bytes encoded as base64url without padding. MUST be 22–64 ASCII characters. Not a sortable identifier. |
| `iat` | Seconds since epoch. MUST satisfy `|now − iat| ≤ window` (default 60 s, RP-configurable, max 300 s). |
| `htm` | HTTP method, uppercase ASCII. |
| `htu` | Target URI per §4.3. |
| `action_hash` | `sha256:<64 lowercase hex>` of the UTF-8 bytes of `JCS(action)` (RFC 8785). |

### 4.3 Canonicalization

- `htu`: per RFC 9449 §4.3 — lowercase scheme `://` lowercase host [`:` port only if non-default] path. No query, no fragment. Percent-encoding normalized per RFC 3986 §6.2.2 (uppercase hex in escapes; unreserved characters decoded). Path otherwise exact: no trailing-slash normalization. RPs behind proxies MUST configure `publicBaseUrl` in the SDK so `htu` reflects the URL the Agent targeted.
- `action`: RFC 8785 JCS over the exact JSON the RP submits to `/verify`. All numbers MUST be integers. Strings are hashed as given; no Unicode normalization. Both SDKs construct `action` from the same wire fields (`type`, `resource`, `amount`, `currency` for `commerce:purchase`; `type`, `method`, `resource` for `api:call`).
- Proof signing input is the standard JWS signing input.

### 4.4 Transport and binding profiles

The Agent sends `Delegus-Grant: <JWS>` and `Delegus-Proof: <JWS>` headers to the RP. The RP forwards both plus its own Action object to `/verify`, constructing the Action from what it actually received. Only the `http` binding profile exists in v0; any other → `PROOF_MALFORMED`.

---

## 5. `POST /verify`

### 5.1 Request

```json
{ "grant": "<JWS>", "proof": "<JWS>", "action": { "type": "commerce:purchase", "resource": "order_92813", "amount": 42700, "currency": "USD" } }
```

Authentication: RP API key. `action.type` is required; other fields per action type; unknown fields → `ACTION_MALFORMED`.

### 5.2 Protocol layer — `delegus-base-v1` (OPEN)

Reproducible by anyone given: Grant, Proof, Action, RP DID, a clock, the issuer's DID document snapshot, and the status-list credential snapshot. Each check yields `true | false | "skipped"`.

| # | Check | DENY reason |
|---|---|---|
| P1 | Grant parses and conforms to §3 (incl. `parentGrant` absent) | `GRANT_MALFORMED` / `UNSUPPORTED_FEATURE` |
| P2 | `kid` names a verification method present in the DID document snapshot | `ISSUER_KEY_NOT_FOUND` |
| P3 | Grant signature verifies | `GRANT_SIGNATURE_INVALID` |
| P4 | `now ≥ validFrom` | `GRANT_NOT_YET_VALID` |
| P5 | `now < validUntil` | `GRANT_EXPIRED` |
| P6 | Status credential signed by Delegus; bit at `statusListIndex` is 0 | `AUTHORITY_REVOKED` |
| P7 | `audience`, if present, contains RP | `AUDIENCE_MISMATCH` |
| P8 | Proof parses and conforms to §4 (incl. `jti` format) | `PROOF_MALFORMED` |
| P9 | Proof signature verifies against Agent key | `PROOF_SIGNATURE_INVALID` |
| P10 | Proof `iss` == Grant `sub` | `PROOF_AGENT_MISMATCH` |
| P11 | Proof `grant` == Grant `id` | `PROOF_GRANT_MISMATCH` |
| P12 | Proof `aud` == RP DID | `PROOF_AUDIENCE_MISMATCH` |
| P13 | `iat` within window | `PROOF_EXPIRED` |
| P14 | `action_hash` == hash of submitted Action | `PROOF_ACTION_MISMATCH` |
| P15 | Action valid for its `type` | `ACTION_MALFORMED` |
| P16 | Some Capability matches `action.type` | `ACTION_NOT_AUTHORIZED` |
| P17 | All constraint keys known | `UNKNOWN_CONSTRAINT` |
| P18 | Currency authorized | `CURRENCY_NOT_AUTHORIZED` |
| P19 | `amount ≤ maxAmount` | `AMOUNT_EXCEEDS_AUTHORITY` |
| P20 | `resource` matches a pattern | `RESOURCE_NOT_AUTHORIZED` |

The normative text of this table, published at a stable URL, is the **profile document**; its SHA-256 is `protocol.profile_hash` in every receipt.

### 5.3 Trust and service layer (DELEGUS) — `delegus-trust-v1`

| # | Check | DENY reason |
|---|---|---|
| T1 | `issuer` is a registered Principal | `ISSUER_UNKNOWN` |
| T2 | Principal is domain-verified | `ISSUER_UNVERIFIED` |
| T3 | `kid` not marked `compromised` | `ISSUER_KEY_COMPROMISED` |
| T5 | Replay: `SET replay:{rp_did}:{jti} 1 NX PX {2 × window_ms}` returns OK. Executed only after P1–P14 pass, as a separate internal operation from the revocation-version read that precedes P6 (§7). Store unavailable → `SERVICE_UNAVAILABLE`. | `PROOF_REPLAYED` / `SERVICE_UNAVAILABLE` |

`delegus-trust-v1` is exactly T1, T2, T3, T5. Not configurable per RP. `ISSUER_NOT_TRUSTED` is a reserved reason code. (T4 and T6 do not exist; identifiers are not reused.)

### 5.4 Canonical evaluation order and reason selection

```
P1 → T1 → T2 → T3 → P2 → P3 → P4 → P5
   → [read status_ver; refresh cached status credential if needed]
   → P6 → P7 → P8 → … → P14
   → [atomic replay SET NX]
   → T5 → P15 → … → P20
```

The bracketed steps are internal operations, not checks; they are shown to fix where the revocation-state read and the replay write occur relative to the checks.

Decision is DENY if any check is `false`; `reason` is the first `false` in this order. `protocol.result` is PASS iff all P-checks are true; `trust.result` is PASS iff all T-checks are true. Both are reported even on DENY; checks whose prerequisites failed are `"skipped"`. `now` is Delegus server time, recorded in the receipt as `evaluated_at`; historical re-verification uses that value as `now` (§6.4).

### 5.5 Hot-path constraint

`/verify` MUST perform no external DID resolution, status-list retrieval, issuer lookup, DNS lookup, or third-party network request. Internal low-latency infrastructure (Redis, Delegus-owned storage) MAY be accessed. Latency objective: p50 ≤ 30 ms, p99 ≤ 200 ms.

### 5.6 Response

The response body is the Receipt (§6) as JSON, including the `receipt` JWS.

---

## 6. The Decision Receipt

### 6.1 Purpose

A Receipt lets any party later prove: at time T, Agent A acting for Principal B presented Grant C to RP D for Action E; Delegus evaluated it against *this* identity state, *this* revocation state, *these* rules, and *this* trust configuration, and decided X. It remains provable after keys rotate, DID documents change, and status lists are replaced.

### 6.2 Schema

```json
{
  "receipt_version": "delegus-receipt-v1",
  "receipt_id": "drc_01K4Q7ZP9X2M3N4R5S6T7V8W9Y",
  "decision": "ALLOW",
  "reason": null,
  "evaluated_at": "2026-09-09T14:03:27.412Z",
  "relying_party": "did:web:delegus.ai:rp:seller",
  "principal": { "id": "did:web:acme.com", "verification": "domain" },
  "agent": "did:key:z6Mk...",
  "grant_id": "urn:delegus:grant:01J9X4...",
  "grant_hash": "sha256:...",
  "proof_hash": "sha256:...",
  "request_hash": "sha256:...",
  "authority": { "action": "commerce:purchase", "constraints": { "maxAmount": 50000, "currency": "USD" } },
  "expires_at": "2026-10-01T00:00:00Z",

  "evidence": {
    "issuer_resolution": {
      "did": "did:web:acme.com",
      "did_document_hash": "sha256:...",
      "resolved_at": "2026-09-09T14:00:11Z"
    },
    "status": {
      "credential_url": "https://status.delegus.ai/acme.com/1",
      "credential_hash": "sha256:...",
      "valid_from": "2026-09-09T14:03:00Z",
      "checked_at": "2026-09-09T14:03:27.410Z",
      "status_list_index": "4127"
    },
    "trust_config_version": "trust_0042",
    "trust_config_hash": "sha256:..."
  },

  "protocol": {
    "result": "PASS",
    "profile": "delegus-base-v1",
    "profile_hash": "sha256:...",
    "checks": { "P1": true, "P2": true, "P20": true }
  },
  "trust": {
    "result": "PASS",
    "policy_version": "delegus-trust-v1",
    "checks": { "T1": true, "T2": true, "T3": true, "T5": true }
  },

  "receipt": "<compact JWS>"
}
```

### 6.3 Hash encoding

One encoding everywhere: `sha256:<64 lowercase hex chars>`. Inputs are the exact bytes Delegus saw:

| Field | Bytes hashed |
|---|---|
| `grant_hash` | ASCII of the compact JWS string as received |
| `proof_hash` | ASCII of the compact JWS string as received |
| `request_hash` (= Proof `action_hash` when P14 passes) | UTF-8 of `JCS(action)` |
| `evidence.issuer_resolution.did_document_hash` | raw response body bytes as fetched |
| `evidence.status.credential_hash` | ASCII of the status-list credential compact JWS |
| `evidence.trust_config_hash` | UTF-8 bytes of the immutable trust-configuration descriptor named by `trust_config_version` |
| `protocol.profile_hash` | raw UTF-8 bytes of the published profile document |

### 6.4 Signature and rules

- `receipt` is a **non-detached** compact JWS: `alg: EdDSA`, `typ: delegus-receipt+jwt`, `kid: did:web:delegus.ai#receipt-<n>`, payload = `JCS(receipt object minus "receipt")`. The JWS string is the canonical receipt; the JSON body is a decoded convenience and MUST equal the payload after JCS.
- On DENY where P3 did not pass, `principal`, `agent`, `grant_id`, `authority`, `expires_at` are omitted. Delegus never echoes an unauthenticated attribution.
- **Offline verification:** given the receipt, the Delegus receipt public key, and the artifacts named by `did_document_hash`, `credential_hash`, `profile_hash`, and `trust_config_hash` (retrievable by hash — §8), a third party can re-run `delegus-base-v1` and confirm `protocol.result`. The `trust` block is Delegus's attestation and is not reproducible by design.
- **Historical verification clock (normative):** when reproducing the protocol result represented by a Receipt, the verifier MUST use the Receipt's authenticated `evaluated_at` value as `now` for every time-dependent protocol check (P4, P5, P13). It MUST NOT use its own wall-clock time. Because `evaluated_at` is inside the signed payload, the historical evaluation time cannot be altered without invalidating the Delegus signature.

```
live /verify:                 now = Delegus server clock  (recorded as evaluated_at)
offline receipt verification: now = signed receipt.evaluated_at
```

---

## 7. `POST /revoke`

```json
{ "grant_id": "urn:delegus:grant:01J9X4..." }
```

Authenticated as the issuing Principal.

**Consistency guarantee (normative):** `POST /revoke` MUST NOT return success until the revocation is durably recorded and every subsequent successful `/verify` anywhere is guaranteed to evaluate against revocation state at least as new as that write. Once a successful revoke response has been returned, a later verification MUST NOT return ALLOW for that Grant.

- `POST /revoke` MUST be idempotent. Revoking an already-revoked Grant returns success with the existing revoked state.
- Revocation is permanent; there is no un-revoke.
- Delegus MUST fail closed (`SERVICE_UNAVAILABLE`) if it cannot establish current revocation state.
- Internal coordination MAY use any implementation that preserves the guarantee. Reference design: one Bitstring Status List per Principal (131,072 bits); `/revoke` sets the bit, re-signs the Principal's status-list credential, and increments `status_ver:{principal}` in the internal revocation store; each engine node caches `{principal → (version, signed credential)}`.
- **Ordering within `/verify` (normative):** before P6, the engine obtains the current `status_ver:{principal}` from the internal revocation store and ensures its cached signed status credential is at least that version. After P1–P14 pass, T5 atomically consumes the Proof `jti`. These are two distinct internal operations; in the worst case `/verify` performs two internal round trips. They MAY be optimized or coalesced (e.g. reading `status_ver` at request start, which still observes every revocation that completed before the request began) provided the canonical evaluation order (§5.4), the rule that T5 executes only after P1–P14, and the global read-after-write revocation guarantee are all preserved. Implementations MUST NOT reorder P6 after T5 or execute T5 before P14 to save a round trip.
- Published credentials at `statusListCredential` URLs serve the latest signed list with `Cache-Control: max-age=30`, for third-party verifiers only.

---

## 8. Evidence preservation (DELEGUS obligation)

Delegus MUST retain, addressable by hash and retrievable by any party holding a receipt that references them: every DID document snapshot, status-list credential snapshot, profile document, and trust-configuration descriptor referenced by a receipt; the receipt itself; and its outcome ledger. Retention: 7 years from `evaluated_at` (open question 8). `GET /evidence/{sha256}` returns the exact bytes.

---

## 9. Outcome Attestations

### 9.1 Principle
Outcomes are events, not states. Nothing is overwritten. Events record what the reporter observed, never a judgment about the agent; judgments arrive only as `dispute_resolved.resolution`.

### 9.2 Endpoint
`POST /decisions/{receipt_id}/outcomes` — authenticated as the RP named in the receipt. Append-only.

```json
{ "type": "dispute_opened", "occurred_at": "2026-09-14T09:12:00Z", "evidence_ref": "https://seller.example/disputes/8812", "note": "buyer claims goods not ordered" }
```

Delegus adds `attestation_id` (`oat_<ULID>`), `reporter` (RP DID), `recorded_at`. Corrections are new events with `correction_of: <attestation_id>`. No edits, no deletions. Ordering is by `recorded_at`.

### 9.3 v0 vocabulary
`fulfilled` · `cancelled` · `dispute_opened` · `dispute_resolved` (`resolution: agent_at_fault | agent_not_at_fault | indeterminate`) · `chargeback_received` · `chargeback_reversed` · `loss_reported` (`loss_amount` minor units and `currency` required). Unknown `type` → 400. `loss_amount` is per event; Delegus never sums or nets.

### 9.4 Reporters and signatures
v0 reporter: the RP named in the receipt only. `reporter_signature` (JWS over JCS of the event by the RP key) is OPTIONAL in v0 and REQUIRED from the day any assurance product reads the ledger. Reserved reporter roles (Principal, adjudicator) are not accepted in v0.

---

## 10. Data provenance and reuse rights

Every decision and outcome record carries `tenant` (RP), `reporter`, `principal`, and a `reuse_rights` tag set at contract time. v0 defaults: used for the tenant's own audit and retrieval; for service operation (replay, abuse prevention); and for Delegus-internal, aggregate, non-identifying risk analysis. **No cross-tenant disclosure** of any Principal's or Agent's history and no per-entity scoring exposed to any party. Any broader reuse requires an explicit rights change recorded against the tenant.

---

## 11. Key lifecycle

- **Rotate:** add key N+1 `active`, mark N `retired`. Grants signed by N remain valid to `validUntil`; new Grants MUST use an `active` key.
- **Compromise:** `POST /keys/{id}/compromise` marks the key; every Grant it signed DENIES (T3) immediately.
- **Agent compromise:** revoke its Grants or use the kill switch; a new key is a new Agent.

---

## 12. Out of scope for v0

Delegation chains and attenuation (field reserved) · cumulative or per-period limits · RP-side policy · non-HTTP binding profiles · non-JWS Grant profiles · external identity adapters · agent key rotation in place · un-revoke · any bureau, score, or cross-tenant history product · any guarantee, warranty, or economic assurance.

---

## 13. Threat Model (normative scope of protection)

### 13.1 v0 explicitly protects against

| ID | Threat | Delivered by |
|---|---|---|
| TM-01 | Forged Grant | P2, P3 |
| TM-02 | Modified Grant | P3 |
| TM-03 | Stolen Grant used without the Agent key | P9, P10 |
| TM-04 | Proof replay at the same RP | P13, T5 |
| TM-05 | Proof forwarded to or replayed at a different RP | P12 |
| TM-06 | Action tampering between Agent and Delegus | P14 |
| TM-07 | Expired or not-yet-valid authority | P4, P5 |
| TM-08 | Revoked authority | P6 + §7 guarantee |
| TM-09 | Issuer key compromise, after Delegus is notified | T3 |
| TM-10 | Stale identity evidence going unrecorded | Receipt pins `did_document_hash`, `resolved_at`; refresh ≤ 5 min |
| TM-11 | Unknown or unverified issuer | T1, T2 |
| TM-12 | Status-list substitution or forgery | P6 (Delegus-signed); receipt pins `credential_hash` |
| TM-13 | Grant used at an RP the Principal excluded | P7 |
| TM-14 | Evidence altered after the fact | Content-addressed store referenced by hash from signed receipts |

### 13.2 v0 explicitly does not protect against

| ID | Threat | Note |
|---|---|---|
| NP-01 | A malicious Principal intentionally authorizing malicious activity | Delegus attests authority, not intent |
| NP-02 | A compromised active Principal key before Delegus learns of it | Bounded by Grant lifetime and key count |
| NP-03 | A compromised Agent private key | Bounded by Grant scope and expiry; KMS/HSM signer recommended |
| NP-04 | Business fraud unrelated to authority | Transaction assurance, not verification assurance (§14) |
| NP-05 | Semantic ambiguity in poorly defined actions | Mitigated only by the closed constraint vocabulary; RPs own the mapping |
| NP-06 | False or mistaken Outcome Attestations | Reporter-tagged, append-only; not adjudicated in v0 |
| NP-07 | A domain-verified Principal impersonating another organization via a lookalike domain | Domain control ≠ organizational identity; roadmap: KYB |
| NP-08 | An RP misreporting the action it received | The receipt records the RP's own submission |
| NP-09 | Compromise of Delegus's own signing keys | Trust root for the `trust` block; HSM custody; open-protocol reproducibility |
| NP-10 | Compromise of DNS or the web PKI | Shared with TLS; out of scope |

### 13.3 Relationship to future assurance
Any verification guarantee MUST be expressed as an explicit subset of §13.1. Nothing in §13.2 is ever covered by verification assurance; NP-04 is the boundary with transaction assurance, a different product.

---

## 14. Non-normative: assurance progression

```
SIGNED       — every decision carries a Delegus signature                       (v0)
RECORDED     — every decision has a pinned evidence snapshot + outcome ledger    (v0)
GUARANTEED   — verification assurance over a stated subset of §13.1             (earned)
RISK-SCORED  — counterparty / action risk from decision + outcome data          (earned)
GUARANTEED   — economic assurance for covered conditions, direct or with a regulated partner (earned)
```

---

## 15. Implementation contract

Build order:
1. `packages/core` — pure Grant/Proof parsing, canonicalization, P-checks, receipt assembly and signing. No I/O; snapshots and clock injected.
2. Conformance suite — ALLOW plus one deterministic vector for every DENY reason, including `htu` normalization, JCS, `jti` format, replay, revocation, and malformed inputs. The engine cannot merge until every vector passes.
3. `apps/api` — Principal registry, domain verification, status lists, replay protection, evidence preservation, receipt persistence, Outcome Attestations.
4. `packages/sdk` + CLI — `delegus.org.create()`, `agent.create()`, `grant.create()`, `agent.sign()`, `delegus.verify()`, outcome reporting.
5. `apps/demo` — buyer/seller: authorized 42700 → `ALLOW`; revoke Grant; **same action with a fresh Proof** → `AUTHORITY_REVOKED`. (A fresh Proof isolates revocation from replay; the replay path is covered separately by the conformance suite.)

**The afternoon integration (relying party, complete):**

```ts
import { Delegus } from "@delegus/sdk";
const delegus = new Delegus({ apiKey: process.env.DELEGUS_API_KEY, publicBaseUrl: "https://seller.example" });

app.post("/orders", async (req, res) => {
  const d = await delegus.verify({
    grant: req.header("Delegus-Grant"),
    proof: req.header("Delegus-Proof"),
    action: { type: "commerce:purchase", resource: req.body.orderId, amount: req.body.amountMinor, currency: req.body.currency },
  });
  if (d.decision !== "ALLOW") return res.status(403).json({ reason: d.reason });
  // proceed; persist d.receipt with the order
});
```

---

## 16. Remaining open questions (non-blocking)

1. Minor units vs. decimals — spec: minor units.
2. Proof window 60 s / 300 s.
3. Replay scope per-RP.
4. Grant max lifetime 90 days.
5. Grant by reference as an alternative to by value.
6. `loss_amount` per event (spec) — confirm with first assurance design.
7. When reporter signatures become REQUIRED.
8. Evidence retention period and storage model.
9. `decision` vs. `receipt` as the primary API noun.

**v0.2 is frozen.**
