Release candidate. This describes v0.3-rc1, the current release candidate. New and existing relying parties are on it (profile
delegus-base-v3). It is not frozen: the commitment construction is awaiting an external cryptography review, and the v0.2 specification stays the normative text until v0.3 is frozen. Details can still change.
"Relying on" here is a protocol idea only: an earlier Delegus decision was a declared input to a later one. It says nothing about legal reliance, whether a deal can be enforced, or what either company owes the other.
Who says what#
Three parties are involved in the later step, and each has one job.
| Party | What it states | Where it goes |
|---|---|---|
| The actor: the agent taking the later action | Which earlier decisions this action depends on, and in which mode | relies_on, inside the Action. The actor's Proof signs the Action, so the declaration is signed too. |
The relying party: the service receiving the later action and calling /verify | Which dependencies it insists on, and in which mode | requires, in the /verify request body, next to the Action |
| Delegus | Whether the declaration covers the requirement, and whether each declared dependency holds | Checks D1 to D7, recorded in the signed receipt |
Only the actor declares a dependency. The relying party cannot add, remove or change an entry in relies_on: the Action is covered by the actor's Proof, so any edit makes the Proof fail its own check (P14, PROOF_). What the relying party can do is state what it requires, and Delegus checks that the actor's declaration covers it (D1).
Both fields are part of v0.3 only. A relying party has to be on the v0.3 rules (the profile delegus-base-v3) to use them; any other relying party that sends requires gets HTTP 400 REQUIREMENTS_.
The declaration: relies_on#
relies_on is an optional field on every v0.3 action type. It is a list of 1 to 8 entries. Each entry has exactly two members:
fingerprint: names the earlier receipt without revealing it (see Fingerprints and openings). Writtensha256:followed by 64 lowercase hex characters.mode:PRIOR_DECISIONorCURRENT_.AUTHORITY
{
"type": "commerce:accept",
"resource": "compute/gpu-hours/order-7731",
"amount": 1840000,
"currency": "USD",
"relies_on": [
{ "fingerprint": "sha256:…", "mode": "CURRENT_AUTHORITY" }
]
}The rules are strict, and a broken declaration is refused as ACTION_ (check P15):
- There is no default mode. An entry without
mode, with an unknown mode, or with any other member is malformed. - An empty list is malformed; leave the field out instead. So are more than 8 entries and a repeated fingerprint.
- The order matters. Delegus checks the entries in the order they are listed.
- An Action never carries a transaction handle. A
transactionmember, or any member not defined for the action type, is malformed. Handles are something Delegus gives out, never something it accepts (see Transaction handles).
The requirement: requires#
The relying party sends requires in the /verify body. It has exactly the same shape as relies_on: 1 to 8 distinct entries, each exactly { fingerprint, mode }.
{
"grant": "<JWS>",
"proof": "<JWS>",
"action": { "type": "commerce:accept", "…": "…", "relies_on": [ { "fingerprint": "sha256:…", "mode": "CURRENT_AUTHORITY" } ] },
"requires": [ { "fingerprint": "sha256:…", "mode": "CURRENT_AUTHORITY" } ]
}- A malformed
requiresis refused before anything is evaluated: HTTP 400REQUIREMENTS_, no receipt, and the Proof is not used up.MALFORMED - A requirement names an exact fingerprint. Requiring a dependency by kind ("some purchase in this deal") is future work.
- The receipt pins what you sent as
requires_hash, so you can later show exactly which requirements you stated.
The two modes#
| Mode | In plain words | What Delegus checks |
|---|---|---|
PRIOR_DECISION | "The earlier decision was really made." | The earlier receipt is genuine, it was an ALLOW, and it belongs to the same deal. It says nothing about now. |
CURRENT_AUTHORITY | "…and the authority behind it is still in force." | All of the above, and the Grant the earlier decision rested on has not expired and has not been revoked, at the moment of this check. |
The difference matters when something changes between the two steps. If the earlier Grant is revoked in between, a PRIOR_DECISION dependency still holds (the earlier decision was real, and a revoke does not rewrite it), while a CURRENT_ dependency fails. Reading PRIOR_DECISION as a statement about the present is a misuse the specification names explicitly (threat model, NP-13).
What each mode discloses also differs; see What a CURRENT_ dependency reveals.
The dependency checks, D1 to D7#
D1 runs once. D2 to D7 run for each relies_on entry, in the order the entries are listed. With neither relies_on nor requires, all seven are true and change nothing.
| Check | Holds when | DENY reason |
|---|---|---|
| D1 | Every requires entry appears in relies_on with the same fingerprint and the same mode | DEPENDENCY_NOT_DECLARED |
| D2 | The fingerprint resolves, inside Delegus, to a receipt Delegus issued | DEPENDENCY_COMMITMENT_MISMATCH |
| D3 | That receipt's signature verifies against a Delegus receipt key, and its body equals its signed payload | DEPENDENCY_RECEIPT_INVALID |
| D4 | Its decision was ALLOW | DEPENDENCY_NOT_ALLOW |
| D5 | It belongs to the same deal (internal transaction) as the first dependency that resolved | DEPENDENCY_TRANSACTION_MISMATCH |
| D6 | CURRENT_AUTHORITY only: its Grant has not expired (now is before the earlier receipt's expires_at) | DEPENDENCY_AUTHORITY_EXPIRED |
| D7 | CURRENT_AUTHORITY only, three things about the earlier Grant, in this order: its Principal is in good standing (as T1 asks of the action's own Principal); the key that signed it is not marked compromised (as T3); and its revocation bit is 0 in its Principal's status-list credential (as P6). All read in the step before P6. | DEPENDENCY_ISSUER_NOT_TRUSTED / DEPENDENCY_ISSUER_KEY_COMPROMISED / DEPENDENCY_AUTHORITY_REVOKED |
For PRIOR_DECISION, D6 and D7 are "skipped". A dependency always names a receipt Delegus itself issued; receipts from other issuers are outside v0.3.
A D2 failure is also what you get when the fingerprint is right in form but was made with the wrong opening, or names nothing Delegus issued.
In the receipt, protocol. to D7 combine all entries: each is false if any entry failed it. The per-entry results come back beside the receipt (see Receipts, v0.3).
Where they run, and what that means for you#
The full v0.3 order is:
P1 → T1 → T2 → T3 → P2 → P3 → P4 → P5
→ [look up every relies_on fingerprint; read revocation state for this Grant
and for the Grant behind every CURRENT_AUTHORITY dependency]
→ P6 → P7 → … → P14
→ [record the Proof as used]
→ T5 → P15 → … → P20
→ P21
→ D1 → (per entry) D2 → D3 → D4 → D5 → D6 → D7
→ [spend budget, only if every check above is true]
→ T7- The actor's own reasons come first.
reasonis the firstfalsein this order, entries taken in order. A request that fails both an amount check (P19) and a dependency check reports P19. - The dependency checks run only when everything before them passed, including P21 (budget headroom). Otherwise they are
"skipped". - Revocation is read before anything is written. Every revocation read, including a dependency's, happens before the Proof is recorded as used, so a revoke that completed before the request is seen.
- A dependency failure uses up the Proof. D1 to D7 run after T5 has recorded the Proof's
jti(its one-time nonce). To try again, the actor needs a fresh Proof; sending the same one again isPROOF_REPLAYED. - A dependency failure spends no budget. The spend (T7) is the very last step and runs only when every other check is true.
- Looking up a fingerprint reads only Delegus's own storage.
/verifystill makes no outside network call.
Fingerprints and openings#
A relies_on entry names an earlier receipt by a fingerprint, never by an address or an id. The fingerprint is a hash over the earlier receipt together with a 32-byte random secret called its opening. Without the opening, nobody can tell which receipt a fingerprint names, even someone holding the receipt.
fingerprint = SHA-256( u8(len(tag)) ‖ tag ‖ opening ‖ u64be(len(R)) ‖ R )
tag = "delegus:relies-on:v1" (20 bytes, ASCII)
opening = 32 random bytes, generated by Delegus
R = the earlier receipt's compact JWS, exactly as issuedWho holds what:
- Delegus generates the opening when it issues an ALLOW receipt and keeps it. A DENY receipt gets no opening.
- The earlier receipt's relying party receives it once, in the
/verifyresponse, asopening: { "fingerprint": "sha256:…", "nonce": "<43 characters>" }. The nonce is the 32 bytes in unpadded base64url: exactly 43 characters, and any other form is invalid. - The actor of the later step receives only the fingerprint, from the earlier relying party, by whatever channel the two already use.
Pass on the fingerprint. Never forward the nonce. The fingerprint is all a later actor needs to declare the dependency, and it gives no access to the receipt. The nonce is what turns the fingerprint back into "this is that receipt"; keep it with the receipt.
Store the whole opening object when you receive it. No route returns it again: GET / carries no fingerprint or nonce, and the only place it reappears is inside a later receipt's disclosure package, for a caller entitled to this receipt. Delegus resolves fingerprints internally (D2); a fingerprint is never looked up by anyone else, and nothing is listed or searchable by fingerprint.
Transaction handles#
Delegus groups the decisions of one deal into an internal transaction. Its identifier never leaves Delegus. What a relying party sees instead is a transaction handle: a short random value that lets it tell which of its own receipts belong to the same deal.
- ALLOW only. Every ALLOW receipt under v0.3 carries a handle in its signed
transactionfield: 16 random bytes, written as exactly 22 characters of unpadded base64url. A DENY receipt carries no handle and no opening. - Per relying party. The same relying party acting again in the same deal receives the same handle. A different relying party receives a different one. Handles held by different companies cannot be linked by comparing them.
- Output only. A handle is never accepted as input. The Action has no
transactionmember and no request field takes one. - How a deal forms. An action with no
relies_onstarts a new deal (on ALLOW, its receipt is the first in it). An action that declaresrelies_onjoins the deal of its first dependency that resolves. If the declared dependencies belong to different deals, D5 fails. - Correlation, not discovery. A handle lets you match your own receipts. It gives no way to find, list or fetch anyone else's.
Delegus issues every receipt in a deal, so Delegus itself can see which receipts belong together. Handles and fingerprints limit what outsiders can link; they do not limit Delegus (threat model, NP-11).
Worked example: 10:02, 10:05, 10:07#
Example companies. Acme's buying agent, procurement-7, orders 400 GPU-hours from ComputeCo. ComputeCo's sales agent, sales-4, accepts the order.
| Time | What happens | Relying party calling /verify |
|---|---|---|
| 10:02 | R1: procurement-7's commerce:purchase for $18,400. ALLOW. | ComputeCo's order system |
| 10:05 | Acme revokes procurement-7's Grant. | none (Acme calls /revoke) |
| 10:07 | R2: sales-4's commerce:accept of that order, declaring relies_on: [R1]. | Acme's procurement system |
What moves where:
- At 10:02, ComputeCo's order system receives R1 with its
openingand, in the receipt, its transaction handle. It keeps both. - It gives sales-4 the fingerprint from
opening., never the nonce.fingerprint - sales-4 builds its
commerce:acceptAction withrelies_, signs a Proof over it, and sends it to Acme's procurement system.on: [{ fingerprint, mode }] - Acme's procurement system calls
/verifywith sales-4's Grant, Proof and Action, exactly as received, plus its ownrequires.
The outcome at 10:07 depends on the mode:
- sales-4 declared
PRIOR_DECISION, and Acme requiredPRIOR_DECISION: R2 is ALLOW, as long as sales-4's own checks pass. R1 was a genuine ALLOW at 10:02; the revoke at 10:05 does not rewrite it. D6 and D7 are"skipped". - Both said
CURRENT_: R2 is DENYAUTHORITY DEPENDENCY_(D7). The authority R1 rested on was withdrawn before the acceptance. sales-4's Proof is now used up.AUTHORITY_ REVOKED - Acme required
CURRENT_but sales-4 declaredAUTHORITY PRIOR_DECISION: R2 is DENYDEPENDENCY_(D1). The declaration has to match the requirement in fingerprint and in mode.NOT_ DECLARED - Without the revoke, the
CURRENT_case is ALLOW.AUTHORITY
In every case, R1 stays an accurate, unchanged record of the 10:02 decision, and the revoke is its own record. sales-4's own authority (did ComputeCo authorize sales-4 to accept this order at this price?) is checked in the same /verify call by P1 to P21, and it is reported first.
Handles in this example: R2, if ALLOW, joins R1's deal. Acme's procurement system receives its own handle for that deal, different from the one ComputeCo's order system holds. If ComputeCo's order system later verifies another action in the same deal, it receives the same handle it got with R1.
Acme's procurement system was not R1's relying party, so it learns R1's fingerprint out of band: ComputeCo's order system, or the agent it hands it to, passes the fingerprint on (never the nonce). Delegus offers no lookup by fingerprint, by design: verifiable, not discoverable (§0.1). If Acme simply copied the fingerprint from sales-4's Action, its requirement would pin the mode but not which earlier receipt, so agree the expected fingerprint with ComputeCo's system before the acceptance arrives.
In code#
The code below uses the v0.3 release-candidate SDK. Names are the SDK's own.
ComputeCo's order system (R1): verify, keep the opening, pass on the fingerprint.
import { Delegus, dependencyOn } from "@delegus/sdk";
const delegus = new Delegus({ apiKey: process.env.DELEGUS_API_KEY, publicBaseUrl: "https://orders.computeco.example" });
const r1 = await delegus.verify({
grant: req.header("Delegus-Grant"),
proof: req.header("Delegus-Proof"),
action: { type: "commerce:purchase", resource: "compute/gpu-hours/order-7731", amount: 1840000, currency: "USD" },
protocol: "v0.3",
});
if (r1.decision !== "ALLOW") return res.status(403).json({ reason: r1.reason });
// Keep the whole response with the order: receipt, authority and its opening, and `opening`.
await orders.save("order-7731", r1);
// What sales-4 needs: the fingerprint and a mode. dependencyOn never includes the nonce.
const dependency = dependencyOn(r1, "CURRENT_AUTHORITY"); // { fingerprint, mode }verify with protocol: "v0. checks the response against the receipt before returning it and throws ResponseIntegrityError if anything does not match (see SDK and CLI, v0.3). dependencyOn throws if the response has no opening, which is the case for every DENY.
sales-4 (R2): declare the dependency in the Action it signs.
import { actionsV3 } from "@delegus/sdk";
const action = actionsV3.accept({
resource: "compute/gpu-hours/order-7731",
amount: 1840000,
currency: "USD",
relies_on: [dependency], // { fingerprint, mode: "CURRENT_AUTHORITY" }; there is no default mode
});
const headers = await sales4.headers({
grant: computecoGrant,
audience: "did:web:procurement.acme.example",
method: "POST",
url: "https://procurement.acme.example/acceptances",
action,
});actionsV3 validates the Action with the engine's v0.3 rules (P15) before anything is signed, and it has no way to add a transaction handle. sales4 is an Agent.
Acme's procurement system (R2): state what it requires.
const r2 = await delegus.verify({
grant: req.header("Delegus-Grant"),
proof: req.header("Delegus-Proof"),
action: req.body.action, // exactly what sales-4 signed, or P14 fails
protocol: "v0.3",
requires: [{ fingerprint: expectedFingerprint, mode: "CURRENT_AUTHORITY" }],
});
if (r2.decision !== "ALLOW") return res.status(403).json({ reason: r2.reason }); // here: DEPENDENCY_AUTHORITY_REVOKEDFrom a shell, the same call is delegus verify - with --grant, --proof and --action; it exits 0 for ALLOW and 2 for DENY. There is no CLI command yet that builds a v0.3 Action with relies_on; write the Action JSON yourself and sign it with delegus proof sign -.
What a CURRENT_AUTHORITY dependency reveals#
To check D7, Delegus reads the revocation list of the earlier Grant's company. Anyone entitled to the later receipt (its relying party or its principal) can later ask for the material to re-check that step, and it includes that revocation list and the Grant's position in it. The list's address names the company (for example https:/). So those parties learn which company issued the earlier Grant, even if they are not entitled to the earlier receipt itself, and because revocation lists are public they can keep watching whether that Grant is revoked afterwards. The list also carries the bits of that company's other Grants, without saying which is which.
A PRIOR_DECISION entry reveals none of this. An actor that does not want its counterparties to learn who issued the earlier Grant, or to follow its revocation, declares PRIOR_DECISION (threat model, NP-15).
What a receipt shows about its dependencies#
Anyone holding only the later receipt can check it is authentic, see from relies_on_hash that dependencies were evaluated, and read the combined results protocol. to D7. The entries themselves (fingerprints, modes, per-entry results) are not in the receipt: they come back beside it, to its relying party, and the receipt pins them by hash. Even with the entries, a fingerprint does not reveal which receipt it names, and nothing in the receipt gives access to the earlier receipts or their evidence. Checking each dependency result takes the dependency disclosure package, which only the later receipt's own parties can fetch. Receipts, v0.3 covers what is public and what is entitled.
New reason codes and errors#
| Code | Where | Meaning |
|---|---|---|
DEPENDENCY_NOT_DECLARED | D1 | A required dependency is missing from relies_on, or declared in a different mode |
DEPENDENCY_COMMITMENT_MISMATCH | D2 | The fingerprint does not resolve to a receipt Delegus issued |
DEPENDENCY_RECEIPT_INVALID | D3 | The earlier receipt's signature or body does not verify |
DEPENDENCY_NOT_ALLOW | D4 | The earlier decision was a DENY |
DEPENDENCY_TRANSACTION_MISMATCH | D5 | The earlier receipt is in a different deal from the first dependency |
DEPENDENCY_AUTHORITY_EXPIRED | D6 | CURRENT_AUTHORITY: the earlier Grant has expired |
DEPENDENCY_ISSUER_NOT_TRUSTED | D7 | CURRENT_AUTHORITY: the earlier Grant's Principal is suspended or disputed |
DEPENDENCY_ISSUER_KEY_COMPROMISED | D7 | CURRENT_AUTHORITY: the key that signed the earlier Grant is marked compromised |
DEPENDENCY_AUTHORITY_REVOKED | D7 | CURRENT_AUTHORITY: the earlier Grant has been revoked |
REQUIREMENTS_MALFORMED | HTTP 400 | requires is malformed; nothing evaluated, no receipt, Proof not used up |
REQUIREMENTS_NOT_SUPPORTED | HTTP 400 | requires was sent by a relying party not on delegus-base-v3 |
A malformed relies_on is the existing ACTION_ (P15). The engine may also report SERVICE_ at D2 or D7 when it cannot read the state it needs; it fails closed.
Not in v0.3#
- Dependencies on receipts from any issuer other than Delegus.
- Requirements by kind rather than by exact fingerprint.
- Any way for a receipt's parties to grant someone else access to its reasoning. Only the receipt's relying party and principal are entitled in v0.3.