[AI assisted] Plans A + C: capabilitiesHash sealing for the Trust Card#12
Open
scourtney-godaddy wants to merge 6 commits into
Open
[AI assisted] Plans A + C: capabilitiesHash sealing for the Trust Card#12scourtney-godaddy wants to merge 6 commits into
scourtney-godaddy wants to merge 6 commits into
Conversation
…attestations Optional Registration Metadata path per ANS_SPEC.md §A.1: operators submit the ANS Trust Card body, the RA computes SHA-256(JCS(content)) at activation, and seals the hex-lowercase digest into the V2 AGENT_REGISTERED TL event under attestations.metadataHashes.capabilitiesHash. The AIM later verifies the live hosted Trust Card against the sealed hash. Reuses the existing metadataHashes map rather than introducing a new struct field, since the map already accommodates well-known hash keys. Spec-only change. Implementation lands in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s key
Reserve the Attestations.MetadataHashes["capabilitiesHash"] key for the
SHA-256(JCS(agentCardContent)) hash sealed at activation per
ANS_SPEC.md §A.1. Export MetadataHashKeyCapabilitiesHash so the RA
service and AIM verifier reach for the same constant rather than
string-literalling the key.
Tests pin three invariants the AIM relies on:
1. The map omits the key when no agentCardContent was submitted.
2. nil and empty MetadataHashes produce identical canonical bytes
(leaf-hash stability across the absence boundary).
3. The hex digest is lowercase 64-char.
No envelope shape change. The map already existed; this commit adds
a constant, documents the convention, and reads from the existing
shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ivation
Implements the §A.1 "hash and forget" Registration Metadata flow:
1. RegisterRequest carries optional AgentCardContent ([]byte) on the
V2 path. The service JCS-canonicalizes (RFC 8785), SHA-256
hashes, and stores the hex-lowercase digest as
AgentRegistration.CapabilitiesHash. The raw content is then
discarded — only the digest persists.
2. Migration 006 adds the capabilities_hash column on
agent_registrations (nullable for backwards compatibility and
for the spec-conformant "no content submitted" path).
3. Activation reads reg.CapabilitiesHash and, when populated,
writes it into the AGENT_REGISTERED event under
attestations.metadataHashes.capabilitiesHash. Empty stays absent.
The metadataHashes map is the right home for this digest: it already
exists, already has omitempty semantics, and the well-known key
constant lives next to its consumers in internal/tl/event.
Validation: malformed JSON (JCS canonicalization fails) returns
INVALID_AGENT_CARD_CONTENT rather than silently dropping the digest.
Tests pin: hash stored, hash absent when content omitted,
JCS-equivalent bodies hash identically across registrations,
malformed JSON rejected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the optional agentCardContent field to the V2 registrationRequest
DTO and forwards the raw bytes through to service.RegisterRequest.
Modeled as json.RawMessage so the operator-submitted bytes reach the
JCS canonicalizer without an intermediate map[string]any round-trip
that could shift the digest.
Tests cover:
1. Field plumbed end-to-end (POST 202 → aggregate carries hash).
2. Field omitted (CapabilitiesHash empty, no metadataHashes
entry at activation).
3. Malformed body returns 422 BAD_JSON without reaching the service.
The fixture exposes the agents store + context.Background() so handler
tests can assert on the persisted aggregate without standing up a
parallel verify-acme/verify-dns flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…GISTERED Drives the full §A.1 flow against the in-memory fixture: register-with-agentCardContent → verify-acme → verify-dns → claim the AGENT_REGISTERED outbox row → assert innerEventCanonical.attestations.metadataHashes.capabilitiesHash equals SHA-256(JCS(agentCardContent)) computed independently by the test. Also extracts the hash-and-store logic into applyAgentCardContentHash so RegisterAgent stays under the funlen 130-line ceiling (the new helper + the existing hashAgentCardContent live in helpers.go). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 16, 2026
There was a problem hiding this comment.
Pull request overview
Adds Trust Card sealing to the V2 registration flow: the RA accepts an optional agentCardContent body, hashes its JCS-canonicalized form with SHA-256, persists the hex digest on the registration aggregate, and emits it as attestations.metadataHashes.capabilitiesHash in the AGENT_REGISTERED Transparency Log event.
Changes:
- New
AgentCardContentfield on the V2 register request, plumbed handler → service → aggregate (json.RawMessageto preserve bytes for JCS). - New
capabilities_hashcolumn (migration 006) onagent_registrations; activation lifecycle writes the hex digest into the event under a new reservedMetadataHashKeyCapabilitiesHashconstant. - OpenAPI/spec docs updated; unit, error-path, and e2e tests cover hash storage, JCS equivalence, omission, malformed JSON rejection, and end-to-end event sealing.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/api-spec-v2.yaml, internal/adapter/docsui/openapi/ra.yaml | Document the new agentCardContent request field. |
| spec/api-spec-tl-v2.yaml, internal/adapter/docsui/openapi/tl.yaml | Document metadataHashes.capabilitiesHash on TL attestations. |
| internal/tl/event/event.go | Adds MetadataHashKeyCapabilitiesHash constant and expanded doc comments on MetadataHashes. |
| internal/tl/event/event_test.go | Tests serialization shape, leaf-stability for nil vs empty map, and hex format. |
| internal/domain/agent.go | Adds CapabilitiesHash field to AgentRegistration. |
| internal/ra/service/registration.go | Adds AgentCardContent to RegisterRequest and invokes the new hash helper. |
| internal/ra/service/helpers.go | New hashAgentCardContent/applyAgentCardContentHash helpers (JCS+SHA-256). |
| internal/ra/service/lifecycle.go | Seals CapabilitiesHash into the AGENT_REGISTERED event's metadataHashes. |
| internal/ra/service/registration_test.go | Service-layer tests: hash storage, JCS equivalence, omission, invalid JSON. |
| internal/ra/handler/registration.go | Wires agentCardContent (RawMessage) through to the service. |
| internal/ra/handler/registration_errors_test.go | Adds handler-level tests for plumbing, omission, malformed-JSON rejection. |
| internal/ra/handler/registration_capabilitieshash_e2e_test.go | New E2E test asserting the sealed hash on the outbox AGENT_REGISTERED payload. |
| internal/ra/handler/lifecycle_test.go | Expands handlerFixture to expose agents store and ctx for assertions. |
| internal/adapter/store/sqlite/agent.go | Persists/reads capabilities_hash column. |
| internal/adapter/store/sqlite/migrations/006_agent_capabilities_hash.sql | New nullable column on agent_registrations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This was referenced May 16, 2026
…layer Two follow-ups from review: 1. applyAgentCardContentHash now JSON-decodes agentCardContent before canonicalizing and rejects with INVALID_AGENT_CARD_CONTENT if the value is not a JSON object. The OpenAPI declares the field as type=object, but JCS canonicalization happily accepts arrays, strings, numbers, and null, so without the shape check an operator could seal a capabilitiesHash for a JSON array that the AIM cannot reproduce against the live Trust Card. 2. Replaces the misleading docstring on the malformed-JSON test with one that says what the test actually pins (BAD_JSON at the handler, service is not reached) and adds the matching service-layer test that submits a JSON array and asserts the new INVALID_AGENT_CARD_CONTENT path. 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.
Seals the Trust Card's capabilities map into a hash that DNS records and Transparency Log entries can carry. A verifier can detect a tampered or stale capabilities list without fetching the full Trust Card JSON.
The V2 register endpoint accepts an optional
agentCardContentbodyholding the operator's Trust Card. The RA hashes those bytes through
JCS canonicalization (a deterministic JSON byte form per RFC 8785),
stores the SHA-256 digest on the registration row, and copies it
into the
AGENT_REGISTEREDTransparency Log event underattestations.metadataHashes.capabilitiesHash. A verifier holdingthe TL receipt and a fetched Trust Card can confirm the body matches
what the RA sealed, without contacting the RA itself.
The corresponding Consolidated Approach SVCB record's
card-sha256SvcParam carries the same digest re-encoded as base64url. The SVCB
emission ships in a follow-up PR in the stack so this PR lands the
seal independent of the DNS record format.
The
agentCardContentfield isjson.RawMessageto keep theoperator's bytes intact through canonicalization. Round-tripping
through
map[string]anywould reorder JSON keys or normalizenumbers and shift the resulting digest. The service hashes the
bytes through the project's
anscrypto.Canonicalizehelper, storeshex-lowercase on the aggregate, then discards the body. Empty
content leaves the digest field empty; the
AGENT_REGISTEREDeventomits the
capabilitiesHashkey entirely (json:",omitempty").Migration 006 adds
capabilities_hash(nullable) toagent_registrations. Rows registered before the column existedload with the field empty; the activation flow does not
regenerate.
End-to-end test in
registration_capabilitieshash_e2e_test.godrives register → verify-acme → verify-dns →
AGENT_REGISTEREDand confirms the sealed digest matches
SHA-256(JCS(submitted bytes)). Negative tests inregistration_errors_test.gocovermalformed JSON (
INVALID_AGENT_CARD_CONTENT) and absent-contentpaths.
Test plan
make check(gofmt + golangci-lint + 90% coverage gate)TestRegistrationCapabilitiesHash_E2E)🤖 Generated with Claude Code