[AI assisted] feat(event): EquivalenceLink V2 event for cross-anchor binding#20
Open
scourtney-godaddy wants to merge 2 commits into
Open
[AI assisted] feat(event): EquivalenceLink V2 event for cross-anchor binding#20scourtney-godaddy wants to merge 2 commits into
scourtney-godaddy wants to merge 2 commits into
Conversation
…r binding
Adds TypeEquivalenceLink to the V2 event type enum (V1 stays
unchanged: V1 is the byte-for-byte reference TL schema and a new
event type would be a wire-shape change). Closes the cross-anchor
binding gap: deployments can now record a cryptographic link between
two registrations under different anchor profiles asserting the same
operational entity, rather than relying solely on a client-side join
on agentHost.
Schema additions:
- TypeEquivalenceLink Type = "EQUIVALENCE_LINK" in the V2 enum.
- IsValid updated to include the new type.
- New EquivalenceLink struct on Event with omitempty:
LinkedAnsID (required when type == EQUIVALENCE_LINK)
LinkedAnsName (optional; absent for base-only registrations)
LinkedAnchorType (optional; "fqdn" / "did" / "lei")
LinkedAnchorResolvedID (optional; the canonical anchor identifier)
Rationale (optional; default "operator-asserted-multi-anchor")
Validation rules (in Event.Validate):
- EQUIVALENCE_LINK MUST carry equivalence; equivalence.linkedAnsId
required and MUST differ from ansId.
- EQUIVALENCE_LINK MUST NOT carry agent or attestations.
- Non-link event types MUST NOT carry equivalence.
Cryptographic semantics: the producer signature on the enclosing
event is the link. The producer (RA) attests that ansId and
linkedAnsId resolve to the same operational entity. Per-deployment
authority checks (controller has rights over both registrations)
are operator concerns; the reference impl provides the type and
validation surface only. RA-side handler endpoint to ingest
operator-initiated link assertions is a follow-up.
The omitempty on Equivalence preserves byte-identical canonical
output for existing AGENT_REGISTERED / AGENT_RENEWED / AGENT_REVOKED
/ AGENT_DEPRECATED events; leaf hashes for those types are unchanged.
Tests: 11 cases covering happy path, missing-equivalence,
missing-LinkedAnsID, self-reference, agent-rejected,
attestations-rejected, non-link-rejects-equivalence, JSON round-trip
shape, and omitempty preservation on non-link events.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the handler that turns the schema PR #20 added into a useful endpoint. POST /v2/ans/agents/{agentId}/equivalence-links emits one EQUIVALENCE_LINK event into the TL linking the path agent to a linked agent named in the body. Authorization is the simplest defensible shape: the same authenticated operator must own both registrations on this RA. The writeOwnership middleware confirms the primary; the service re-checks ownership of the linked agent inside (404-shaped to preserve existence-hiding for agents owned by others). Service-layer LinkEquivalence rejects empty owner/primary/linked, self-links, missing-linked, non-active linked, and propagates store errors. Inner-event builder leaves Agent and Attestations nil and populates Equivalence per the schema; the existing outbox + TL append path reuses unchanged because v2Codec.ParseAndBuild already calls Validate() which has accepted EQUIVALENCE_LINK since PR #20. A future amendment may admit federated link events that span two RAs with two distinct producer signatures, requiring an Envelope schema cosigner block. Within one RA the existing producer signature is enough. Tests: unit coverage for anchor recovery + inner-event shape (FQDN/LEI, with/without AnchorClaim). Live integration verified against the demo stack: register two agents under one operator, POST link, observe outbox-worker delivery EQUIVALENCE_LINK leafIndex 2 in the TL, plus 422/404 rejection paths for self-link, missing linkedAnsId, and non-existent linked agent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
BLUF. Adds the
EQUIVALENCE_LINKV2 event type that lets a deployment record cross-anchor coexistence (the same operator running an FQDN registration and an LEI registration, for example) without mutating either registration. This is the schema the layered-spec proposal'sIMPLEMENTATION_NOTES.mdnamed as missing from ANS-1's event set.What lands
internal/tl/event/event.gogainsTypeEquivalenceLink Type = "EQUIVALENCE_LINK"in the V2 enum, plus anEquivalenceLinkstruct carryingLinkedAnsID,LinkedAnsName,LinkedAnchorType,LinkedAnchorResolvedID, and a free-textRationale. The struct attaches to the existingEventaggregate through an optionalEquivalencefield withomitempty, so existingAGENT_REGISTEREDenvelopes serialize byte-identically to pre-change.The validation rules in
IsValid()andValidate()keep link events distinct from agent-lifecycle events:EQUIVALENCE_LINKevent MUST carry anEquivalenceblock, whoseLinkedAnsIDMUST be non-empty and MUST differ fromAnsID.EQUIVALENCE_LINKevent MUST NOT carryAgentorAttestations. Linking is metadata about two existing registrations, not a registration in its own right.AGENT_REGISTERED,AGENT_DEPRECATED, etc.) MUST NOT carry anEquivalenceblock.A self-reference (
LinkedAnsID == AnsID) is rejected with a clear error so a deployment cannot accidentally link an agent to itself and confuse a downstream verifier.Tests
internal/tl/event/equivalence_link_test.gocovers the happy path, every rejection path (missing equivalence block, missing linked ID, self-reference, agent field on link, attestations field on link, equivalence on non-link event), the JSON round trip with field-name spot-checks (linkedAnsId,linkedAnchorType,linkedAnchorResolvedId,rationale), and theomitemptyguarantee that pre-changeAGENT_REGISTEREDbytes stay byte-identical.What this enables, what this does not do
The event type is the schema only. The TL append path, the receipt path for link events, and the search-API surface are out of scope for this PR; a follow-up wires those in once the schema is upstream. A regulated deployment that wants to claim "this LEI registration corresponds to this FQDN registration" needs the schema first; this PR delivers it.
Test plan
go build ./...cleango test ./internal/tl/event/...cleanAGENT_REGISTEREDevent marshalled before and after this PR produces byte-identical JSONEQUIVALENCE_LINKevent withLinkedAnsID == AnsIDrejects with a self-reference error🤖 Generated with Claude Code