Status: v0.3-rc1, release candidate, published, NOT frozen. v0.3 ships as this release candidate; v0.2 stays the normative text until v0.3 is frozen. docs/ (errata 1–3) remains the sole normative source until this draft is frozen. Freezing requires an external cryptographic review of the dependency commitment (§R) and its byte framing, and every v0.3 conformance vector passing.
What v0.3 changes (summary):
- The receipt pins the matched authority by commitment; the plaintext Capability is returned to the relying party in the response and is not part of the signed receipt (receipt v2, §6).
- Dependencies (chain of reliance): an Action may declare
relies_on[]commitments to prior receipts, each with a mandatory mode (§4.3, §R); each ALLOW receipt carries a pairwise transaction handle as an output (§R.3); the relying party states which dependencies it requires (§5.1); dependency checks D1–D7 run after P21 (§5.2–§5.4). The receipt pins the declared entries and the requirements by hash (§6.3). - Seller-side action types:
commerce:quote,commerce:accept,commerce:fulfil(§3.3). - Consumable grants: the
budgetconstraint, check P21 (headroom) and trust check T7 (the atomic spend). The spend is the very last step of an evaluation and happens only when every other check is true, so a DENY never consumes budget (§5.2–§5.4). - Receipt v2, the profile document
delegus-base-v3with trust policydelegus-, reason codes for the above, and §6.3 rows for every new field.trust- v3 - Privacy principles as normative text; receipts private by default; an entitled disclosure package for reproducing dependency checks (§0.1, §8, §R.5).
Opt-in. v0.3 applies to a relying party registered on the profile delegus-base-v3. v0.2 (delegus-base-v1, delegus-, delegus-) remains the default and is unchanged; every v0.2 conformance vector passes unchanged.
Out of v0.3 core: accepting foreign authority formats (import, mapping), a general disclosure capability, auditor roles, disclosure levels, cross-issuer receipts.
(v0.2 text follows, amended in place. Unchanged sections are v0.2 verbatim.)#
v0.2 status (for reference): 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); 3: unusable Ed25519 public keys — non-canonical or small-order keys are rejected (§2.1, §2.2, §5.2 P1/P2/P8, §13.1). 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:
- An ordinary backend engineer can implement the protocol layer from this document with a standard JOSE library.
- 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.
- Fail closed. Anything unrecognized, missing, ambiguous, or unavailable is DENY.
- No per-transaction state except replay protection and the receipt/outcome ledger.
- Someone must be able to verify a Delegus artifact without calling Delegus.
0.1 Privacy principles (normative, v0.3)#
- Verifiability does not imply discoverability. Being able to verify a receipt, a dependency or a transaction handle never grants the ability to find, list or retrieve other receipts or evidence.
- Evidence should reveal no more than is necessary to verify the claim being made. A receipt carries hashes and commitments where the plaintext is not needed to verify it.
- Receipt authenticity is public. Receipt reasoning is entitled. Anyone holding a receipt can check it is authentic and unmodified (§6.4 public verification). Re-deriving why it was decided, including upstream authority, requires the entitled disclosure package (§R.5, §6.4 entitled reproduction, §8).
- A receipt may disclose that its decision depended on prior authority decisions. It does not, by itself, confer access to the underlying receipts or evidence.
- Transaction handles provide correlation, not discovery. Knowing a transaction handle or a dependency fingerprint never lets a party enumerate or retrieve the receipts it relates to.
Not claimed: Delegus issues and evaluates every receipt in a transaction, so Delegus itself can correlate them. Opaque handles and commitments limit correlation by outsiders, not by 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. |
| Dependency (v0.3) | A prior ALLOW receipt that an Action declares it rests on, in relies_on, with a mode: PRIOR_DECISION (the prior decision was genuine and ALLOW) or CURRENT_AUTHORITY (and its authority is still in force now). |
| Fingerprint (v0.3) | The commitment that names a dependency without revealing it: a hash over the prior receipt under a secret opening (§R.1). |
| Opening (v0.3) | The 32-byte secret, returned only to the relying party of an ALLOW receipt, that makes its fingerprint (§R.2). |
| Requirement (v0.3) | A relying party's statement, in requires, of which dependencies an Action must declare (§5.1, D1). |
| Transaction handle (v0.3) | An opaque, pairwise identifier on an ALLOW receipt that lets its relying party correlate its own receipts in one transaction; an output only (§R.3). |
| Disclosure package (v0.3) | The material that lets the receipt's principal or relying party reproduce its dependency checks offline (§R.5). |
| Budget (v0.3) | A consumable limit on a Capability (total, per period, uses), checked by P21 and spent by T7. |
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.) that are visually and cryptographically distinct from production. Test signing keys MUST never appear in the production DID document.delegus. ai - 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:/is production infrastructure with the same availability and TLS discipline as/ delegus. ai/ . well- known/ did. json /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., created byai:org:<slug> delegus..org. create() - Customer-hosted:
did:web:acme., accepted at onboarding.com
- Managed:
- 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. A key whose encoding is non-canonical (RFC 8032 §5.1.3: y ≥ p, or x = 0 with the sign bit set) or which is one of the eight points of small order MUST be rejected and is treated as absent (erratum 3). Key statesactive | retired | compromisedare Delegus trust data, not DID-document data. - Domain verification: DNS TXT at
_ordelegus. <domain> https:/containing a Delegus-issued token. The file counts only when the exact host serves it: a redirect to any other host,/ <domain>/ . well- known/ delegus. json www.included, proves nothing. Re-verified every 24 h. Failure sets the Principalunverified. - 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. Adid:keywhose key is non-canonical or of small order is malformed (erratum 3).- Generated locally by the SDK.
POST /agentsregisters the public key for inventory and the agent-level kill switch (POST /revokes every Grant naming that Agent). Registration is not consulted byagents/ {did}/ disable /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.or customer-hostedai:rp:<slug> did:web. - Authenticates to
/verifywith 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.3)#
{ "action": "commerce:purchase", "constraints": { "maxAmount": 50000, "currency": "USD" }, "resources": ["*"] }action—<namespace>:<verb>. v0.3 (delegus-base-v3) defines exactly five. The first two are unchanged from v0.2; the last three are the seller-side commitments, new in v0.3:
action | Constraints (all REQUIRED unless marked OPTIONAL) | Meaning of an authorized action |
|---|---|---|
commerce:purchase | maxAmount (integer, minor units), currency (ISO 4217, single); OPTIONAL budget (total, perPeriod, uses) | the actor commits its Principal to buy |
api:call | methods (non-empty array of uppercase HTTP methods); OPTIONAL budget (uses only) | the actor calls an API on its Principal's behalf |
commerce:quote | maxAmount, currency; OPTIONAL budget (uses only) | the actor offers, for its Principal, to sell at the stated price |
commerce:accept | maxAmount, currency; OPTIONAL budget (uses only) | the actor accepts an order for its Principal at the stated price |
commerce:fulfil | none; OPTIONAL budget (uses only) | the actor commits its Principal to deliver the named resource |
constraints— keys MUST all be defined for that action type in this spec version; underdelegus-base-v3,budgetis defined for every type as in the table (a v3 implementation MUST NOT reject it at P17). Unknown key → DENYUNKNOWN_(P17). A missing or ill-typed REQUIRED key makes the Grant malformed (P1).CONSTRAINT merchantCategory, listed as OPTIONAL in v0.2, is not defined in v0.3: the §4.3 action has no field it could be compared with, so it cannot be evaluated; a Grant carrying it fails P17 (as v0.2 engines already do).resources— OPTIONAL glob array matched againstaction.resource(*any run of characters except/,**any run of characters; everything else literal; whole-string match). Absent =["*"].- Authorization: at least one Capability matches; a Capability matches iff
actionis equal and every constraint is satisfied.currency(P18) andmaxAmount(P19) apply to every action that carries an amount (commerce:purchase,commerce:quote,commerce:accept);methodsapplies toapi:callwith the resource patterns at P20;commerce:fulfilis matched onresourcesalone. These are per-action limits; cumulative limits are the OPTIONALbudgetconstraint below. All amounts are integers in minor units; no floats anywhere in the protocol. - A Grant evaluated under a v0.2 profile (
delegus-base-v1,delegus-base-v2) MUST NOT contain the v0.3 action types: under those profiles they are unknown action types and the Grant is malformed (P1). budget(v0.3) — OPTIONAL on every action type: a consumable limit on the Capability, checked at P21 and spent by T7 (§5.2, §5.3). An object with at least one of these members and no other:total— non-negative integer, minor units of the Capability'scurrency: the most that may be spent over the Grant's life.commerce:purchaseonly.perPeriod—{ "amount": <non-, exactly those two members: the most that may be spent in one period. Periods are anchored at the Grant'snegative integer, minor units>, "period": "day" | "week" | "month" } validFromin UTC; amonthperiod that starts on a day the target month lacks starts on its last day.commerce:purchaseonly.uses— positive integer: how many ALLOW decisions the Capability may authorize. Every action type.
total or perPeriod on any other action type, an empty budget, an unknown member, or an ill-typed value makes the Grant malformed (P1). Money budgets on commerce:quote and commerce:accept are an open question (§16, item 12). Under delegus-base-v1, budget is an unknown key (P17).
3.3.1 Budget arithmetic and the consumption snapshot (v0.3)#
Periods. For a perPeriod budget, the period containing now starts at:
day:validFrom + floor((now − validFrom) / 86 400 s) × 86 400 s;week: the same with 604 800 s;month:validFromadvanced by k calendar months in UTC, for the largest k ≥ 0 whose start is ≤now. Advancing keeps the time of day and the day of month; a day the target month lacks becomes that month's last day (a Grant valid from 31 January has periods starting 28 or 29 February, 31 March, 30 April, …).
All arithmetic is in UTC milliseconds; a period start is written with whole seconds, YYYY-.
The consumption snapshot is what the service read for the Grant. A service MAY read it lazily, when P21 is reached with a budgeted P20 survivor, and MUST NOT read or keep one for a request that does not reach P21 with a budgeted survivor. It is a JSON object with exactly these members:
{ "grant_id": "urn:delegus:grant:…", "as_of": "<RFC 3339 UTC>", "version": 7,
"capabilities": [ { "key": "sha256:…", "spent_total": 57300, "spent_in_period": 0,
"period_start": "2026-09-08T00:00:00Z" | null, "uses_spent": 3 } ] }key is the ledger key of a Capability, sha256 over UTF-8 JCS(capability) exactly as it appears in the Grant; it is internal to the service and never appears in a receipt (§R.6). All counts are non-negative integers (minor units for the two spent_ members).
Headroom (P21). For a budgeted Capability with snapshot entry e, an action of amount a (0 for types without an amount) and one use, the dimensions are checked in the order total, period, uses, and the first without headroom fails:
- total:
e.;spent_ total + a ≤ total - period: the effective spend is
e.whenspent_ in_ period e.period_startequals the start of the current period, and 0 whene.period_startisnullor earlier; aperiod_startlater than the current period start is inconsistent state and givesSERVICE_. ThenUNAVAILABLE effective + a ≤ perPeriod.;amount - uses:
e..uses_ spent + 1 ≤ uses
The P20 survivors are tried in Grant order: a survivor without budget, or the first with headroom, is chosen; a budgeted survivor with no snapshot, or with no entry for its key, stops the search with SERVICE_ (fail closed). If none is chosen, P21 is false with AUTHORITY_. The snapshot's hash in the receipt is evidence. (§6.3).
Restoring a spend. A cancelled outcome attestation (§8) on an ALLOW receipt restores what that receipt consumed, once; an attestation that corrects a cancelled one applies the spend again. No other outcome type touches consumption. The trust-configuration descriptor records this as consumable_.
3.4 Example payload#
{
"@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 configurepublicBaseUrlin the SDK sohtureflects 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 constructactionfrom the same wire fields.- Proof signing input is the standard JWS signing input.
Action wire fields (v0.3). An Action has exactly the fields its type lists, plus the two OPTIONAL fields below; any other field is ACTION_ (P15).
type | Fields |
|---|---|
commerce:purchase, commerce:quote, commerce:accept | type, resource (non-empty string), amount (non-negative integer, minor units), currency (ISO 4217) |
api:call | type, method (uppercase HTTP method), resource (absolute http(s) URI) |
commerce:fulfil | type, resource (non-empty string) |
OPTIONAL on every type (v0.3):
relies_on— the prior decisions this action is taken in dependence on: an array of 1 to 8 entries, each an object with exactly two members,fingerprint(sha256:<64 lowercase hex>, §R.1) andmode(PRIOR_DECISIONorCURRENT_). Fingerprints within one array MUST be distinct. There is no default mode: an entry withoutAUTHORITY mode, with an unknown mode, or with any other member isACTION_. An empty array isMALFORMED ACTION_(omit the field instead).MALFORMED
relies_on is part of the Action, so it is inside JCS(action) and bound by the actor's Proof through action_hash (P14). Only the actor declares a dependency. A relying party states what it requires (§5.1); it cannot add, remove or change a declaration without failing P14. The order of relies_on entries is significant: the dependency checks run in array order (§5.4).
The Action has no transaction member. A transaction handle is an output of /verify only, never an input (§R.3), so no handle crosses a trust boundary on the way in. An Action carrying transaction, or any other member not listed above, is ACTION_ (unknown member, P15).
4.4 Transport and binding profiles#
The Agent sends Delegus- and Delegus- 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.
R. Dependency commitments, openings and transaction handles (v0.3)#
This section is normative for delegus-base-v3. It is subject to the external cryptographic review required before freeze (status header).
R.1 The relies_on fingerprint#
A relies_on entry names a prior receipt by a commitment, never by an address. For a receipt R and a 32-byte opening nonce:
fingerprint(nonce, R) = SHA-256( u8(len(tag)) ‖ tag ‖ nonce ‖ u64be(len(R)) ‖ R )
tag = "delegus:relies-on:v1" 20 bytes, ASCII
nonce = 32 bytes from a CSPRNG the opening
R = the receipt's compact JWS, ASCII bytes, exactly as issued (the `receipt` member, §6.4)
u8 = one unsigned byte; u64be = unsigned 64-bit big-endian integerThe value is written sha256:<64 lowercase hex> (§6.3). Every variable-length field is length-prefixed and the nonce has a fixed length, so the encoding is injective: no two distinct (tag, nonce, R) produce the same preimage. The length-prefixed tag separates these digests from every other hash in this specification, whose inputs are bare bytes, and versions the construction (a successor uses a new tag). The 256-bit nonce hides which receipt a fingerprint names from anyone without the opening; binding rests on SHA-256 collision resistance; authenticity is the named receipt's own signature (D3), which is why this is a plain hash and not a MAC.
R.2 Openings#
- Delegus generates the opening when it issues an ALLOW receipt (a DENY receipt has none, so the service never resolves one; D4 remains as a fail-closed check), keeps it, and returns it only to that receipt's relying party, in the
/verifyresponse (§5.6):{ "fingerprint": "sha256:…", "nonce": "<base64url>" }. The nonce's wire form is the canonical unpadded base64url encoding of the 32 bytes: exactly 43 characters; any other form is invalid. - A relying party passes the fingerprint, never the nonce, to a party that will declare the dependency. A fingerprint confers no access to the receipt (§0.1).
- D2 resolves a declared fingerprint inside Delegus: it holds the value only if recomputing
fingerprint(nonce, R)from a receipt R Delegus issued and that receipt's stored opening yields exactly the declared value. Resolution is a lookup in Delegus-owned storage (§5.5). - Resolution precedes P15, so it is restricted. The lookup in the bracketed step before P6 (§5.4) happens before P15 validates the Action. It MUST consider only entries whose
fingerprintalready has the §6.3 form (sha256:and 64 lowercase hex characters), at most the first 8 entries, by exact fingerprint only (an equality lookup, never a prefix, range or scan), and it MUST have no effect other than reading: it reads the issued-receipt records (each receipt with its opening), then the internal-transaction records of the receipts found and, forCURRENT_entries, each dependency Principal's standing, the key state of each dependency Grant'sAUTHORITY kidand its Principal's status-list credential, and nothing else: no write, no counter, no allocation. Its answer is used only if P15 later holds; if P15 fails, the answer is discarded and D1–D7 are"skipped". A malformed entry is therefore never looked up, and a lookup can never change the outcome of a request that P15 refuses.
R.3 Transaction handles#
- Delegus keeps an internal transaction identifier; it never appears in a receipt, a response or an API.
- On ALLOW, the receipt carries a pairwise handle for its relying party: 16 bytes from a CSPRNG, written as exactly 22 characters of canonical unpadded base64url (
transaction, §6.2), returned in the response (§5.6). A DENY receipt carries no handle and no opening. - A handle is per internal transaction and relying party. The same relying party acting again in the same internal transaction receives the same handle; a different relying party receives a fresh one. A handle is never reused across relying parties, so handles held at different trust boundaries cannot be linked by comparing them.
- A handle is an output only. It is never accepted as an input: the Action has no
transactionmember (§4.3), and no request field takes one. - An action with no
relies_onstarts a new internal transaction (on ALLOW, the receipt is its first member). An action that declaresrelies_onjoins the internal transaction of its first resolved dependency, in Action order. D5 holds for an entry only if that dependency belongs to the same internal transaction; if the declared dependencies belong to different internal transactions, D5 is false. - Transaction handles give correlation, not discovery: no API or evidence lookup by handle is offered to anyone who is not a party to the receipt it came from (§0.1, §8).
R.4 The CURRENT_AUTHORITY evidence commitment#
For a CURRENT_ dependency the receipt commits to the evidence D6–D7 used, without disclosing it:
evidence_commitment = SHA-256( u8(len(tag)) ‖ tag ‖ nonce_e ‖ u64be(len(E)) ‖ E )
tag = "delegus:dependency-evidence:v1"
nonce_e = 32 fresh bytes from a CSPRNG, kept by Delegus
E = UTF-8 of JCS({ "receipt_hash": sha256 of R's JWS ASCII,
"status_credential_hash": the dependency Principal's status-list credential as read,
"status_list_index": the dependency Grant's index })Entitled reproduction (the principal or the relying party, §8) receives E and nonce_e; public verification checks only the receipt's signature.
R.5 Reproducing a dependency decision (entitled disclosure package)#
A receipt's authenticity is public: anyone holding it can check its signature (§6.4). Reproducing why its dependency checks came out as they did is entitled (§0.1): it needs material a third party does not have, and Delegus provides it only to an entitled party, the principal or the relying party of the receipt being reproduced (§8).
For a receipt R2 that declared dependencies, the dependency disclosure package contains, per relies_on entry, in declaration order:
Per entry, every party entitled to R2 receives: checks (D2–D7 as evaluated), transaction_ (D5, Delegus's attestation), receipt_hash (sha256 of the dependency receipt's JWS) and, for CURRENT_AUTHORITY, status_, status_ and evidence_nonce, which open evidence_ without the dependency receipt. The dependency receipt's JWS (receipt) and its opening (opening) are included only when the requesting party is itself entitled to that dependency receipt (its relying party or principal). Declaring a fingerprint, or being entitled to R2, never entitles a party to the dependency receipt. No package is kept for a DENY receipt.
| Member | Who receives it | Lets the holder re-run |
|---|---|---|
checks | every party entitled to R2 | nothing: Delegus's per-entry attestation of D2–D7 |
transaction_equal | every party entitled to R2 | D5, as Delegus's attestation (internal transactions are never disclosed, §R.3) |
receipt_hash | every party entitled to R2 | with the CURRENT_AUTHORITY members, the §R.4 opening of evidence_commitment |
CURRENT_AUTHORITY only: status_list_index, status_credential (as read before P6), evidence_nonce | every party entitled to R2 | D7 against the credential, and evidence_commitment = the §R.4 commitment over JCS({receipt_hash, status_credential_hash, status_list_index}) |
receipt: the dependency receipt's compact JWS | only a party also entitled to the dependency receipt | D3 (signature, body = payload), D4 (decision), D6 (expires_at), and receipt_hash |
opening: its 32-byte nonce, base64url | only a party also entitled to the dependency receipt | D2: fingerprint(opening, receipt) equals the declared fingerprint (§R.1) |
With the package, now = R2's evaluated_at (§6.4): D7's revocation part and the evidence commitment are reproducible offline by every party entitled to R2; D7's standing and key-state parts are Delegus's attestation, like D5 (the package carries neither). D2–D4 and D6 are reproducible only by a party entitled to both R2 and the dependency receipt; every other party entitled to R2 relies on Delegus's per-entry attestation (checks) for them. D5 is reproducible by no one outside Delegus: it is Delegus's attestation, like the trust block. Without the package, a third party can verify R2's signature and read which entries were declared, by fingerprint and mode, and nothing more: a fingerprint does not reveal which receipt it names (§R.1).
What a CURRENT_ dependency discloses (stated, accepted). Every party entitled to R2 receives, for each CURRENT_ entry, the dependency Principal's status-list credential and the dependency Grant's status_. The credential's id is the Principal's status-list URL (for example https:/), so these parties learn who the dependency's Principal is, even when they are not entitled to the dependency receipt itself. With the index they can read that Grant's revocation bit, and because status-list credentials are public by nature (§8), they can keep observing its revocation status afterwards by fetching the current credential. The credential also carries the bits of the Principal's other Grants in the same list, which this index does not identify. A PRIOR_DECISION entry discloses none of this. An actor that does not want its counterparties to learn the dependency's Principal, or to follow its revocation, declares PRIOR_DECISION (§13.2 NP-15).
The same route also returns, as consumption_, the consumption snapshot a budgeted receipt pins by evidence., which makes its P21 reproducible (§6.4). The package is delivered only through authorized access (§8). It is never published by hash, and holding R2 does not by itself entitle anyone to it.
R.6 Authority and capability-key commitments#
A Capability has little entropy (an action type, an amount, a currency, a few patterns), so a plain hash of it would let anyone holding a receipt confirm a guess. A v2 receipt therefore pins the matched Capability, and the ledger key of a budgeted one, by commitment under the §R.1 framing, each with its own tag and its own fresh 32-byte nonce:
authority_commitment = SHA-256( u8(20) ‖ "delegus:authority:v1" ‖ nonce_a ‖ u64be(len(C)) ‖ C )
capability_key_commitment = SHA-256( u8(25) ‖ "delegus:capability-key:v1" ‖ nonce_k ‖ u64be(len(K)) ‖ K )
C = UTF-8 of JCS(matched Capability), exactly as it appears in the Grant
K = ASCII of the ledger key, "sha256:" ‖ 64 lowercase hex (§3.3.1)nonce_aandnonce_kare independent, from a CSPRNG, and never reused. Their wire form is the 43-character opening encoding (§R.2).- The response returns
authoritywithauthority_(§5.6), andopening capability_whenkey_ opening evidence.carriesconsumption capability_. Entitled parties can retrieve both openings again with the receipt (§8). The ledger key itself is never returned; an entitled party recomputes it fromkey_ commitment authority. - The relying party checks
authority_by recomputing it fromcommitment authorityandauthority_. A party holding only the receipt learns nothing about the Capability beyond the fact that one matched.opening - The four tags (
relies-on,dependency-,evidence authority,capability-key) are distinct, so no value computed under one can be presented as another.
5. POST /verify#
5.1 Request#
{ "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_.
v0.3: dependency requirements. The body MAY carry requires, the relying party's statement of which prior decisions this action must rest on:
{ "grant": "<JWS>", "proof": "<JWS>", "action": { "...": "...", "relies_on": [ { "fingerprint": "sha256:…", "mode": "CURRENT_AUTHORITY" } ] },
"requires": [ { "fingerprint": "sha256:…", "mode": "CURRENT_AUTHORITY" } ] }requiresis chosen by the relying party;action.is declared by the actor and signed throughrelies_ on action_hash(P14). Delegus checks that the declaration covers every requirement (D1) and evaluates every declared entry (D2–D7).- Each entry names a
fingerprint(§R) and amode,PRIOR_DECISIONorCURRENT_. There is no default mode: an entry without a known mode is malformed (P15 forAUTHORITY relies_on; forrequires, HTTP 400REQUIREMENTS_before evaluation, no receipt, theMALFORMED jtinot consumed). requireshas the §4.3relies_onentry shape exactly: 1 to 8 distinct entries, each exactly{ fingerprint, mode }. Because the shape is exact, the array as validated is the array as submitted;requires_hashis computed over it, in submitted order (§6.3), and a client computes the same value from what it sent.requiresis accepted only for a relying party ondelegus-base-v3; any other relying party that sends it gets HTTP 400REQUIREMENTS_, before evaluation.NOT_ SUPPORTED - At most 8 entries in
relies_on. - A requirement names exact fingerprints. A requirement by kind ("a
commerce:purchasereceipt in this transaction") is future work.
Service gates before evaluation (every profile). Delegus refuses a request with HTTP 403, no receipt and the Proof's jti not consumed, when the relying party itself is not fit to receive a decision: its standing is not good (RP_SUSPENDED, set by staff with a reason, audited), or it is customer-hosted and its organization holds no verified claim on its domain (RP_UNVERIFIED, §2: a claim that lapses turns the refusal on until it is re-verified). These are service refusals, not checks: they precede P1 and appear in no receipt. Registering a customer-hosted relying party or Principal likewise needs a verified claim first (HTTP 403 CLAIM_REQUIRED); a pending claim reserves nothing.
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 |
v0.3 (delegus-base-v3). Every check P1–P20 is as above, with P15 applying the v0.3 Action rules (§4.3) and P16–P20 the v0.3 vocabulary (§3.3). v0.3 adds P21 and D1–D7.
| # | Check | DENY reason |
|---|---|---|
| P21 | Some P20 survivor has budget headroom for the action, or declares no budget; periods are anchored at validFrom in UTC; counts come from the consumption snapshot. A missing snapshot for a budgeted survivor is SERVICE_UNAVAILABLE | AUTHORITY_EXHAUSTED |
The first P21 survivor in Grant order is the matched authority (authority_, §R.6) and the Capability the spend decrements. §3.3.1 gives the period arithmetic, the headroom order and the snapshot format.
v0.3 dependency checks. Evaluated for each entry of action., in array order, after P20; D1 is evaluated once, before the per-entry checks. Absent relies_on and absent requires: D1–D7 are true (vacuous). A dependency always names a receipt Delegus itself issued; receipts from another issuer are out of v0.3 core.
| # | Check | DENY reason |
|---|---|---|
| D1 | Every requires entry appears in action.relies_on with the same fingerprint and the same mode | DEPENDENCY_NOT_DECLARED |
| D2 | The fingerprint resolves, inside Delegus, to a receipt Delegus issued (§R: the fingerprint recomputed from that receipt's bytes and its stored opening equals the declared value). A stored record that is malformed is unavailable state, not a mismatch → SERVICE_UNAVAILABLE | DEPENDENCY_COMMITMENT_MISMATCH / SERVICE_UNAVAILABLE |
| D3 | That receipt's signature verifies against a Delegus receipt key and its body equals its payload | DEPENDENCY_RECEIPT_INVALID |
| D4 | Its decision is ALLOW | DEPENDENCY_NOT_ALLOW |
| D5 | It belongs to the same internal transaction as the first resolved dependency, in Action order (§R.3) | DEPENDENCY_TRANSACTION_MISMATCH |
| D6 | CURRENT_AUTHORITY only: the dependency's Grant is still within its validity at now (now < expires_at of the dependency receipt) | DEPENDENCY_AUTHORITY_EXPIRED |
| D7 | CURRENT_AUTHORITY only: the dependency Grant's Principal is in good standing (as T1 requires of the action's own Principal), the key that signed the dependency Grant (its kid) is not marked compromised (as T3), and the dependency Grant's bit is 0 in its Principal's status-list credential (as P6); all read in the bracketed step before P6 and checked in that order. Standing, key state or credential unavailable → SERVICE_UNAVAILABLE | DEPENDENCY_ISSUER_NOT_TRUSTED / DEPENDENCY_ISSUER_KEY_COMPROMISED / DEPENDENCY_AUTHORITY_REVOKED |
For PRIOR_DECISION, D6 and D7 are "skipped". PRIOR_DECISION states only that the prior decision was genuine, ALLOW and in this transaction; it says nothing about now. CURRENT_ additionally states that the authority it rested on was still in force at this evaluation.
The normative text of this table, published at a stable URL, is the profile document; its SHA-256 is protocol. in every receipt. The v0.3 profile document (delegus-base-v3) covers P1–P21 and D1–D7 and names the order in §5.4.
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- is exactly T1, T2, T3, T5. Not configurable per RP. ISSUER_ is reserved under delegus- and delegus-; delegus- emits it at T1 (below). (T4 and T6 do not exist; identifiers are not reused.)
v0.3 (delegus-) is T2, T3, T5, T1 strengthened as below, and T7. (delegus-, the policy of the retired delegus-base-v2 profile, is T1 as in v1, T2, T3, T5 and T7.)
| # | Check | DENY reason |
|---|---|---|
| T1 | issuer is a registered Principal in good standing. Standing is Delegus trust data: staff mark a Principal suspended or disputed on a misuse report, with a reason, audited, and lift it the same way; an issued receipt is never altered. A Principal not in good standing is not trusted: a signed DENY, and the Proof's jti is consumed as for any DENY that reaches T5. | ISSUER_UNKNOWN / ISSUER_NOT_TRUSTED |
| T7 | The answer of the [consume] step: the atomic decrement of the matched Capability's budget for this action. consumed → true; exhausted (another request took the headroom first) → false; store unavailable → SERVICE_UNAVAILABLE. A Capability without budget makes T7 true without a write. | AUTHORITY_EXHAUSTED / SERVICE_UNAVAILABLE |
[consume] runs only when every other check in the order is true. Otherwise T7 is "skipped" and nothing is consumed: a DENY never spends budget.
5.4 Canonical evaluation order and reason selection#
P1 → T1 → T2 → T3 → P2 → P3 → P4 → P5
→ [resolve every relies_on fingerprint (v0.3);
read status_ver for this Grant's Principal and for the Principal of every CURRENT_AUTHORITY dependency;
for every CURRENT_AUTHORITY dependency, read its Principal's standing and its Grant key's state (D7);
refresh cached status credentials if needed]
→ P6 → P7 → P8 → … → P14
→ [atomic replay SET NX]
→ T5 → P15 → … → P20
→ P21 (v0.3)
→ D1 → (per relies_on entry) D2 → D3 → D4 → D5 → D6 → D7 (v0.3)
→ [consume: atomic budget decrement, only if every check above is true] (v0.3)
→ T7 (v0.3)The bracketed steps are internal operations, not checks; they are shown to fix where the revocation-state reads and the replay write occur relative to the checks. Every revocation-state read, including a dependency's, precedes the replay write (erratum 1). Dependency checks run after P21 so the actor's own reasons are reported first. A dependency check that fails after T5 has consumed the Proof's jti, like any check after T5: the actor needs a fresh Proof to try again. The spend is the last step so that no request consumes budget and is then refused.
Deferred writes (v0.3). An implementation MAY evaluate first with the replay write and the spend deferred, then perform them in this order (the replay write, then the spend) and report their answers as T5 and T7, provided the result is identical to evaluating in the order above. This is how a receipt's size is known before anything is written (§6.4).
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. Resolving a dependency fingerprint (v0.3) is a lookup in Delegus-owned storage. 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.
v0.3. The response to the relying party also carries, beside the receipt and outside its signed payload:
authorityandauthority_: the matched Capability in plaintext and the 32-byte opening (base64url, 43 characters), present exactly when the receipt pinsopening authority_(on ALLOW, and on a DENY decided after a Capability matched). The relying party recomputescommitment authority_from them (§R.6).commitment capability_: present exactly whenkey_ opening evidence.pinsconsumption capability_(a budgeted Capability); it opens that commitment to the ledger key, which an entitled party recomputes fromkey_ commitment authority(§R.6).relies_on: the per-dependency results, present exactly when the receipt pinsrelies_on_hash: an array, in Action order, of{ fingerprint, mode, checks: { D2 … D7 }, evidence_. The relying party checkscommitment? } sha256(JCS(relies_=on)) relies_on_hash.opening: on ALLOW only,{ "fingerprint": "sha256:…", "nonce": "<base64url, 32 bytes>" }, the commitment to this receipt that a later action may declare, and its opening (§R.2). It is returned only in this response, only to this relying party; Delegus keeps the nonce to resolve the fingerprint internally (D2). A downstream agent is given the fingerprint by the relying party, out of band, never the nonce.
The transaction handle is not among these: it is the receipt's own signed transaction field, present on ALLOW only (§6.4, §R.3). A relying party that verifies through the AuthZEN evaluation route (POST /) receives the same beside-fields under context.delegus, so its receipts open and can be relied on in the same way. None of the fields above are part of the receipt payload; the receipt stays verifiable on its own (§6.4), and a client MUST treat a response whose beside-fields do not match the hashes the receipt pins as invalid.
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.
v0.3 (delegus-). The receipt pins the authority it relied on by a commitment (authority_, §R.6) instead of carrying the Capability in plaintext, pins the per-dependency results by hash (relies_on_hash, each dependency named only by its commitment) and the relying party's requirements by hash (requires_hash), and, on ALLOW, carries a pairwise transaction handle. A v2 receipt is bounded in size whatever the Grant contains or how many dependencies it declares, and discloses no Principal-authored text beyond the identifiers needed to verify it. delegus- receipts keep verifying under v0.2 rules.
6.2 Schema#
{
"receipt_version": "delegus-receipt-v2",
"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_commitment": "sha256:...",
"transaction": "<pairwise handle, ALLOW only, §R.3>",
"requires_hash": "sha256:...",
"relies_on_hash": "sha256:...",
"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"
},
"consumption": {
"snapshot_hash": "sha256:...", "as_of": "2026-09-09T14:03:27.400Z",
"capability_key_commitment": "sha256:...", "capability_index": 0, "consumed": "consumed"
},
"trust_config_version": "trust_0003",
"trust_config_hash": "sha256:..."
},
"protocol": {
"result": "PASS",
"profile": "delegus-base-v3",
"profile_hash": "sha256:...",
"checks": { "P1": true, "P2": true, "P20": true, "P21": true, "D1": true, "D2": true, "D7": true }
},
"trust": {
"result": "PASS",
"policy_version": "delegus-trust-v3",
"checks": { "T1": true, "T2": true, "T3": true, "T5": true, "T7": true }
},
"receipt": "<compact JWS>"
}evidence. is present when the matched Capability has a budget (or P21 failed on one): snapshot_hash and as_of always (§3.3.1); capability_ (§R.6) and capability_ for the Capability chosen; exhausted (total | period | uses), the first dimension without headroom, when P21 refused; consumed records the T7 answer when the spend ran.
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 |
authority_commitment (v0.3) | not a plain hash: the §R.6 commitment over UTF-8 JCS(matched Capability), exactly as the Capability appears in the Grant, tag delegus:authority:v1, opened by authority_opening; absent when no Capability matched |
evidence.consumption.capability_key_commitment (v0.3) | not a plain hash: the §R.6 commitment over the ASCII ledger key, tag delegus:capability-key:v1, opened by capability_key_opening; present when the spend's Capability is identified |
evidence.consumption.snapshot_hash (v0.3) | UTF-8 of JCS(consumption snapshot), the §3.3.1 object exactly as read for this evaluation |
requires_hash (v0.3) | UTF-8 of JCS(requires), the relying party's requirement array exactly as submitted (§5.1); absent when the request had no requires |
relies_on_hash (v0.3) | UTF-8 of JCS(relies_on entries): the array of per-dependency results returned beside the receipt (§5.6), each { fingerprint, mode, checks, evidence_commitment? }, in Action order; absent when nothing was declared |
relies_on[].fingerprint (v0.3, response entry pinned by relies_on_hash) | not a plain hash: the §R.1 commitment over the prior receipt's JWS ASCII under its opening, preimage u8(len(tag)) ‖ tag ‖ nonce ‖ u64be(len) ‖ receipt, tag delegus:relies-on:v1 |
authority_opening, capability_key_opening (v0.3, response only) | not hashes: 32-byte nonces, 43-character base64url (§R.2, §R.6) |
relies_on[].evidence_commitment (v0.3, response entry pinned by relies_on_hash) | not a plain hash: the §R.4 commitment over JCS(dependency evidence), tag delegus:dependency-evidence:v1; present only for CURRENT_AUTHORITY |
transaction (v0.3) | not a hash: a random 16-byte handle, 22-character base64url (§R.3); an output only, never an input |
6.4 Signature and rules#
receiptis a non-detached compact JWS:alg: EdDSA,typ: delegus-,receipt+jwt kid: did:web:delegus., payload =ai#receipt- <n> 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_,commitment expires_atare omitted. Delegus never echoes an unauthenticated attribution. - v0.3 fields:
authority_is present when a Capability matched (on ALLOW, and on a DENY decided after P16); its opening is returned beside the receipt (§5.6) and again to entitled parties (§R.6).commitment transactionis present on ALLOW only: this receipt's pairwise handle (§R.3), an output only. A DENY receipt carries none.relies_on_hashis present when the Action declared dependencies. It pins the per-entry results returned beside the receipt (§5.6): each entry'sfingerprint,mode, per-entry resultsD2–D7and, forCURRENT_, anAUTHORITY evidence_to the dependency evidence it used (§R.4).commitment requires_hashis present exactly when the request carriedrequires(§5.1).protocol.–checks. D1 D7are the conjunction over entries, andreasonis the firstfalsein §5.4 order, entries taken in Action order.
- Size (normative): the receipt's JWS signing input MUST NOT exceed 4,096 bytes. Delegus determines the complete receipt before any write: it evaluates with the replay write and the spend deferred (§5.4), with the transaction handle's fixed length counted for an ALLOW, and measures the signing input. If it fits, the writes follow in order. If it does not: when the
jtiis already recorded the result is thePROOF_REPLAYEDreceipt, and when the replay store is unavailable theSERVICE_receipt (both small, as the §5.4 order would give); otherwise Delegus refuses with HTTP 422UNAVAILABLE RECEIPT_, no receipt, nothing written, theTOO_ LARGE jtinot consumed and no budget spent. With the authority, dependencies and requirements pinned by hash, a v2 receipt's size does not grow with the Grant or the number of dependencies. - Public verification (v0.3): anyone holding a receipt can check its signature, that its body equals its payload, and that the hashes it pins are well formed. This shows the receipt is authentic and unmodified; it does not reveal why it was decided.
- Entitled reproduction (v0.3): re-deriving the decision needs, in addition, the Grant, Proof and Action, the plaintext Capability and dependency results returned beside the receipt (to open
authority_and checkcommitment relies_on_hash), therequiressent (to checkrequires_hash), and the dependency disclosure package (§R.5). With them, P1–P21, D1–D4, D6 and D7's revocation part are reproducible offline withnow=evaluated_at. D5, and D7's standing and key-state parts, are Delegus's attestation, stated per entry in the package; P21 is reproducible given the consumption snapshot pinned byevidence.. Only the receipt's principal and relying party are entitled in v0.3 (§8).consumption. snapshot_ hash - Offline verification (v1 receipts, and the protocol layer of v2 receipts given the entitled material): given the receipt, the Delegus receipt public key, and the artifacts named by
did_,document_ hash credential_hash,profile_hash, andtrust_(retrievable by hash — §8), a third party can re-runconfig_ hash delegus-base-v1and confirmprotocol.result. Thetrustblock 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_atvalue asnowfor every time-dependent protocol check (P4, P5, P13). It MUST NOT use its own wall-clock time. Becauseevaluated_atis 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_at7. POST /revoke#
{ "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 /revokeMUST 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_) if it cannot establish current revocation state.UNAVAILABLE - Internal coordination MAY use any implementation that preserves the guarantee. Reference design: one Bitstring Status List per Principal (131,072 bits);
/revokesets the bit, re-signs the Principal's status-list credential, and incrementsstatus_in the internal revocation store; each engine node cachesver:{principal} {principal → (version, signed credential)}. - Ordering within
/verify(normative): before P6, the engine obtains the currentstatus_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 Proofver:{principal} jti. These are two distinct internal operations; in the worst case/verifyperforms two internal round trips. They MAY be optimized or coalesced (e.g. readingstatus_verat 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
statusListCredentialURLs serve the latest signed list withCache-, for third-party verifiers only.Control: max- age=30
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). An evidence object is locked from its first write, and a later receipt may reference an object first stored long before; Delegus therefore extends the retention of every evidence object a receipt references to at least 7 years after the end of that receipt's UTC day, and a retention date only ever moves later, never earlier. GET / returns the exact bytes.
v0.3: receipts are private by default. The artifacts that are public by nature (profile documents, trust-configuration descriptors, DID document snapshots, status-list credentials) stay retrievable by hash by anyone. A receipt itself, the consumption snapshot a v2 receipt pins, and the dependency disclosure package (§R.5) are retrieved only through authenticated access by the receipt's relying party or principal (their own key, a key of their organization, or Delegus staff): GET / for the receipt, and GET / for the dependency package and the consumption snapshot the receipt pins. A request by anyone else, for an unknown receipt, or for a receipt with nothing to disclose gets the same 404, so the answer never confirms that a receipt exists (§0.1). GET / does not serve consumption snapshots. Nothing is listed or searchable by fingerprint or transaction handle. Retention is the same 7 years.
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_.
9.2 Endpoint#
POST / — authenticated as the RP named in the receipt. Append-only.
{ "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_), reporter (RP DID), recorded_at. Corrections are new events with correction_. No edits, no deletions. Ordering is by recorded_at.
9.3 v0 vocabulary#
fulfilled · cancelled · dispute_opened · dispute_ (resolution: agent_) · chargeback_ · chargeback_ · 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_ (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 Nretired. Grants signed by N remain valid tovalidUntil; new Grants MUST use anactivekey. - Compromise:
POST /marks the key; every Grant it signed DENIES (T3) immediately.keys/ {id}/ compromise - 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) · 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.
Out of scope for v0.3: dependency requirements by kind rather than exact fingerprint · dependencies on receipts from another issuer · accepting foreign authority formats · a general disclosure capability, auditor roles, disclosure levels · money budgets (total, perPeriod) on action types other than commerce:purchase.
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; (v0.3) issuer not in good standing | T1, T2 (delegus-trust-v3: T1 also requires good standing; a Principal staff marked suspended or disputed gets a signed DENY ISSUER_NOT_TRUSTED) |
| 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 |
| TM-15 | (v0.3) Relying on a prior decision that was forged, altered or never issued | D2 (commitment opens to a receipt Delegus issued), D3 (signature) |
| TM-16 | (v0.3) Relying on a prior DENY as if it were an ALLOW | D4 |
| TM-17 | (v0.3) Splicing dependencies from unrelated transactions into one action | D5 |
| TM-18 | (v0.3) Relying on authority that expired, was revoked, whose Principal was suspended, or whose signing key was compromised after the prior decision | D6, D7 (CURRENT_AUTHORITY; standing, key state and status read before the replay write) |
| TM-19 | (v0.3) An actor omitting a dependency the relying party requires, or weakening its mode | D1; requires_hash records what was required |
| TM-20 | (v0.3) An actor inserting or altering declared dependencies after signing | P14 (relies_on is inside the signed action_hash) |
| TM-21 | (v0.3) Spending beyond a budget, including concurrent requests racing for the last headroom | P21, T7 (atomic decrement) |
| TM-22 | (v0.3) Budget spent by a request that is then refused | [consume] runs last, only when every other check is true |
| TM-23 | (v0.3) Discovering or enumerating receipts from a fingerprint or transaction handle | Commitments (§R.1), pairwise output-only handles (§R.3), no lookup by either, uniform 404 (§8) |
| TM-24 | (v0.3) Learning the plaintext authority or dependency evidence from a receipt alone | Pinned by hash and commitment; plaintext only beside the receipt and in the entitled package |
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 |
| NP-11 | (v0.3) Correlation by Delegus itself | Delegus issues every receipt in a transaction; handles and commitments limit outsiders only (§0.1) |
| NP-12 | (v0.3) An entitled party disclosing what it holds | A relying party or principal may share a receipt, its opening or its package; Delegus does not control that |
| NP-13 | (v0.3) Relying on PRIOR_DECISION as a statement about the present | By design it says nothing about now; use CURRENT_AUTHORITY |
| NP-14 | A low-entropy jti chosen by a faulty Agent | The format is checked (P8); entropy cannot be checked per value. Obligation on the Agent |
| NP-15 | (v0.3) Parties to a CURRENT_AUTHORITY dependency learning the dependency's Principal and observing its revocation status | Accepted by design: D7 needs the status-list credential and index, which name the Principal and let the holder re-read the bit later (§R.5). PRIOR_DECISION discloses neither |
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:
packages/core— pure Grant/Proof parsing, canonicalization, P-checks, receipt assembly and signing. No I/O; snapshots and clock injected.- Conformance suite — ALLOW plus one deterministic vector for every DENY reason, including
htunormalization, JCS,jtiformat, replay, revocation, and malformed inputs. The engine cannot merge until every vector passes. apps/api— Principal registry, domain verification, status lists, replay protection, evidence preservation, receipt persistence, Outcome Attestations.packages/sdk+ CLI —delegus.,org. create() agent.create(),grant.create(),agent.sign(),delegus., outcome reporting.verify() apps/demo— buyer/seller: authorized 42700 →ALLOW; revoke Grant; same action with a fresh Proof →AUTHORITY_. (A fresh Proof isolates revocation from replay; the replay path is covered separately by the conformance suite.)REVOKED
15.1 v0.3 conformance vectors#
A v0.3 implementation claims conformance only when every v0.3 vector passes and every v0.2 vector still passes unchanged under the v0.2 profiles. The v0.2 vectors are frozen: v0.3 adds vectors, it never edits one. The v0.3 set is published as a new version of the conformance package with its own vector-set hash, and it contains at least:
- Seller-side actions. An ALLOW for each of
commerce:quote,commerce:acceptandcommerce:fulfil, and a DENY for each of P18 and P19 oncommerce:quote/commerce:acceptand P20 oncommerce:fulfil. A v0.3 action type in a Grant evaluated under a v0.2 profile gives P1GRANT_MALFORMED. - Action fields (P15). A
relies_onentry withoutmode, with an unknown mode, or with an extra member; an emptyrelies_on; more than 8 entries; a repeated fingerprint; atransactionmember (handles are output only, §R.3). Each isACTION_.MALFORMED - Declaration versus requirement. The relying party requires a dependency the actor did not declare (D1); requires a different mode for a declared fingerprint (D1); inserts
relies_oninto the submitted Action after the actor signed it (P14). - Each dependency check, one vector per reason: a fingerprint that resolves to no issued receipt, including the right receipt under the wrong opening (D2); a dependency receipt whose signature fails (D3); a DENY dependency (D4); two dependencies from different internal transactions (D5); an expired dependency grant (D6); a revoked dependency grant, a dependency grant whose Principal is later suspended, and one whose signing key is later marked compromised (D7).
- The worked example. R1 ALLOW; its grant revoked; R2 declaring R1:
PRIOR_DECISION→ ALLOW with D6–D7"skipped";CURRENT_→ DENYAUTHORITY DEPENDENCY_; a control without the revoke → ALLOW.AUTHORITY_ REVOKED - Ordering. A request failing both P19 and a dependency check reports P19; a dependency failure after T5 has consumed the
jti(a second submission of the same Proof isPROOF_REPLAYED), as a pair of vectors that differ only in the recordedjti. Several entries. Each Dn is false if any entry is false,"skipped"never counts as false, andreasonis the first false Dn in §5.4 order, not the first failing entry: an entry failing D7 before an entry failing D2 reports D2; a later entry failing D4 leaves the earlier entry's results unchanged;PRIOR_DECISIONandCURRENT_entries mixed give ALLOW. Every vector with dependencies carries the per-entry results, andAUTHORITY relies_on_hashin its receipt equalssha256(JCS(those results)). Budgets. ALLOW with one spend; P21 exhausted (nothing spent); the last headroom lost at T7; the spend unavailable (SERVICE_); the snapshot missing; no spend on a DENY at P6, at T5 or at a dependency check. A DENY never calls the spend except when T7 itself decides it.UNAVAILABLE - Framing (§R). Fixed sets, like the v0.2
htu/JCS/jtisets: known-answer fingerprints, evidence commitments, authority commitments and capability-key commitments over published inputs (§R.6: each beside the plain hash it replaces); strict opening and handle decoding; and the domain separation among the four tags. - Size. A receipt with the maximum 8
relies_onentries stays within the signer's input limit (§6.4).
Every vector's bytes are generated by the reference implementation, not written by hand; a vector whose engine result contradicts its stated intent is refused, never written. Each carries the committed receipt, which verifies against the test receipt key and re-runs byte-identically from the vector's JSON alone. The v0.3 set ships in the conformance package beside the frozen v0.2 set, with its own runner (packages/).
The afternoon integration (relying party, complete):
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)#
- Minor units vs. decimals — spec: minor units.
- Proof window 60 s / 300 s.
- Replay scope per-RP.
- Grant max lifetime 90 days.
- Grant by reference as an alternative to by value.
loss_amountper event (spec) — confirm with first assurance design.- When reporter signatures become REQUIRED.
- Evidence retention period and storage model.
decisionvs.receiptas the primary API noun.- (v0.3, erratum candidate) "Integers only, no floats" is about the value: a number is a safe integer whatever its JSON literal form. For Actions, JCS makes
5e4and50000identical; a Grant is hashed as its exact signed bytes, so its literal form is pinned anyway. - (v0.3, erratum candidate)
jtiis 22–64 characters of the base64url alphabet, as the reference engine enforces; the v0.2 text says "22–64 ASCII". Clarify the text rather than loosen the engine. - (v0.3) Money budgets on
commerce:quoteandcommerce:accept.
v0.2 is frozen.