[AI assisted] Plan G integration: anchor block on V2 register, persist + surface#16
Open
scourtney-godaddy wants to merge 3 commits into
Open
[AI assisted] Plan G integration: anchor block on V2 register, persist + surface#16scourtney-godaddy wants to merge 3 commits into
scourtney-godaddy wants to merge 3 commits into
Conversation
Plan G Slice 5a: integrates the AnchorResolver-produced IdentityClaim into the registration flow without changing storage. Existing FQDN registrations continue to work unchanged (AnchorClaim is nil); new DID/LEI registrations submit a verified claim through RegisterRequest.AnchorClaim. Domain: - AgentRegistration gains an AnchorClaim *IdentityClaim field. Nil for legacy FQDN-only registrations; populated when the caller routed through ANS-0's AnchorResolver port. - NewRegistration accepts the claim as a new parameter. Today it's stored verbatim on the aggregate; persistence lands in Slice 5b. - New domain rule: a non-FQDN AnchorClaim accompanied by a non-zero ansName or non-nil identityCSR is rejected with NON_FQDN_REQUIRES_BASE_ONLY. Until ANS-2 admits a non-FQDN URI SAN, DID/LEI registrations must take the base-only path. This keeps the aggregate's invariants coherent: AnchorClaim.AnchorType authoritatively selects the registration shape. Service: - RegisterRequest gains an AnchorClaim field. The service passes it through to NewRegistration verbatim; no other behavior changes. Tests: - FQDN claim alongside a versioned registration attaches cleanly (the versioned-FQDN path is the dominant case today). - DID claim with a versioned ansName is rejected with NON_FQDN_REQUIRES_BASE_ONLY. - DID + base-only is accepted; AgentHost (service FQDN where the agent is reachable) and AnchorClaim.ResolvedID (did:web URI) are intentionally distinct fields. - LEI + base-only is accepted; the agent's service FQDN remains the operational endpoint. The 4 existing NewRegistration callers were updated to pass nil for the new parameter. All 168 existing tests continue to pass. Coverage holds at 90.3%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan G Slice 5b: persists the IdentityClaim that produced a registration so the V2 list and detail endpoints can surface anchor type to clients (Slice 6) and the audit trail records which anchor profile each registration came in through. Migration 009 adds two nullable columns to agent_registrations: - anchor_type: "fqdn", "did", or "lei" - anchor_resolved_id: canonical FQDN, DID URI, or 20-char LEI Both nullable so pre-Plan-G rows continue to load (their AnchorClaim surfaces as nil; the application layer treats nil as "FQDN-implicit-from-agent_host" for backward compatibility). PublicKeyJWK is intentionally NOT persisted on the row. ANS-0 verifiers re-resolve the claim through the AnchorResolver on demand to honor the per-profile freshness budget (1h for FQDN, 24h for did:web, 7d for LEI). A stored stale JWK would defeat that. Agents whose key has rotated produce a fresh resolution result the next verification cycle picks up; a verifier that needs the historical key for an audited event reads it from the ANS-4 Transparency Log entry. Storage shape: - Save (insert) writes anchor_type + anchor_resolved_id from agent.AnchorClaim through anchorClaimColumns helper. - Save (update) does NOT write anchor columns: the AnchorClaim is fixed at registration time and never mutates through lifecycle transitions. A future rotation event creates a new event in the TL, not a row mutation. - toDomain rehydrates AnchorClaim from the columns (PublicKeyJWK left nil). Tests: - Round-trip parametric across FQDN/DID/LEI claims and the no-claim legacy path: each saves, reads back, confirms type + resolved_id match, confirms PublicKeyJWK is absent. - Distinct fixtures per case avoid the per-FQDN base-only uniqueness check. Coverage holds at 90.3%. All 168 existing tests continue to pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan G Slice 6: opens the V2 register handler to anchor-aware
registrations and surfaces the anchor type on V2 list and detail
responses so clients can distinguish FQDN, DID, and LEI agents.
Wire shape:
POST /v2/ans/agents
{
"agentDisplayName": "...",
"agentHost": "agent.example.com",
"endpoints": [...],
"anchor": {
"anchorType": "did",
"input": "did:web:agent.example.com"
}
}
The anchor block is OPTIONAL. When omitted, the registration
takes the legacy FQDN-implicit path (existing behavior). When
present, the handler:
- Validates anchorType is one of fqdn, did, lei.
- Validates input is non-empty.
- For the fqdn profile, validates input matches agentHost
(case-insensitive).
- Constructs an IdentityClaim with anchorType + input as the
canonical resolved ID and passes it through to the service.
The handler-side IdentityClaim has PublicKeyJWK left empty: full
resolver dispatch (fetching the DID document, verifying the
GLEIF chain) lands in a later slice. The current shape stores
the anchor type so V2 list and detail responses surface it,
which is enough for live testing and for downstream tooling to
filter by anchor profile.
DTO additions:
- listItem.anchorType + anchorResolvedId
- agentDetails.anchorType + anchorResolvedId
Both omitempty so legacy registrations emit nothing.
A shared anchorFields helper reads (anchorType, anchorResolvedId)
from a registration's AnchorClaim, returning ("", "") for nil
claims. Centralizing the read keeps the list and detail emission
paths in lockstep.
Tests:
- resolveAnchorClaim happy paths for fqdn, did, lei.
- INVALID_ANCHOR_TYPE for missing or unknown types.
- MISSING_ANCHOR_INPUT when input is empty.
- ANCHOR_INPUT_AGENT_HOST_MISMATCH when fqdn input diverges
from agentHost.
- Case-insensitive fqdn matching against agentHost.
- anchorFields helper for nil/empty claim and populated claim.
Coverage holds at 90.3%. The (nil, nil) return for the omitted-
anchor signal is documented and nolint:nilnil tagged because it
is the documented "no anchor block" sentinel; an alternate would
be a custom typed wrapper but the existing nil-claim convention
in the service layer is simpler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tasks
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.
Wires the AnchorResolver into the V2 register handler and the storage layer. A registration now carries an optional
anchorblock; the V2 list endpoint surfacesanchorTypeandanchorResolvedIdper row. Plan F's base-only path is the carrier for any non-FQDN anchor.The second of three Plan G PRs. The previous PR added the
AnchorResolverabstraction and the resolver implementations(FQDN, did:web, LEI). This PR plumbs the resulting
IdentityClaimthrough the registration service, persists it on the registration
row, and surfaces it on V2 list and detail responses. The V2
register endpoint accepts an optional
anchorblock on the wire.Domain layer:
AgentRegistrationgains anAnchorClaim *IdentityClaimfield. Nil for legacy registrations where the identity is
implicit in
AgentHost; populated when the V2 register callcarried an explicit
anchorblock.NewRegistrationaccepts the claim as a new parameter.Non-FQDN claims (DID, LEI) carrying a non-zero ANSName or
non-nil identity CSR fail with
NON_FQDN_REQUIRES_BASE_ONLY. Until ANS-2 admits non-FQDNURI SANs, DID and LEI registrations must take the base-only
path.
Service layer:
RegisterRequestgains anAnchorClaimfield; the servicepasses it through to
NewRegistrationverbatim.(host, anchor_type)rather thanhostalone (this fixships in the third Plan G PR; this PR carries the field but
not the scoping change).
Storage layer:
anchor_typeandanchor_resolved_id(both nullable) to
agent_registrations. Pre-Plan-G rowsload with both empty.
Savewrites the columns through a newanchorClaimColumnshelper.
toDomainrehydratesAnchorClaimfrom the columns,leaving
PublicKeyJWKempty (verifiers re-resolve throughthe AnchorResolver to honor each profile's freshness budget,
rather than trusting a stored value).
Handler layer:
registrationRequestgains an optionalanchorblock:{ "anchorType": "fqdn"|"did"|"lei", "input": "..." }. Emptyblock omitted from the request signals the legacy
FQDN-implicit path.
resolveAnchorClaimvalidates the wire shape:anchorTypemust be one of the three values;
inputmust be non-empty;the FQDN profile additionally requires
inputto matchagentHostcase-insensitively (ANCHOR_INPUT_AGENT_HOST_MISMATCH).listItem) and detail (agentDetails) DTOsgain
anchorTypeandanchorResolvedId(both omitempty solegacy registrations emit nothing). A shared
anchorFieldshelper reads from
reg.AnchorClaim, returning empty stringsfor nil claims.
The handler-side
IdentityClaimcarries an emptyPublicKeyJWK:full resolver dispatch (fetching the DID document, calling the
GLEIF API) lands in a follow-up PR. The current shape stores the
anchor type so V2 list and detail responses can surface it,
which is enough for client-side filtering and for downstream
tooling to differentiate FQDN, DID, and LEI registrations.
Stacks on #15 (Plan G core) → #14 (Plan F) → #13 → #12. Merge
order: #12 → #13 → #14 → #15 → this.
Test plan
make check(gofmt + golangci-lint + 90% coverage gate)🤖 Generated with Claude Code