[AI assisted] Plan G core: AnchorResolver abstraction + FQDN/did:web/LEI profiles#15
Open
scourtney-godaddy wants to merge 5 commits into
Open
[AI assisted] Plan G core: AnchorResolver abstraction + FQDN/did:web/LEI profiles#15scourtney-godaddy wants to merge 5 commits into
scourtney-godaddy wants to merge 5 commits into
Conversation
Plan G Slice 1: lift FQDN-specific identity handling into a typed AnchorResolver port + IdentityClaim domain type, behind a per-profile adapter package. Subsequent slices add did:web (Slice 2), LEI stub (Slice 3), and the multi-profile registry facade (Slice 4) without disturbing the existing FQDN registration flow. Domain: - AnchorType enum with three values (fqdn, did, lei) matching ANS-0 §3 in the proposal at docs/proposals/2026-05-16-spec-skeletons/ ans-0-identity-anchor.md. The enum is intentionally closed: a fourth top-level type is an ANS-0 amendment, not a profile addition. - IdentityClaim struct shaped to match the TypeScript signature in the proposal's architectural-index skeleton §3.1. PublicKeyJWK as []byte rather than a parsed key keeps the package free of a JWK library dependency at this layer; downstream consumers parse on demand. - IdentityClaim.Validate() is a defense-in-depth check at API boundaries; AnchorResolver implementations are the authoritative source for the four ANS-0 §4.2 verification checks. - IdentityClaim.FQDN() returns the canonical FQDN when the anchor is type fqdn, empty otherwise; callers needing the FQDN regardless of anchor type continue to read AgentRegistration.AgentHost. Port: - AnchorResolver interface with Resolve(ctx, input) and SupportedProfiles(). Higher-spec code (RegistrationService, VerificationWorker, TrustIndex once those refactor lands) reads identity through this interface only. FQDN adapter: - internal/adapter/anchor/fqdn implements AnchorResolver for the ANS-0 §0.A FQDN profile. Slice 1 keeps the resolver shape-only: ResolveWithKey takes a pre-validated public key from the caller (registration service) and shapes the IdentityClaim. Resolve itself returns FQDN_RESOLVE_NOT_IMPLEMENTED to flag the migration boundary; Slice 4 absorbs DNS resolution + cert chain validation into this package. - Canonicalization rules: lowercase, strip trailing dot, RFC 1123 hostname constraints, label LDH-only, no underscore-prefixed labels (ANS-3 reserves _-prefixed names for the registry's own records). - Tests cover canonical happy path, metadata URL, all the malformed- format cases (empty, single-label, too-long, whitespace, empty label, oversize label, hyphen edges, underscore rejection, non-LDH characters), missing public key, and the migration- boundary error code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan G Slice 2: implements ANS-0 §0.B DID anchor profile for did:web
per docs/proposals/2026-05-16-spec-skeletons/anchor-0b-did.md §3.
Resolver pipeline:
1. Lexical validation: did:web prefix, percent-encoded port
rejection, consecutive-colon rejection, empty-component
rejection.
2. Resolution URL construction: empty path components yield
/.well-known/did.json; non-empty yield /<comp1>/<comp2>/.../did.json
with URL-escaped path components.
3. HTTPS GET with Accept: application/did+json, application/json.
Redirect policy enforces same-effective-second-level-domain;
cross-site redirects fail with DID_REDIRECT_DOMAIN_MISMATCH;
5-hop limit.
4. Response validation: status 200, content-type one of the two
accepted, 1MiB body cap.
5. DID document parsing: id field required, must equal the input
DID URI canonically.
6. Verification method selection: assertionMethod first, then
authentication, then any verificationMethod entry. Within
candidates, newest updated/created wins; ties broken
lexicographically by id.
7. JWK conversion: publicKeyJwk supported (re-canonicalized for
stable byte form); publicKeyMultibase and publicKeyPem return
typed not-implemented errors flagging the slice 2.1 boundary.
8. IdentityClaim shaped with anchorType=did, IssuedAt=now,
ExpiresAt=now+24h (the freshness budget per profile spec §3.5).
Tests cover:
- parseDIDWeb canonical happy paths and every documented error
code.
- buildResolutionURL for both well-known and path-based shapes,
plus path-component URL escaping.
- selectVerificationMethodJWK: assertionMethod precedence,
authentication fallback, most-recent picking, embedded objects,
no-method fallback to first verificationMethod entry.
- verificationMethodToJWK unsupported-shape paths (multibase,
pem, missing, malformed JWK).
- sameEffectiveDomain across same-site, cross-domain, ports.
- Resolve happy path against an httptest TLS server, with a
routing client transport that maps did:web URLs to the test
server's loopback listener.
- Resolve failure modes: 404, bad content type, malformed JSON,
missing id, ID mismatch, no verification method.
- Redirect policy: 5-hop limit and cross-domain rejection;
same-site (last two labels) admitted.
Coverage holds at 90.1%. publicKeyMultibase and publicKeyPem
decoding are deferred to Slice 2.1; the typed errors keep callers
on the JWK path until then.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace stale Slice 1 reference to DNSid with the generic external key-locator framing the proposal now uses, matching the rewritten anchor-0a-fqdn.md §3.4. The resolver is structured to admit adapters for any parallel agentic-discovery convention; ANS does not name a specific external spec normatively in either the spec or the reference implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan G Slice 3: implements ANS-0 §0.C LEI anchor profile per docs/profiles/anchor-0c-lei.md. The resolver carries the format validation that is portable across deployments (no GLEIF API access required) and stubs the actual GLEIF resolution behind a GLEIFClient interface. Format validation is fully implemented: - 20 ASCII alphanumeric characters, uppercase canonicalized. - ISO 17442 §5.1 mod-97 check digit verification using the ISO 7064 MOD 97-10 algorithm with letter-pair expansion (A=10, ..., Z=35). - The well-known GLEIF documentation example LEI (529900T8BM49AURSDO55) passes; perturbed check digits fail. GLEIF resolution is behind a Resolver.WithClient injection: - A Resolver constructed via New() (no client) returns LEI_GLEIF_NOT_CONFIGURED on Resolve, surfacing the slice boundary clearly. Format validation still runs first so callers testing against malformed LEIs get the same error as a fully- configured deployment. - A Resolver.WithClient(c) where c implements GLEIFClient runs the full pipeline: lookup, status check (only ACTIVE admitted; INACTIVE/LAPSED/RETIRED/MERGED rejected), attestation-key presence check, IdentityClaim construction with ExpiresAt = IssuedAt + 7 days (the freshness budget per profile spec §3.5). GLEIFClient is defined in this package as a minimal interface so the production HTTP client can land in a follow-up slice without touching the resolver. The LookupRecord shape (LEI, EntityName, EntityStatus, Jurisdiction, AttestationJWK, UpdatedAt) carries exactly what the resolver needs; richer fields (parent/child relationships, registration history) belong to a thicker client that composes with this one. Tests cover: - Canonicalize happy path, lowercase->uppercase, whitespace trim. - LEI_BAD_FORMAT for empty, too-short, too-long, hyphen, embedded space, non-alphanumeric. - LEI_BAD_CHECK_DIGITS for perturbed check digits. - LEI_GLEIF_NOT_CONFIGURED when no client injected. - LEI_RESOLUTION_FAILED on client error. - LEI_UNKNOWN on nil record. - LEI_INACTIVE for each non-ACTIVE status (parametric). - LEI_NO_ATTESTATION_KEY when AttestationJWK is empty. - Happy path with deterministic clock + injected fake client. Coverage holds at 90.2%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan G Slice 4: composes the per-profile resolvers (FQDN, did:web, LEI) behind a single AnchorResolver facade that the registration service plumbs to. Lexical-form dispatch per ANS-0 §4.1: "did:" prefix routes to DID, 20-char ASCII alphanumeric routes to LEI, hostname shape with at least one dot routes to FQDN, anything else fails INVALID_ANCHOR_FORMAT. Composition is configuration-driven: deployments register the resolvers their accepted-profile list admits. An empty registry rejects every input cleanly; a partial registry (e.g., FQDN-only) returns PROFILE_NOT_CONFIGURED on inputs whose dispatched branch has no registered resolver. The configuration validator at startup audits Registry.SupportedProfiles() against the deployment's accepted list before the first registration request lands. The Registry uses a function-typed FQDN seam (fqdnShapeResolver) so the slice-4 commit composes with the FQDN package's current Resolve stub. Slice 5 absorbs DNS resolution into the FQDN package; both surfaces stay aligned because the seam type matches port.AnchorResolver's Resolve method. Tests: - Lexical dispatch matrix: FQDN, did:web (lowercase + uppercase prefix), LEI (20-char alphanumeric), FQDN-shape edge case where a 20-char hostname with a dot beats the LEI shape. - INVALID_ANCHOR_FORMAT for empty input plus five categories of non-matching shapes. - PROFILE_NOT_CONFIGURED when the dispatched branch has no registered resolver, parametric across all three branches. - SupportedProfiles union when all three branches are registered. - End-to-end LEI dispatch with an injected fake GLEIF client proving the composition produces a clean IdentityClaim. - Lexical helpers (looksLikeLEI, looksLikeFQDN) tested independently for the shape boundaries. Compile-time check that *Registry satisfies port.AnchorResolver lives in the test file; the production package's import graph stays free of port to avoid an import-cycle risk if the port package ever needs an anchor type. Coverage holds at 90.3%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Introduces the AnchorResolver interface so the RA can use something other than the FQDN as the agent's cryptographic identity anchor. Every registration still carries an FQDN as
agentHost(the operational endpoint where the agent terminates TLS); what changes is what serves as the identity anchor. The existing FQDN path lifts behind the interface; two new resolvers land alongside it: did:web and LEI. No wire changes yet; that is the next PR.The first of three Plan G PRs. This one adds the ANS-0 Identity
Anchor abstraction the proposal at
docs/proposals/2026-05-16-spec-skeletons/ans-0-identity-anchor.mdspecifies, plus two concrete anchor-profile resolvers (FQDN, DID
via
did:web, LEI), and the registry facade that dispatches inputsto the right resolver by lexical form. No service or handler
changes yet; those land in the next PR in the stack. Two more DID
methods (
did:key,did:pkh) plus real-world LEI vectors land inthe third PR in the stack.
Domain types:
domain.AnchorTypeenum:fqdn,did,lei. Closed; a fourthtop-level type is an ANS-0 amendment, not a profile addition.
domain.IdentityClaimcarries the typed result of resolution:anchor type, canonical resolved ID, public key in JWK form,
optional metadata URL pointer, IssuedAt + ExpiresAt for cache
bounds. The struct shape matches the TypeScript signature in
the architectural-index skeleton §3.1.
Port:
port.AnchorResolveris the contract higher-spec code readsidentity through:
Resolve(ctx, input) → (*IdentityClaim, error)plus
SupportedProfiles() []stringfor configuration audit.Adapters under
internal/adapter/anchor/:fqdn: lifts the FQDN canonicalization and label validationthat previously lived inline in the registration service. RFC
1123 hostname constraints, lowercase + trailing-dot strip,
rejection of underscore-prefixed labels (ANS-3 reserves those
for registry records).
did: implementsdid:webresolution. HTTPS GET against/.well-known/did.json(or the path-component variant forscoped DIDs), DID document parsing, verification method
selection (assertionMethod first, then authentication, then
any verificationMethod entry; tie-breaker on the most recent
updated/created), JWK conversion (publicKeyJwk passesthrough; publicKeyMultibase and publicKeyPem return typed
not-implemented errors flagging the migration boundary).
Cross-domain redirects fail closed with
DID_REDIRECT_DOMAIN_MISMATCH.lei: ISO 17442 mod-97 (ISO 7064 MOD 97-10) check digitsin pure Go, plus a
GLEIFClientinterface stub. A productionGLEIF API client lands when an actual GLEIF testbed is wired
into CI; today the resolver is useful for testbeds and unit
tests with no network call (
ResolvereturnsLEI_GLEIF_NOT_CONFIGUREDif no client is injected).registry: dispatches by lexical form per ANS-0 §4.1.did:prefix → DID branch; 20-char ASCII alphanumeric → LEIbranch; RFC 1123 hostname with at least one dot → FQDN branch;
anything else →
INVALID_ANCHOR_FORMAT. Configuration-driven:unconfigured branches return
PROFILE_NOT_CONFIGUREDso thestartup validator catches deployment shape errors before the
first registration request.
Tests:
long, embedded whitespace, empty/oversize labels, hyphen
edges, underscore rejection, non-LDH characters).
documented failure modes (404, bad content type, ID mismatch,
no verification method, missing publicKey* shape, redirect
policy).
and LEI_BAD_CHECK_DIGITS cases. Resolution against an
injected fake GLEIFClient covers ACTIVE acceptance,
INACTIVE/LAPSED/RETIRED/MERGED rejection, lookup error
propagation, missing-attestation-key rejection.
FQDN-shape with a dot beats LEI shape, every reject path,
PROFILE_NOT_CONFIGURED branches, full SupportedProfiles
union).
port.AnchorResolvercompile-timecheck (
var _ port.AnchorResolver = (*Registry)(nil)).The DNSid mention in the original FQDN package comment was
replaced with the spec's generic external-key-locator framing
per the proposal's update on the same day. The resolver itself
names no specific external discovery effort.
Stacks on #14 (Plan F) → #13 (Plan D) → #12 (Plans A + C).
Merge order: #12 → #13 → #14 → this.
Test plan
make check(gofmt + golangci-lint + 90% coverage gate)🤖 Generated with Claude Code