Skip to content

feat(identity): one delegation-chain rule for demand-side grants - #601

Merged
bokelley merged 1 commit into
mainfrom
bokelley/delegation-chain-vectors-beta3
Nov 2, 2026
Merged

bokelley merged 1 commit into
mainfrom
bokelley/delegation-chain-vectors-beta3

Conversation

@bokelley

@bokelley bokelley commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Summary

This aligns identity.GrantEvaluator with adcontextprotocol/adcp#8473 (an erratum to ratified R6, shipped in AdCP v3.3.0-beta.3) and runs its 48 cross-SDK delegation-chain vectors. It ports adcontextprotocol/adcp-client#3452.

  • Implicit authority: account.operator and the signer's operator each only need to be the brand or its house, in either combination. Mixed cases are now implicit, for example a brand-operated account where the house's own agent signs.
  • One chain rule: a chain has at most two grants from the house's record to the signer's operator. Every grant must cover the claim, and account.operator must be the brand, the house, or the first grant's grantee. This replaces the three fixed shapes. Brand- and house-operated accounts can now use two-grant chains, and the brand's own record can supply the second grant.
  • Several candidate chains: intermediary records are read in parallel, at most 8 at a time. An unreadable intermediary is an outage (SERVICE_UNAVAILABLE) only if no other chain authorizes. The authorization expires with the latest-expiring chain that authorizes.
  • Exclusivity: exclusive_grants: true on the house's record makes grants[] the complete list whatever the scope. An uncovered claim gets grant_absent, and SellerAuthorization is never called. An exclusive list with no grants authorizes no one. This reverses the per-scope reading. Only the house's flag counts.
  • New: GrantAuthorization.Source (implicit, grant, seller, legacy) reports what authorized a claim. The vectors need it to tell implicit from authorized.

Tests

  • identity/grants_vectors_test.go runs all 48 vectors from static/test-vectors/delegation-chains.json. They're vendored byte for byte from v3.3.0-beta.3, which is identical to the #8473 head that adcp-client vendors, and pinned by sha256 in testdata/delegation-chains/UPSTREAM.json. The vectors use .example names, which have no registrable domain, so the harness maps them to registrable placeholders, as adcp-client does.
  • Two tests encoded the per-scope exclusivity rule and are updated: TestGrantExclusiveAndHooks and TestNonRegistrableGranteeStaysExclusive (renamed).
  • New: TestUnreadableIntermediaryIsAnOutageOnlyIfNoOtherChainAuthorizes.
  • I checked that the vectors can fail by reverting the account.operator rule and record-wide exclusivity on purpose: 9 vectors failed.
  • go test -race ./identity ./signing passes, and golangci-lint run ./identity/... (go1.26.2) reports 0 issues.

docs/agent-identity-3-3-migration.md now describes the new rule.

🤖 Generated with Claude Code

Align GrantEvaluator with adcontextprotocol/adcp#8473, the erratum to
ratified R6 now in AdCP 3.3.0-beta.3, and run its cross-SDK
delegation-chain vectors.

- Implicit authority: account.operator and the signer's operator are
  each the brand or its house, in either combination.
- One chain rule: at most two grants from the house's record to the
  signer's operator, every grant covering the claim, with
  account.operator the brand, the house, or the first grant's grantee.
  Brand- and house-operated accounts can use two-grant chains, and the
  brand's own record can supply the second grant. An unreadable
  intermediary is an outage only if no other chain authorizes.
- exclusive_grants on the house's record makes grants[] the complete
  list whatever the scope. This replaces the per-scope reading.
- GrantAuthorization.Source reports what authorized a claim.

The 48 vectors are vendored byte for byte from v3.3.0-beta.3 and pinned
by checksum in testdata/delegation-chains/UPSTREAM.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve.

Checked:

  • GrantEvaluator aligned with adcp#8473 (erratum to ratified R6, experimental in 3.3); 48 cross-SDK delegation-chain vectors pinned by sha256.
  • grants.go rewrite (hunk +326..498) is fail-closed: authorization expiry defaults to zero time so now.Before(expiry) denies on absence; firsts() enforces account.operator ∈ {brand, house, first-grant grantee}; isRegistrableDomain guard applied at both call sites (no fail-open); authorizing chain decided before outage so unreadable intermediary yields SERVICE_UNAVAILABLE only when no other chain authorizes; record-wide exclusive_grants gates grant_absent vs seller discretion off the house flag.
  • Operational readiness: bounded fetchIntermediaries (8-way sem), per-fetch trustDiscoveryTimeout=10s, wg.Wait + mutex-guarded map, go test -race ./identity passes.
  • No schema/types_gen.go, tmproto, or identity-agent surface touched; additive Source field; feat(identity) correct.

Non-blocking follow-up noted by reviewer: intermediary fetch count grew from 1 to N distinct first-grantees — cold-cache latency on a pathological house record, acceptable given caching and the 10s per-fetch bound.

No critical/high/medium findings. gated_paths false; no author team gate; high_risk false. Falls through to row 9.

@bokelley
bokelley merged commit 1b1eaf2 into main Nov 2, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant