DelegusDocsv0.3 rc1

Docs/Relying on earlier decisions

Guides

Relying on earlier decisions

A deal between two companies' agents usually takes more than one step. One agent places an order; later, another agent accepts it. The later step often makes sense only because of the earlier one. In v0.3, the later step can say so: "this action depends on that earlier approval." It can say it in one of two ways. Either the earlier decision was genuinely made, or the authority behind it is still in force right now.

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.

PartyWhat it statesWhere it goes
The actor: the agent taking the later actionWhich earlier decisions this action depends on, and in which moderelies_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 /verifyWhich dependencies it insists on, and in which moderequires, in the /verify request body, next to the Action
DelegusWhether the declaration covers the requirement, and whether each declared dependency holdsChecks 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_ACTION_MISMATCH). 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_NOT_SUPPORTED.

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:

json
{
  "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_MALFORMED (check P15):

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 }.

json
{
  "grant": "<JWS>",
  "proof": "<JWS>",
  "action": { "type": "commerce:accept", "…": "…", "relies_on": [ { "fingerprint": "sha256:…", "mode": "CURRENT_AUTHORITY" } ] },
  "requires": [ { "fingerprint": "sha256:…", "mode": "CURRENT_AUTHORITY" } ]
}

The two modes#

ModeIn plain wordsWhat 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_AUTHORITY 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_AUTHORITY 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.

CheckHolds whenDENY reason
D1Every requires entry appears in relies_on with the same fingerprint and the same modeDEPENDENCY_NOT_DECLARED
D2The fingerprint resolves, inside Delegus, to a receipt Delegus issuedDEPENDENCY_COMMITMENT_MISMATCH
D3That receipt's signature verifies against a Delegus receipt key, and its body equals its signed payloadDEPENDENCY_RECEIPT_INVALID
D4Its decision was ALLOWDEPENDENCY_NOT_ALLOW
D5It belongs to the same deal (internal transaction) as the first dependency that resolvedDEPENDENCY_TRANSACTION_MISMATCH
D6CURRENT_AUTHORITY only: its Grant has not expired (now is before the earlier receipt's expires_at)DEPENDENCY_AUTHORITY_EXPIRED
D7CURRENT_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.checks.D1 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:

text
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

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.

text
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 issued

Who holds what:

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 /decisions/{receipt_id} 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.

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.

TimeWhat happensRelying party calling /verify
10:02R1: procurement-7's commerce:purchase for $18,400. ALLOW.ComputeCo's order system
10:05Acme revokes procurement-7's Grant.none (Acme calls /revoke)
10:07R2: sales-4's commerce:accept of that order, declaring relies_on: [R1].Acme's procurement system

What moves where:

  1. At 10:02, ComputeCo's order system receives R1 with its opening and, in the receipt, its transaction handle. It keeps both.
  2. It gives sales-4 the fingerprint from opening.fingerprint, never the nonce.
  3. sales-4 builds its commerce:accept Action with relies_on: [{ fingerprint, mode }], signs a Proof over it, and sends it to Acme's procurement system.
  4. Acme's procurement system calls /verify with sales-4's Grant, Proof and Action, exactly as received, plus its own requires.

The outcome at 10:07 depends on the mode:

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.

ts
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.3" 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.

ts
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.

ts
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_REVOKED

From a shell, the same call is delegus verify --v3 --requires '<json|file>' 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 --action.

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://status.delegus.ai/acme.example/1). 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.checks.D1 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#

CodeWhereMeaning
DEPENDENCY_NOT_DECLAREDD1A required dependency is missing from relies_on, or declared in a different mode
DEPENDENCY_COMMITMENT_MISMATCHD2The fingerprint does not resolve to a receipt Delegus issued
DEPENDENCY_RECEIPT_INVALIDD3The earlier receipt's signature or body does not verify
DEPENDENCY_NOT_ALLOWD4The earlier decision was a DENY
DEPENDENCY_TRANSACTION_MISMATCHD5The earlier receipt is in a different deal from the first dependency
DEPENDENCY_AUTHORITY_EXPIREDD6CURRENT_AUTHORITY: the earlier Grant has expired
DEPENDENCY_ISSUER_NOT_TRUSTEDD7CURRENT_AUTHORITY: the earlier Grant's Principal is suspended or disputed
DEPENDENCY_ISSUER_KEY_COMPROMISEDD7CURRENT_AUTHORITY: the key that signed the earlier Grant is marked compromised
DEPENDENCY_AUTHORITY_REVOKEDD7CURRENT_AUTHORITY: the earlier Grant has been revoked
REQUIREMENTS_MALFORMEDHTTP 400requires is malformed; nothing evaluated, no receipt, Proof not used up
REQUIREMENTS_NOT_SUPPORTEDHTTP 400requires was sent by a relying party not on delegus-base-v3

A malformed relies_on is the existing ACTION_MALFORMED (P15). The engine may also report SERVICE_UNAVAILABLE at D2 or D7 when it cannot read the state it needs; it fails closed.

Not in v0.3#