Skip to content

[AI assisted] Plans A + C: capabilitiesHash sealing for the Trust Card#12

Open
scourtney-godaddy wants to merge 6 commits into
mainfrom
feat/plan-acd-capabilities-hash
Open

[AI assisted] Plans A + C: capabilitiesHash sealing for the Trust Card#12
scourtney-godaddy wants to merge 6 commits into
mainfrom
feat/plan-acd-capabilities-hash

Conversation

@scourtney-godaddy
Copy link
Copy Markdown

@scourtney-godaddy scourtney-godaddy commented May 16, 2026

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 agentCardContent body
holding 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_REGISTERED Transparency Log event under
attestations.metadataHashes.capabilitiesHash. A verifier holding
the 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-sha256
SvcParam 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 agentCardContent field is json.RawMessage to keep the
operator's bytes intact through canonicalization. Round-tripping
through map[string]any would reorder JSON keys or normalize
numbers and shift the resulting digest. The service hashes the
bytes through the project's anscrypto.Canonicalize helper, stores
hex-lowercase on the aggregate, then discards the body. Empty
content leaves the digest field empty; the AGENT_REGISTERED event
omits the capabilitiesHash key entirely (json:",omitempty").

Migration 006 adds capabilities_hash (nullable) to
agent_registrations. Rows registered before the column existed
load with the field empty; the activation flow does not
regenerate.

End-to-end test in registration_capabilitieshash_e2e_test.go
drives register → verify-acme → verify-dns → AGENT_REGISTERED
and confirms the sealed digest matches SHA-256(JCS(submitted bytes)). Negative tests in registration_errors_test.go cover
malformed JSON (INVALID_AGENT_CARD_CONTENT) and absent-content
paths.

Test plan

  • make check (gofmt + golangci-lint + 90% coverage gate)
  • End-to-end seal test (TestRegistrationCapabilitiesHash_E2E)
  • Negative path tests (malformed JSON, absent content)
  • Reviewer manually verifies the migration applies cleanly against the dev DB

🤖 Generated with Claude Code

scourtney-godaddy and others added 5 commits May 15, 2026 20:49
…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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AgentCardContent field on the V2 register request, plumbed handler → service → aggregate (json.RawMessage to preserve bytes for JCS).
  • New capabilities_hash column (migration 006) on agent_registrations; activation lifecycle writes the hex digest into the event under a new reserved MetadataHashKeyCapabilitiesHash constant.
  • 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.

Comment thread internal/ra/handler/registration_errors_test.go Outdated
Comment thread internal/ra/service/helpers.go
…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>
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.

2 participants