Skip to content

[AI assisted] feat(event): EquivalenceLink V2 event for cross-anchor binding#20

Open
scourtney-godaddy wants to merge 2 commits into
feat/lei-gleif-clientfrom
feat/equivalence-link-event
Open

[AI assisted] feat(event): EquivalenceLink V2 event for cross-anchor binding#20
scourtney-godaddy wants to merge 2 commits into
feat/lei-gleif-clientfrom
feat/equivalence-link-event

Conversation

@scourtney-godaddy
Copy link
Copy Markdown

BLUF. Adds the EQUIVALENCE_LINK V2 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's IMPLEMENTATION_NOTES.md named as missing from ANS-1's event set.

What lands

internal/tl/event/event.go gains TypeEquivalenceLink Type = "EQUIVALENCE_LINK" in the V2 enum, plus an EquivalenceLink struct carrying LinkedAnsID, LinkedAnsName, LinkedAnchorType, LinkedAnchorResolvedID, and a free-text Rationale. The struct attaches to the existing Event aggregate through an optional Equivalence field with omitempty, so existing AGENT_REGISTERED envelopes serialize byte-identically to pre-change.

The validation rules in IsValid() and Validate() keep link events distinct from agent-lifecycle events:

  • A EQUIVALENCE_LINK event MUST carry an Equivalence block, whose LinkedAnsID MUST be non-empty and MUST differ from AnsID.
  • A EQUIVALENCE_LINK event MUST NOT carry Agent or Attestations. Linking is metadata about two existing registrations, not a registration in its own right.
  • A non-link event (AGENT_REGISTERED, AGENT_DEPRECATED, etc.) MUST NOT carry an Equivalence block.

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.go covers 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 the omitempty guarantee that pre-change AGENT_REGISTERED bytes 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 ./... clean
  • go test ./internal/tl/event/... clean
  • An AGENT_REGISTERED event marshalled before and after this PR produces byte-identical JSON
  • An EQUIVALENCE_LINK event with LinkedAnsID == AnsID rejects with a self-reference error

🤖 Generated with Claude Code

…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>
@scourtney-godaddy scourtney-godaddy added the AI assisted Pull request created with AI assistance label May 17, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI assisted Pull request created with AI assistance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant