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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/d165-cost-policy-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@atrib/verify': minor
---

Add the [D165](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d165-orchestration-wiring-with-routing-and-cost-accounting) `cost_policy` capability-envelope sub-field (`model_tiers`, `max_tokens`) to `DelegationScope`, with `checkCostPolicy(policy, usage)` for caller-supplied usage facts. Signal-only per [§6.7.3](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#673-out-of-envelope-is-a-signal-not-invalidation); the record-only [§1.11.4](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#1114-verifier-walk) walk produces no `cost_policy` mismatch. New conformance case `walk-scope-cost-policy` with pinned usage vectors.
9 changes: 5 additions & 4 deletions CLAUDE.md

Large diffs are not rendered by default.

57 changes: 31 additions & 26 deletions DECISIONS.md

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions atrib-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1343,12 +1343,12 @@ A delegation certificate is a JSON object, canonicalized with JCS (RFC 8785, sam
"not_before": 1767225600000, // OPTIONAL; Unix ms; default 0 when absent
"principal_key": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w", // MUST; base64url 32-byte Ed25519 principal public key
"run_pubkey": "gTl3Dqh9F19Wo1Rmw0x-zMuNipG07jeiXfYPW4_Js5Q", // MUST; base64url 32-byte Ed25519 run public key
"scope": { "tool_names": ["search", "read_email"], "max_amount": { "currency": "USD", "value": 100 } }, // OPTIONAL; §6.7.1 capability envelope schema, verbatim
"scope": { "tool_names": ["search", "read_email"], "max_amount": { "currency": "USD", "value": 100 }, "cost_policy": { "model_tiers": ["standard"], "max_tokens": 500000 } }, // OPTIONAL; §6.7.1 capability envelope schema, verbatim
"signature": "iMouU-GLdyyISG2S6fRZEEYE5brlP1x6ycuidxY4dIhneSCEKMD_irR3hrHMDg9QfK_2LNct6xPuqm0yQj2pDA" // MUST; Ed25519 by principal_key
}
```

JCS lexicographic field order is exactly as listed: `cert_type` < `context_id` < `not_after` < `not_before` < `principal_key` < `run_pubkey` < `scope` < `signature`. Optional fields MUST be omitted, not null, when absent — presence/absence changes the canonical form and therefore the signature, mirroring the `session_token` rule ([§1.3](#13-canonical-serialization)). The `scope` object reuses the [§6.7.1](#671-identity-claim-extension) capability envelope schema **verbatim** (`tool_names`, `max_amount`, `counterparties`, `event_types`, `expires_at`); one schema, two carriers — directory-published (per-key, identity-claim cadence) and certificate-carried (per-run, issuance cadence). When `scope.expires_at` is present, the effective expiry is `min(not_after, scope.expires_at)`.
JCS lexicographic field order is exactly as listed: `cert_type` < `context_id` < `not_after` < `not_before` < `principal_key` < `run_pubkey` < `scope` < `signature`. Optional fields MUST be omitted, not null, when absent — presence/absence changes the canonical form and therefore the signature, mirroring the `session_token` rule ([§1.3](#13-canonical-serialization)). The `scope` object reuses the [§6.7.1](#671-identity-claim-extension) capability envelope schema **verbatim** (`tool_names`, `max_amount`, `counterparties`, `event_types`, `cost_policy`, `expires_at`); one schema, two carriers — directory-published (per-key, identity-claim cadence) and certificate-carried (per-run, issuance cadence). When `scope.expires_at` is present, the effective expiry is `min(not_after, scope.expires_at)`.

#### 1.11.2 Signing, Certificate Hash, and Depth

Expand Down Expand Up @@ -1389,7 +1389,7 @@ Given record `R` and available certificate set `C`, the walk is offline and dete
2. Select certificates `c ∈ C` with `c.run_pubkey === R.creator_key`. No covering certificate → **depth 0**: attribute to `R.creator_key` as today. If `R`'s context genesis (signed by `R.creator_key`) carries `delegation_cert_hash` but no valid covering certificate resolved, surface `delegation_unresolved: true` — signal, not invalidation, the [D113](DECISIONS.md#d113-unvalidated-informed_by-refs-are-omitted-by-default) posture. A covering certificate that is invalid per [§1.11.2](#1112-signing-certificate-hash-and-depth) is rejected as evidence: the walk reports its `cert_hash`, `cert_valid: false`, and the rejection error, and falls back to depth 0.
3. For a valid matching `c`, evaluate: `(c.not_before ?? 0) <= R.timestamp <= c.not_after` → `in_window`; `c.context_id` absent → `context_bound: null`, else `c.context_id === R.context_id` → `context_bound`; when the context genesis was signed by `R.creator_key` AND carries `delegation_cert_hash`, `genesis.delegation_cert_hash === cert_hash(c)` → `cert_bound`, otherwise `cert_bound: null` (the standing state for run keys that joined a multi-producer context, [§1.11.6](#1116-multi-producer-contexts)).
4. Consult the directory ([§6.3](#63-verifier-consultation-algorithm)) for `c.principal_key`, not the run key. Run keys never enter the directory; the expected lookup result for a run key is a non-membership proof, and a directory claim found *for the run key itself* is surfaced as the structural anomaly `run_key_in_directory: true`.
5. Scope check per [§6.7.2](#672-verifier-semantics) semantics against `c.scope`: `in_scope` with a `mismatches[]` list naming the failed constraints (`tool_names`, `event_types`, `max_amount`, `counterparties`). When the principal's directory envelope is available, `attenuation_ok` reports whether `c.scope` is a subset of it (a certificate granting what the principal's own envelope excludes sets `attenuation_ok: false`); when no directory envelope is supplied, `attenuation_ok` is `null`. All scope outputs are signals, never invalidation, per [§6.7.3](#673-out-of-envelope-is-a-signal-not-invalidation).
5. Scope check per [§6.7.2](#672-verifier-semantics) semantics against `c.scope`: `in_scope` with a `mismatches[]` list naming the failed constraints (`tool_names`, `event_types`, `max_amount`, `counterparties`, `cost_policy.model_tiers`, `cost_policy.max_tokens`). `cost_policy` constraints are evaluable only against caller-supplied usage facts per [§6.7.2](#672-verifier-semantics); the record-only walk produces no `cost_policy` mismatch. When the principal's directory envelope is available, `attenuation_ok` reports whether `c.scope` is a subset of it (a certificate granting what the principal's own envelope excludes sets `attenuation_ok: false`); when no directory envelope is supplied, `attenuation_ok` is `null`. All scope outputs are signals, never invalidation, per [§6.7.3](#673-out-of-envelope-is-a-signal-not-invalidation).
6. Revocation: scan for [§1.9](#19-key-rotation-and-revocation) `key_revocation` records retiring either the run key ([§1.11.5](#1115-run-key-revocation)) or the principal. A revoked principal cascades: its certificates are invalid as delegation evidence for records at `log_index >= R` per [§1.9.3](#193-verifier-semantics) semantics.

The verifier output is an optional `delegation` block:
Expand Down Expand Up @@ -1449,7 +1449,7 @@ Producers that opt in stamp `delegation_cert_hash` on the genesis record *only w

#### 1.11.11 Conformance

Implementations MUST pass all vectors in [`spec/conformance/delegation-certificates/`](spec/conformance/delegation-certificates/). The corpus covers six case families: certificate canonical form and signing (full/minimal optional-field forms, self-certificate, wrong-signer), the [§1.11.4](#1114-verifier-walk) verifier walk (valid, expired, scope mismatch as signal, wrong principal signature, run-key mismatch with `delegation_unresolved`), depth-0 byte-identity against [`spec/conformance/1.4/signing-vectors.json`](spec/conformance/1.4/signing-vectors.json), `delegation_cert_hash` lex-slotting with distinct signatures for presence vs. absence, run-key revocation extending [`spec/conformance/1.9/`](spec/conformance/1.9/) (authorized rule-3 revocation and the not-covering rejection), and the [D067](DECISIONS.md#d067-multi-producer-chain-composition-precedence-contract) `cert_bound: null` posture. The generator is `packages/log-dev/scripts/generate-conformance-delegation-certificates.ts`; the reference implementation is `packages/verify/test/conformance-delegation-certificates.test.ts`. Two implementations given the same record and certificate set MUST produce identical `delegation` blocks.
Implementations MUST pass all vectors in [`spec/conformance/delegation-certificates/`](spec/conformance/delegation-certificates/). The corpus covers six case families: certificate canonical form and signing (full/minimal optional-field forms, self-certificate, wrong-signer), the [§1.11.4](#1114-verifier-walk) verifier walk (valid, expired, scope mismatch as signal, cost-policy scope with pinned usage vectors, wrong principal signature, run-key mismatch with `delegation_unresolved`), depth-0 byte-identity against [`spec/conformance/1.4/signing-vectors.json`](spec/conformance/1.4/signing-vectors.json), `delegation_cert_hash` lex-slotting with distinct signatures for presence vs. absence, run-key revocation extending [`spec/conformance/1.9/`](spec/conformance/1.9/) (authorized rule-3 revocation and the not-covering rejection), and the [D067](DECISIONS.md#d067-multi-producer-chain-composition-precedence-contract) `cert_bound: null` posture. The generator is `packages/log-dev/scripts/generate-conformance-delegation-certificates.ts`; the reference implementation is `packages/verify/test/conformance-delegation-certificates.test.ts`. Two implementations given the same record and certificate set MUST produce identical `delegation` blocks.

#### 1.11.12 What This DOES NOT Cover

Expand Down Expand Up @@ -4300,12 +4300,17 @@ The [§6.1](#61-identity-claim-format) identity claim format gains an OPTIONAL `
"https://atrib.dev/v1/types/tool_call",
"https://atrib.dev/v1/types/observation",
],
"cost_policy": {
// optional compute-spend scope for delegated runs (D165)
"model_tiers": ["economy", "standard"], // optional allowlist of host-defined tier labels
"max_tokens": 500000, // optional total token budget for the certified run
},
"expires_at": 1761000000000, // optional; envelope rotates with the identity claim
},
}
```

All capability sub-fields are individually optional. A claim with `capabilities: {}` declares no scope (equivalent to omitting the field). A claim with some sub-fields and not others applies only the present constraints.
All capability sub-fields are individually optional. A claim with `capabilities: {}` declares no scope (equivalent to omitting the field). A claim with some sub-fields and not others applies only the present constraints. `cost_policy` scopes what a key may spend rather than what it may do: `model_tiers` is an allowlist of host-defined tier labels (free-form strings, the `tool_names` idiom) and `max_tokens` caps total token spend. Its primary carrier is the [§1.11](#111-delegation-certificates) certificate scope, where a principal grants a delegated run a compute budget; the protocol records the grant and never enforces it.

#### 6.7.2 Verifier semantics

Expand All @@ -4316,6 +4321,7 @@ A verifier that has resolved a record's `creator_key` to an identity claim with
- If `tool_names` is present, the record's `tool_name` MUST be in the list (for tool_call records). The verifier MAY use a `tool_name` field disclosed on the record or caller-supplied facts resolved from the local record body or upstream protocol event. If no tool name is available, the verifier MUST flag the check as `unresolvable: true`.
- If `event_types` is present, the record's `event_type` URI MUST be in the list.
- For transaction records, if `max_amount` and/or `counterparties` are present, the verifier MUST resolve the transaction amount and counterparty from the protocol-specific transaction event the record commits to (per [§1.7](#17-transaction-event-hooks)'s payment-protocol definitions: ACP order envelope, UCP envelope, x402 PAYMENT-RESPONSE header, MPP Payment-Receipt, AP2 CheckoutReceipt or PaymentReceipt, a2a-x402 receipts). The resolved amount MUST NOT exceed `max_amount`; the resolved counterparty MUST be in the `counterparties` allowlist. When the protocol-specific event is not available out-of-band or through caller-supplied resolved facts, the verifier MUST flag the check as `unresolvable: true` rather than passing or failing silently.
- If `cost_policy` is present, the constraint is evaluable only against caller-supplied usage facts (model tier label, tokens spent): signed records carry neither, since spend accounting lives in local sidecar and join-record content per the [D142](DECISIONS.md#d142-orchestration-topology-baton-pass-and-join-records-as-attest-conventions) orchestration conventions. With usage facts, the claimed tier MUST be in `model_tiers` and claimed spend MUST NOT exceed `max_tokens`; mismatches are named `cost_policy.model_tiers` / `cost_policy.max_tokens`. Without usage facts the check produces no mismatch (nothing checkable is claimed).
- If `expires_at` is present and the record's timestamp is after it, the envelope is expired (treated as having no constraint and flagged separately).
3. Surface the result as `capability_check: { envelope: CapabilityEnvelope | null, in_envelope: bool, mismatches: string[], unresolvable: bool }` on the verification output.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ interface CapabilityScope {
counterparties?: string[]
event_types?: string[]
expires_at?: number
cost_policy?: { model_tiers?: string[]; max_tokens?: number }
}

/** Delegation certificate per §1.11.1. Optional fields omitted (not null) when absent. */
Expand Down Expand Up @@ -493,6 +494,76 @@ async function main(): Promise<void> {
},
})

// ── Case: walk-scope-cost-policy ──────────────────────────────────
const ctxC9 = 'c9'.repeat(16)
const certC9 = await signCertificate(
{
cert_type: 'atrib/delegation-cert/v1',
context_id: ctxC9,
not_after: CERT_NOT_AFTER,
not_before: CERT_NOT_BEFORE,
principal_key: principalKey,
run_pubkey: runKey,
scope: {
tool_names: ['search', 'read_email'],
cost_policy: { model_tiers: ['economy', 'standard'], max_tokens: 500_000 },
},
},
PRINCIPAL_SEED,
)
const walkCostPolicyRecord = await runKeyGenesis(
ctxC9,
'c9',
REFERENCE_TIME_MS + 60_000,
'search',
certC9,
RUN_SEED,
runKey,
)
writeCase('walk-scope-cost-policy', {
name: 'walk-scope-cost-policy',
spec_section: '1.11',
description:
'A certificate whose scope carries the D165 cost_policy sub-field (model_tiers allowlist, max_tokens budget). From the record alone the walk reports no cost_policy mismatch: signed records carry neither model tier nor token spend, so cost_policy is evaluable only against caller-supplied usage facts (checkCostPolicy, §6.7.2). The usage_vectors pin that evaluation: within-grant usage is in scope; a tier outside the allowlist plus spend over the budget produce both mismatches. Signals, never invalidation (§6.7.3): the record signature and depth-1 resolution are unaffected.',
input: {
record: walkCostPolicyRecord,
genesis_record: walkCostPolicyRecord,
certificates: [certC9],
usage_vectors: [
{
name: 'within-grant',
usage: { model_tier: 'standard', tokens_spent: 120_000 },
expected: { in_scope: true, mismatches: [] },
},
{
name: 'tier-and-budget-exceeded',
usage: { model_tier: 'premium', tokens_spent: 500_001 },
expected: {
in_scope: false,
mismatches: ['cost_policy.model_tiers', 'cost_policy.max_tokens'],
},
},
],
},
expected: {
record_signature_valid: true,
record_hash: recordHash(walkCostPolicyRecord),
signal_not_block: true,
delegation: {
depth: 1,
principal_key: principalKey,
cert_hash: certHash(certC9),
cert_valid: true,
in_window: true,
context_bound: true,
cert_bound: true,
scope_check: { in_scope: true, attenuation_ok: null, mismatches: [] },
revoked: false,
errors: [],
},
},
})

// ── Case: walk-wrong-principal-signature ──────────────────────────
const ctxB4 = 'b4'.repeat(16)
const certB4Bad = await signCertificate(
Expand Down Expand Up @@ -1059,6 +1130,7 @@ async function main(): Promise<void> {
'walk-valid',
'walk-expired',
'walk-scope-mismatch',
'walk-scope-cost-policy',
'walk-wrong-principal-signature',
'walk-run-key-mismatch',
'depth0-identity',
Expand All @@ -1085,7 +1157,7 @@ async function main(): Promise<void> {
rogue_pubkey: rogueKey,
},
note:
'Six case families: certificate canonical form + signing (4), verifier walk (5), ' +
'Six case families: certificate canonical form + signing (4), verifier walk (6), ' +
'depth-0 byte-identity against spec/conformance/1.4/signing-vectors.json (1), ' +
'delegation_cert_hash genesis lex-slotting (1), run-key revocation extending ' +
'spec/conformance/1.9/ (2), D067 multi-producer cert_bound: null posture (1). ' +
Expand Down
Loading
Loading