Skip to content

feat(ra): Implement ARD-compliant discovery service and event feed - #46

Merged
kperry-godaddy merged 18 commits into
mainfrom
feat/ans-finder
Jul 17, 2026
Merged

feat(ra): Implement ARD-compliant discovery service and event feed#46
kperry-godaddy merged 18 commits into
mainfrom
feat/ans-finder

Conversation

@kperry-godaddy

Copy link
Copy Markdown
Member

This pull request introduces the new ans-finder service, which implements the Agentic Resource Discovery (ARD) service over the ANS reference implementation. It also makes several improvements to the RA service, including the addition of a public agent-events feed and enhanced handler logging. The Makefile is updated to support building and documenting the new finder service. Minor cleanups and dependency updates are also included.

New ANS Finder Service:

  • Added the new ans-finder binary (cmd/ans-finder/main.go), which:
    • Polls the RA's agent-events feed, indexes events for full-text search, and serves public discovery endpoints per the ARD contract.
    • Includes liveness/readiness endpoints and operational runbook documentation.
    • Features a rate-limited, anonymous HTTP API and a Swagger UI docs endpoint.

Makefile and Build System Updates:

  • Updated the Makefile to support building the new ans-finder binary and to sync its OpenAPI spec into the docs UI. [1] [2] [3] [4]

RA Service Improvements:

  • Added a public /v1/agents/events feed to the RA, with exact-path anonymous access and handler wiring for event streaming. [1] [2] [3] [4] [5]
  • Enhanced security by adding an HTTP middleware to set X-Content-Type-Options: nosniff on all responses, especially for the public events feed.
  • Updated all handler constructors to accept a logger, improving logging consistency and observability. [1] [2] [3]

Code Cleanup and Dependency Updates:

  • Cleaned up unused imports and removed the unused VerifiedCheckpoint struct in cmd/ans-verify/walk.go. [1] [2] [3] [4]

Add spec/api-spec-finder-v1.yaml — the ANS Finder discovery surface,
implementing the Agentic Resource Discovery Specification v0.5 Registry
REST interface (POST /v1/search, POST /v1/explore).

- House style of spec/api-spec-v2.yaml; RFC 7807 Problem errors shaped
  per spec/api-spec-tl-v2.yaml, carrying the five ARD standard error
  codes (ARDS Appendix B); 400 INVALID_ARGUMENT for bad arguments.
- Each operation documents why POST (structured query body) and that
  responses are not GET-cacheable.
- Query model per ARDS §7.1: text required for search / optional for
  explore, filter as dot-path keys to arrays (OR within, AND across).
- CatalogEntry url-XOR-data via oneOf, with prose scoping the invariant
  to Active entries (tombstones carry neither and never reach the wire).
- TrustManifest/Attestation per ARDS §5.1/§5.2; free-text fields marked
  untrusted publisher content; §-citations throughout.
- federation auto|referrals|none (default auto); pageSize default 10,
  max 100. Discovery routes are anonymous (security: []), rate-limited.

No Makefile/docs-sync wiring — that lands with the server and its
conformance test in a later PR.

Signed-off-by: kperry <kperry@godaddy.com>
Add internal/finder/feed and internal/finder/project — the consumer-side
ingestion contract and the pure event-to-catalog-entry projection.

internal/finder/feed:
- EventPageResponse/EventItem/AgentEndpoint/AgentFunction mirror the
  production swagger (swagger_ans.json) field-for-field: JSON tags and
  required/optional (omitempty) match, Items marshals as [] never null,
  tokens are the production hyphenated forms (HTTP-API, STREAMABLE-HTTP,
  JSON-RPC). EventItem.Validate enforces required fields, agentId UUID,
  createdAt RFC 3339, and binds agentHost to the ansName FQDN via
  domain.ParseAnsName in one step.

internal/finder/project:
- FromEvent is the single entry point. The lifecycle split is the safety
  rule: REVOKED/DEPRECATED mint identity-only tombstones from required
  fields, never touching label minting or URL policy, so a malformed
  display field can never block a revocation. feed.Validate failure is a
  hard error; an unknown eventType is an alertable Skip, not an error, so
  a growing producer enum cannot wedge ingestion at the cursor.
- Two security chokepoints: one text sanitizer strips Cc controls plus
  bidi/zero-width Cf from every emitted string, and validateEmittedURL
  (https-or-AllowHTTP, no userinfo/query/fragment, host-bound) is the
  only way any URL — including the constructed well-known fallback —
  enters an entry. A present-but-invalid metaDataUrl fails closed (no
  fallback rescue); an invalid agentUrl is omitted from metadata, not a
  Skip. URN is a lineage handle (urn:ai:host:agents:label); empty label
  Skips the Active path but never a tombstone.
- Active mapping per ARDS §4.2: A2A/MCP fan out one entry each, HTTP-API
  is excluded, capabilities/tags are sanitized, deduped, sorted and
  capped, and entries sort by (identifier, type, url) so duplicate
  protocols stay deterministic. Standard encoding/json marshaling.

Both packages at 100% statement coverage; golden-vector harness with
UPDATE_GOLDEN matches internal/tl/event. Conformance against the OSS RA
feed route is closed by PR 2's byte-equality and enum-value tests.

Signed-off-by: kperry <kperry@godaddy.com>
…alidation

Review-pass fixes for three blockers plus folded-in polish.

B1 — tombstone dropped createdAt. ProjectedEntry now carries CreatedAt
(json:"-"), populated verbatim in both the tombstone and active paths;
the test-local goldenView struct gained the field, goldens regenerated.
The index orders suppression by this timestamp, so a tombstone that
lost it could be applied out of order. The tombstone table test now
asserts createdAt directly (the prior comment falsely claimed a golden
covered it).

B2 — emitted URLs bypassed the text chokepoint. Two parts:
(a) sanitizeText now strips ALL of unicode.Cf (a superset of the prior
    enumerated bidi/zero-width list — also covers U+061C, U+2060, and
    the U+E0000-E007F TAG block) alongside unicode.Cc;
(b) validateEmittedURL now REJECTS fail-closed (does not strip — a URL
    is structural) any raw URL containing a Cc/Cf rune, before
    returning it. A bidi-bearing metaDataUrl in event_adversarial_text
    now proves URL coverage via the golden (SkipInvalidURL).

B3 — full validation ran before the lifecycle switch, so a
REVOKED/DEPRECATED event missing an Active-only field (e.g. version)
errored and never tombstoned — the exact fail-open the lifecycle split
prevents. Validation is now split: feed.ValidateIdentityKeys (logId,
agentId UUID, ansName parse + FQDN==lower(agentHost), createdAt RFC3339)
runs before the switch; the full feed.Validate (eventType, agentHost,
version presence) runs only on the Active path. A table test proves a
version-less REVOKED/DEPRECATED still tombstones, and an inverse test
keeps the Active path erroring.

Folded-in polish:
- Skip.Detail uses strconv.Quote (raw eventType/protocol with control
  chars reach operator logs).
- agentHost lowercased once at the top of projectActive and tombstone
  so URN, trustManifest.identity, and the well-known fallback agree;
  case-variant events no longer mint byte-different identities.
- validateEmittedURL also rejects u.ForceQuery (bare trailing "?").
- spec: SearchRequest.query uses an allOf overlay adding required:[text]
  so a schema validator rejects a text-less search (prose-only before).
- doc comments drop the local checkout path and PR-N references for the
  public repo.

Both packages remain at 100% statement coverage; make check green;
goldens regenerated.

Signed-off-by: kperry <kperry@godaddy.com>
Add GET /v1/agents/events on the RA: a public, unauthenticated feed of
agent lifecycle events the ANS Finder ingests. The response is
byte-compatible with the production getAgentEvents contract (consumer
mirror: internal/finder/feed).

Pipeline:
- Migration 006 adds outbox_events.log_id, an index on log_id (cursor
  resolution), and a partial (created_at_ms, id) feed index (retention-
  seekable reads). The outbox worker persists the TL-assigned logId
  atomically with sent_at_ms via MarkSent(ctx, id, logID); the feed
  gates on both being non-NULL so an item in the feed is provably
  sealed and its receipt is resolvable from logId. An empty logId from
  a non-compliant TL is treated as a delivery anomaly (row kept pending),
  never written. Open() runs ANALYZE so the planner uses the feed
  indexes (SEARCH, not a primary-key SCAN over aged-out rows).
- port.FeedReader/FeedRow/FeedQuery is the read port; the SQLite
  FeedStore implements it (JOINs registrations + endpoints, retention
  window, outbox-id-ASC ordering, lastLogId cursor resolved to its
  lowest matching outbox id, providerId -> empty page).
- service.EventsService projects each row into the wire EventItem and
  owns the domain->wire token map (driven by domain.AllProtocols/
  AllTransports). providerId is never emitted.
- V1EventsHandler parses limit (1-200, 422 on out-of-range), lastLogId,
  providerId.

Security/correctness hardening:
- Auth exemption for the feed is EXACT-match (WithAnonymousExactPath),
  not prefix: a subtree exemption let chi backtrack /v1/agents/events/*
  onto the authenticated /v1/agents/{agentId}/* routes with auth
  skipped. Both static and OIDC providers now match exact paths exactly
  and subtree paths on a / boundary (so /docsfoo is not under /docs).
- 500 responses for unexpected (non-domain) errors return a fixed
  generic detail; raw fault text no longer leaks to clients. To avoid
  swallowing the cause, error responses route through a shared embedded
  responder seam (handlers embed it; one injected zerolog.Logger, no
  globals) whose writeError logs the real cause of any 5xx before
  sanitizing — enforced by construction across all RA handlers, not the
  events route alone. The package WriteError is retained as the
  domain-error-only entry point for the ownership middleware.
- X-Content-Type-Options: nosniff on all responses.

Conformance tests pin the contract: byte-equality through the consumer
mirror (full AND minimal item), enum-value membership against the
swagger sets via domain.AllProtocols/AllTransports plus the enqueued
eventType tags, feed.EventItem.Validate() over every emitted item, and
an EXPLAIN QUERY PLAN assertion that both feed queries use their
indexes. Auth regression tests assert /v1/agents/events/revoke without
credentials is 401, not a silent bypass; a responder test asserts a
non-domain 500 returns a generic body AND logs the real cause.

events-feed retention added to RA config (default 720h/30d).

Signed-off-by: kperry <kperry@godaddy.com>
…lore

Add the runnable ans-finder binary serving the ARD discovery surface over
the ANS reference implementation.

Pipeline:
- internal/finder/index defines the Catalog port and query vocabulary;
  internal/adapter/store/sqlitefinder is its SQLite FTS5 implementation.
  Applying an Active event REPLACES the complete row set for its ansName
  (grouped by ansName+logId), so an endpoint whose (type,url) changes or
  is dropped between versions never lingers ACTIVE. bm25-ranked search
  normalized 0-100; type/tags/capabilities/publisher/attestation-type
  filters; GROUP BY facets with limit/minCount/otherCount; ansName-keyed
  tombstone suppression gated on created_at; replay-safe (a newer-or-equal
  tombstone is never overridden by replaying an older Active event); a
  revoke that suppresses nothing while the agent is still active is
  reported for a WARN. User search text is quoted into FTS5 string
  literals so operators can never be injected.
- internal/finder/poller drains the RA events feed from the cursor,
  projects each item via project.FromEvent, and applies pages atomically.
  A structural feed error aborts the round without advancing the cursor;
  an unknown eventType is a logged Skip. A non-2xx (incl. 429) is a
  transient retry, never a cursor reset. A no-progress page (same cursor,
  more claimed) breaks the drain loop; repeated failure at one cursor
  escalates a wedge line; idle rounds log DEBUG, ingesting rounds INFO.
  The HTTPS feed client enforces the transport policy (https unless
  AllowHTTP, TLS never skipped), refuses redirects, and caps the body.
- internal/finder/handler serves POST /v1/search and /v1/explore per the
  frozen spec, RFC 7807 Problem errors, a query-bound opaque pageToken, a
  global token-bucket rate limiter (Retry-After + nosniff on responses),
  the additive staleSince signal, per-request cost caps (text size/tokens,
  filter-value count, facet count + dedup), and control-character
  rejection on query text. Filter values accept the spec's bare-scalar or
  array form. Readiness (/v1/admin/ready) is gated on the first completed
  poll; health is liveness-only.
- cmd/ans-finder wires config.LoadFinder, the index, the poller goroutine,
  and the HTTP server (chi, hardened timeouts, graceful shutdown that
  drains the poller before closing the store on either exit path); docs at
  /docs; the package comment documents health/ready semantics and the
  ingestion-wedge recovery runbook.

Wiring: Makefile build-finder + docs-sync; docsui.SpecFinder embed with a
byte-equality drift guard; demo start.sh/stop.sh/run-lifecycle.sh gain an
ans-finder stage that discovers the demo agent (publisher-filtered) end to
end. The frozen finder spec is amended with the additive staleSince
response field and a note that an over-max pageSize is clamped.

internal/finder/{index,poller,handler} and the sqlitefinder adapter are
table-tested against in-memory SQLite and httptest feed servers; a
conformance test validates response field names AND spec-required keys
against the embedded spec. cmd/ans-finder is excluded from the coverage
denominator per repo policy; overall coverage stays above the 90% gate.

Signed-off-by: kperry <kperry@godaddy.com>
…gnote

The C2SP signed-note checkpoint parser was duplicated ~85% between the
offline verifier (cmd/ans-verify) and the TL checkpoint-read path
(internal/tl/service). Consolidate it into a new internal/lognote
package that depends only on internal/crypto and its leaf deps — no
internal/tl/logstore, no storage adapters, no Tessera. This lets
cmd/ans-verify link the verification path without pulling the
log-writer dependency tree.

lognote exposes:
  - Signature{Name, Raw, Blob} with KeyHash/KeyHashHex/Body/Classify
  - SplitNote(raw) (body, sigs, found) — lenient tokenization; its doc
    comment carries the safety invariant that splitting proves nothing
    and only VerifyCheckpointNote's unconditional gate (known keyhash
    AND valid ECDSA sig over the fixed body) establishes trust
  - Checkpoint{Origin, Size, RootHash}
  - VerifyCheckpointNote(raw, keysByHash) (*Checkpoint, error)
  - VerifyC2SPECDSA (moved verbatim from logstore, DER + legacy P1363)

Migration:
  - logstore.VerifyC2SPECDSA deleted outright (no alias); the signer
    stays. Its round-trip test now verifies through lognote.
  - internal/tl/service deletes splitNoteBody/keyhashFromSumdbSig/
    classifySumdbSig and the sigType consts; viewFromRecord maps
    lognote.Signature (origin fallback, "0x"+KeyHashHex(), algES256);
    the enrich switch gains a default arm; enrichC2SPSignature calls
    lognote.VerifyC2SPECDSA.
  - cmd/ans-verify deletes verifyCheckpointNote + keyHashHex and the
    VerifiedCheckpoint type; verifiedCheckpoint returns
    *lognote.Checkpoint and delegates to lognote.VerifyCheckpointNote.
    go list -deps ./cmd/ans-verify no longer references tl/logstore or
    Tessera client/storage.

internal/lognote ships with table-driven tests at 100% of statements,
including a golden note signed over real bytes with a real ECDSA key
and the adversarial case (known keyhash + garbage sig rejected, loop
continues to a later valid line). Exhaustive cases live in lognote;
thin smoke tests remain at the migrated sites.

Wire-delta disclosures (behavior changes from unifying the two
parsers onto internal/lognote, recorded for reviewers):
  - CheckpointView.Signatures: an invalid-base64 signature line is now
    omitted from the rendered signatures instead of surfaced with
    Valid=false. This state is reachable only via corrupted checkpoint
    storage (the TL never writes a malformed line), and dropping it is
    fail-closed — a line we cannot decode carries no trustworthy
    signer/keyhash to display.
  - cmd/ans-verify checkpoint tokenization is unified onto the service
    parser's semantics. Consequence: tab-separated signature lines no
    longer parse (no writer emits tabs — sumdb-note uses single
    spaces), and CRLF / leading-whitespace lines now become signature
    candidates (still subject to the same keyhash+signature gate, so
    no verification weakening).

Signed-off-by: kperry <kperry@godaddy.com>
Add docs/pr-specs/FINDER-ard-discovery-service.md, the design of record
for ans-finder: an ARD-conformant (Agentic Resource Discovery v0.5)
discovery service over ANS-registered agents.

Covers the feed-only ingestion decision and its accepted trades, the
pure EventItem-to-CatalogEntry projection with the tombstone safety
rule, the /v1/search and /v1/explore API surface, the trust model and
receipt semantics, the text-hygiene and URL-policy security contracts
that must precede any wire freeze, the deviations from the ARD and ANS
RA contracts, and verbatim ARDS v0.5 and production-swagger field
tables in the appendix.

Docs-only; new docs/pr-specs/ directory.

Signed-off-by: kperry <kperry@godaddy.com>
Adds docs/architecture/ans-finder.md — the as-built companion to the
FINDER design spec: system topology, register-to-discover-to-prove
sequence, poller round semantics, index entry lifecycle, and the
asserted-vs-provable trust split, each as a Mermaid diagram with the
operational details (feed gating, projection chokepoints, request-cost
caps, readiness semantics, runbook pointers) as built and verified.

Trues up the design spec's deviations table with the two rows the
implementation added: nextPageToken response naming vs ARD §7.2's
example, and the additive optional staleSince freshness field.

Signed-off-by: kperry <kperry@godaddy.com>
…spec

Signed-off-by: kperry <kperry@godaddy.com>
…ver-card+json and adjust pagination token references

Signed-off-by: kperry <kperry@godaddy.com>
Comment thread spec/api-spec-v2.yaml
Comment thread spec/api-spec-v2.yaml
Comment thread internal/adapter/store/sqlitefinder/search.go Outdated
Brings in verified identities (#41), the x/mod 0.37.0 bump (#44), and
pluggable server-cert issuance via the certificate-order lifecycle
(#45). Both lanes now sit side by side: the finder feed/discovery
surface and the verified-identity surface.

Conflict resolutions:
- internal/config RAConfig: keep both Identity (main) and EventsFeed
  (finder) fields.
- internal/port/store.go: keep both FeedReader (finder) and
  IdentityStore/IdentityLinkStore (main) — independent ports.
- cmd/ans-ra/main.go: construct the feed store + events service and the
  identity stores + identity service together; lifecycle handler is
  NewLifecycleHandler(regSvc, logger).WithIdentityViews(identitySvc).
- spec/api-spec-v2.yaml + docsui mirror: keep both the Events and
  Verified Identities tags (docsui regenerated via make docs-sync).
- scripts/demo/run-lifecycle.sh: keep finder's offline-verify
  (bin/ans-verify) and ans-finder discovery steps alongside main's
  identity steps; renumbered to 14-20.
- internal/ra/handler/identity_handler_test.go: pass zerolog.Nop() to
  the logger-param handler constructors finder introduced.
- internal/config: hoist the "self" and "noop" adapter-type
  discriminators to constants. goconst flags them only once both
  branches' validators (main's CA-server + resolver checks, finder's
  config package additions) combine in one package.

make check passes; internal/ coverage 90.6% (>= 90% gate).

Signed-off-by: kperry <kperry@godaddy.com>
…mp comment

spec/api-spec-v2.yaml: the agent-events feed's 422/500 responses
declared application/problem+json but referenced ErrorResponse, whose
shape (status: "ERROR" string enum, message/details, no type/title) does
not match what the handler emits. writeProblem
(internal/ra/handler/errors.go) serializes RFC 7807: {type, title,
status (int), detail, code}. Add a Problem component schema matching that
shape (type/title/status required; detail/code optional) and reference it
from both responses. ErrorResponse is retained for the routes that still
use it. docsui mirror regenerated via make docs-sync.

internal/adapter/store/sqlitefinder/search.go: the rfc3339 comment
claimed "the feed normalizes its timestamps," which is false — the finder
carries feed timestamps verbatim (project.ProjectedEntry) and feed.go
validates createdAt with time.Parse but discards the result. Reword to
state the real dependency: the lexical expires_at compare is sound only
because the RA emits canonical UTC RFC 3339, not because anything
normalizes.

Signed-off-by: kperry <kperry@godaddy.com>
@kperry-godaddy kperry-godaddy changed the title Implement ARD-compliant discovery service and event feed feat(ra): Implement ARD-compliant discovery service and event feed Jun 26, 2026
kperry-godaddy and others added 2 commits June 26, 2026 17:16
Brings in Adapter Style DNS Discovery Profiles (#25), the 0.1.6 release
(#39), and dependency bumps: x/net 0.56.0 (#49), x/crypto 0.53.0 (#51),
go-oidc 3.19.0 (#48), sqlite 1.53.0 (#52), plus the actions/checkout and
actions/setup-go CI bumps (#50, #53).

#25 threaded a discovery ProfileRegistry through NewRegistrationService;
that signature change auto-merged into cmd/ans-ra/main.go and the RA
handler tests, and build + vet are clean across the finder code.

Only go.mod conflicted: kept the higher dependency versions from main
(x/net 0.56.0, sqlite 1.53.0) alongside the finder's direct
gopkg.in/yaml.v3, then go mod tidy. docsui mirror re-synced from the
merged spec; the RFC 7807 Problem schema and the feed's 422/500
references survive intact.

make check passes; internal/ coverage 90.8% (>= 90% gate).

Signed-off-by: kperry <kperry@godaddy.com>
The ARD spec (ards-project/ard-spec, adr/0009-urn-nid-length-air.md) selects
`air` as the URN namespace identifier: a two-character NID (`ai`) is invalid
under URN NID length rules (NID must exceed two characters), so the lineage
handle is `urn:air:<publisher>:<namespace>:<agent-name>`.

Updates the finder identifier construction, tests, OpenAPI specs, golden mock
data, and docs from `urn:ai:` to `urn:air:`. Aligns with the ai-catalog work,
which already uses `urn:air:`.

Signed-off-by: scourtney-godaddy <scourtney@godaddy.com>
Signed-off-by: kperry <kperry@godaddy.com>

# Conflicts:
#	go.mod
#	internal/config/config.go
@kperry-godaddy
kperry-godaddy marked this pull request as ready for review July 16, 2026 17:45
Copilot AI review requested due to automatic review settings July 16, 2026 17:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an ARD-compliant discovery component (ans-finder) on top of the ANS RA/TL reference stack by introducing a public RA agent-events feed, persisting TL log cursors for those events, and implementing a Finder that polls/indexes/serves search + explore endpoints with OpenAPI-backed docs.

Changes:

  • Add a public, exact-path unauthenticated RA feed (GET /v1/agents/events) backed by delivered outbox rows with TL logId cursors.
  • Introduce the new ans-finder service (poller + projection + SQLite FTS5 index + HTTP handlers), plus config, docs UI spec embedding, and demo scripts.
  • Refactor RA handlers to embed a responder that logs server-side causes for sanitized 500s, and tighten auth anonymous-path matching semantics.

Reviewed changes

Copilot reviewed 112 out of 112 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
scripts/demo/stop.sh Stop script updated for ans-finder + longer SIGTERM grace period.
scripts/demo/start.sh Demo start script now composes finder config, starts ans-finder, and checks finder health/ready.
scripts/demo/run-lifecycle.sh Demo lifecycle now includes an ans-finder search step and updates step numbering.
scripts/demo/common.sh Adds FINDER_URL default for demo scripts.
Makefile Adds build-finder and syncs finder OpenAPI spec into docs UI.
internal/tl/service/checkpoint_helpers_test.go Updates/renames tests to service-layer signature-view mapping.
internal/tl/logstore/signer_errors_test.go Removes negative-path tests for VerifyC2SPECDSA (moved to lognote).
internal/tl/logstore/c2spsigner.go Removes VerifyC2SPECDSA from logstore.
internal/tl/logstore/c2spsigner_test.go Switches verification smoke test to internal/lognote verifier.
internal/ra/outbox/worker.go Persists TL logId into outbox rows; rejects empty logId responses by retrying.
internal/ra/outbox/worker_test.go Adds regression tests for persisting logId and keeping empty-logId rows pending.
internal/ra/handler/v1renewal.go Handler now embeds responder + takes injected logger; uses h.writeError.
internal/ra/handler/v1registration.go Handler now embeds responder + takes injected logger; uses h.writeError.
internal/ra/handler/v1lifecycle.go Handler now embeds responder + takes injected logger; uses h.writeError.
internal/ra/handler/v1events.go New public events-feed handler for GET /v1/agents/events.
internal/ra/handler/v1certificates.go Handler now embeds responder + takes injected logger; uses h.writeError.
internal/ra/handler/registration.go Handler now embeds responder + takes injected logger; uses h.writeError.
internal/ra/handler/lifecycle.go Handler now embeds responder + takes injected logger; uses h.writeError.
internal/ra/handler/lifecycle_test.go Updates fixtures to pass a logger into handler constructors.
internal/ra/handler/identity_handler_test.go Updates fixtures to pass a logger into handler constructors.
internal/ra/handler/errors.go Adds responder to log underlying 500 causes while returning sanitized details.
internal/port/store.go Adds feed read-model port types (FeedRow, FeedQuery, FeedReader).
internal/finder/project/urn.go Adds URN minting + labelization for ARD identifiers.
internal/finder/project/types.go Defines Finder projection output types (Entry/ProjectedEntry/Skips).
internal/finder/project/sanitize.go Adds control/bidi sanitization and strict URL validation gate.
internal/finder/project/internal_test.go Adds white-box tests for projection helper branches.
internal/finder/project/testdata/event_registered.json Adds projection input fixture for AGENT_REGISTERED.
internal/finder/project/testdata/event_registered.golden.json Adds golden output for AGENT_REGISTERED projection.
internal/finder/project/testdata/event_renewed.json Adds projection input fixture for AGENT_RENEWED.
internal/finder/project/testdata/event_renewed.golden.json Adds golden output for AGENT_RENEWED projection.
internal/finder/project/testdata/event_revoked.json Adds projection input fixture for AGENT_REVOKED.
internal/finder/project/testdata/event_revoked.golden.json Adds golden output for AGENT_REVOKED projection.
internal/finder/project/testdata/event_revoked_minimal.json Adds minimal revoked-event fixture.
internal/finder/project/testdata/event_revoked_minimal.golden.json Adds golden output for minimal revoked-event fixture.
internal/finder/project/testdata/event_deprecated.json Adds projection input fixture for AGENT_DEPRECATED.
internal/finder/project/testdata/event_deprecated.golden.json Adds golden output for AGENT_DEPRECATED projection.
internal/finder/project/testdata/event_no_endpoints.json Adds projection input fixture with no endpoints.
internal/finder/project/testdata/event_no_endpoints.golden.json Adds golden output for no-endpoints fixture.
internal/finder/project/testdata/event_no_displayname.json Adds projection input fixture with no display name.
internal/finder/project/testdata/event_no_displayname.golden.json Adds golden output for no-displayname fixture.
internal/finder/project/testdata/event_adversarial_text.json Adds adversarial text fixture for sanitization/URL policy.
internal/finder/project/testdata/event_adversarial_text.golden.json Adds golden output for adversarial text fixture.
internal/finder/project/testdata/event_nonz_offset.json Adds non-Z offset timestamp fixture.
internal/finder/project/testdata/event_nonz_offset.golden.json Adds golden output for non-Z offset fixture.
internal/finder/poller/httpclient.go Implements HTTP feed client with strict base URL policy and response caps.
internal/finder/poller/httpclient_test.go Tests feed client URL policy and fetch behavior.
internal/finder/index/index.go Defines Finder index port + supported filter/facet fields.
internal/finder/index/index_test.go Tests supported-field set and constants.
internal/finder/handler/ratelimit.go Adds global token-bucket limiter for anonymous discovery endpoints.
internal/finder/handler/ratelimit_test.go Tests rate limiter behavior.
internal/finder/handler/errors.go Adds RFC7807 Problem writing with nosniff on Finder responses.
internal/finder/handler/dto_internal_test.go Tests pageToken encode/decode and query hashing.
internal/finder/handler/handler_error_test.go Ensures internal errors are sanitized for anonymous callers.
internal/finder/handler/conformance_test.go Enforces handler response keys match embedded OpenAPI spec.
internal/adapter/store/sqlitefinder/store_edge_test.go Adds store edge-case coverage (reopen, unknown lifecycle, unsupported fields).
internal/adapter/store/sqlitefinder/sqlite.go Adds SQLite FTS5-backed Finder store with embedded migrations.
internal/adapter/store/sqlitefinder/migrations/001_initial.sql Defines initial Finder index schema + FTS + cursor table.
internal/adapter/store/sqlitefinder/search.go Implements ranked search + expiry filtering + deterministic ordering.
internal/adapter/store/sqlitefinder/explore.go Implements facet exploration over the matched set.
internal/adapter/store/sqlitefinder/filter.go Builds safe filter SQL clauses with side-table mapping.
internal/adapter/store/sqlitefinder/ftsquery.go Escapes user text into safe FTS5 MATCH expression.
internal/adapter/store/sqlitefinder/cursor.go Persists/reads poll cursor + last successful poll time.
internal/adapter/store/sqlite/stores_test.go Updates outbox tests for MarkSent(logId) behavior.
internal/adapter/store/sqlite/sqlite.go Runs ANALYZE after migrations to seed planner stats for feed queries.
internal/adapter/store/sqlite/outbox.go Updates MarkSent to atomically set sent_at_ms + log_id.
internal/adapter/store/sqlite/migrations/006_outbox_log_id.sql Adds outbox log_id column + indexes for feed/cursor performance.
internal/adapter/store/sqlite/feed.go Adds FeedStore implementing FeedReader over delivered+logged outbox rows.
internal/adapter/docsui/docsui.go Embeds finder OpenAPI spec into docs UI bundle.
internal/adapter/docsui/docsui_test.go Verifies embedded finder spec matches canonical spec file.
internal/adapter/auth/static.go Adds exact-path anonymous exemptions and safer subtree matching semantics.
internal/adapter/auth/oidc.go Mirrors exact/subtree anonymous matching semantics for OIDC auth.
internal/domain/protocol.go Adds AllProtocols / AllTransports enumerators.
internal/domain/protocol_test.go Tests protocol/transport enumerators contain all known constants.
internal/config/finder.go Adds Finder config types + loader + validation.
internal/config/finder_test.go Tests Finder config defaults, env overrides, and validation.
internal/config/defaults.go Adds Finder defaults and RA events-feed default retention.
internal/config/config.go Adds RA events-feed config and shared URL validation helper.
internal/config/config_test.go Tests RA events-feed retention defaulting/clamping.
config/finder-local.yaml Adds local dev config for ans-finder.
go.mod Promotes yaml.v3 to a direct dependency.
cmd/ans-verify/walk.go Moves checkpoint note parsing/verification to internal/lognote.
cmd/ans-verify/walk_test.go Updates checkpoint verification tests to new lognote-based implementation.
cmd/ans-ra/main.go Wires feed store + events service + handler; adds nosniff middleware; passes logger into handlers; adds exact anonymous exemption for feed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/adapter/store/sqlite/sqlite.go
Comment thread internal/adapter/store/sqlite/outbox.go
Comment thread internal/adapter/store/sqlite/feed.go
Comment thread internal/adapter/store/sqlitefinder/search.go
Comment thread internal/adapter/store/sqlitefinder/explore.go
Comment thread internal/config/config.go Outdated
The EventsFeed.Retention doc comment promised a config-level disable
(non-positive value = no lower bound) that RAConfig.Validate actually
normalizes back to the 720h default. Reword the comment to state the
real behavior: the store's no-floor mode exists only via direct
construction, which tests use.

Signed-off-by: kperry <kperry@godaddy.com>
TestPoller_TickerDrivesSecondRound slept a fixed 120ms and expected a
30ms ticker round to have completed, which flaked under -race on a
loaded CI runner. Poll the index for the expected two entries with a
5s deadline instead: the wait stretches under load rather than
failing, and the store's single pinned connection serializes the
concurrent read with the poller's writes.

Signed-off-by: kperry <kperry@godaddy.com>
@jhateley-godaddy
jhateley-godaddy self-requested a review July 16, 2026 22:05
@kperry-godaddy
kperry-godaddy enabled auto-merge July 16, 2026 22:34
@kperry-godaddy
kperry-godaddy disabled auto-merge July 16, 2026 22:34
@kperry-godaddy
kperry-godaddy force-pushed the feat/ans-finder branch 2 times, most recently from 5060b80 to d058b3a Compare July 16, 2026 22:55
Comment thread internal/config/config.go
if c.TLClient.Timeout <= 0 {
c.TLClient.Timeout = 10 * time.Second
}
if c.EventsFeed.Retention <= 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The /v1/agents/events returns events up to 30 days ago, is it possible to return events from the very beginning?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think we can do this as a fast follow if needed. If users are spinning up the RI though, wouldn't they need to keep it around for 30 days?

runRound started Run in a goroutine, slept a fixed 50ms, and cancelled;
runFor did the same over a fixed duration. On a loaded CI runner the
cancel landed mid-apply, so positive assertions raced the round
(TestPoller_SkipDoesNotAbort and TestPoller_TombstoneNoOpLogsWarn failed
with 'context canceled' during the sqlitefinder apply).

Export the existing single-round method as RunOnce — Run is unchanged
and still wraps it with the interval ticker — and call it synchronously
from runRound: no goroutine, no sleep, no wall-clock dependence. The
wedge-escalation test now drives exactly the five failing rounds the
threshold requires instead of hoping a 15ms ticker fires five times in
300ms, and the unused runFor helper is deleted.

Signed-off-by: kperry <kperry@godaddy.com>
@kperry-godaddy
kperry-godaddy added this pull request to the merge queue Jul 17, 2026
Merged via the queue into main with commit 9dbc5c1 Jul 17, 2026
3 checks passed
@kperry-godaddy
kperry-godaddy deleted the feat/ans-finder branch July 17, 2026 03:13
csnitker-godaddy added a commit that referenced this pull request Jul 17, 2026
…discovery service

Reconciles this branch with main's ARD discovery work (#46) at the two
points where the features genuinely interact, and addresses the PR #47
Copilot review.

Feed visibility for inline-sealed activations
- The agent-events feed (GET /v1/agents/events, the Finder's ingest
  source) is a read model over DELIVERED outbox rows, and the inline
  activation seal bypasses the outbox worker — so a sealed
  AGENT_REGISTERED would never surface on the feed and the Finder would
  never discover the agent. SealAgentEvent now returns the TL ack's
  logId, and VerifyDNS records a PRE-DELIVERED outbox row
  (OutboxStore.RecordSealed: payload + sent_at_ms + log_id at insert)
  inside the activation transaction: feed visibility commits atomically
  with ACTIVE, the worker never claims the row, and the feed serves the
  exact bytes the TL verified. Pinned by
  TestVerifyDNS_SealedActivationIsFeedVisible and end-to-end by the
  lifecycle demo's Finder discovery step.

Finder-parity URN labels
- The Finder mints urn:air:{host}:agents:{label} with the label derived
  from the labelized display name (internal/finder/project/urn.go); the
  catalog derived it from the leftmost DNS label, so search results and
  the published ai-catalog.json handed consumers two different lineage
  identifiers for the same agent. The catalog now applies the Finder's
  derivation (trim, collapse whitespace runs to hyphens, preserve case,
  lowercase host) so both surfaces mint ONE handle — also fixing the
  collision where two distinct same-version agents on one host would
  have shared a leftmost-label URN. A registration whose display name
  is missing or sanitizes away to nothing is not catalog-eligible
  (NO_LABEL), mirroring the Finder's skip. This supersedes the earlier
  leftmost-label choice; the lifecycle demo now asserts the Finder and
  the catalog return the identical URN.

Copilot review fixes
- The 409 AGENT_HOST_TAKEN detail now says a live (ACTIVE or
  DEPRECATED) registration holds the FQDN — DEPRECATED blocks reuse
  too, and the old text implied otherwise.
- A handler-test comment claimed the exclusivity check is re-checked
  atomically at activation; aligned with the documented best-effort
  semantics (the check runs before the inline seal, outside the tx).

Signed-off-by: Connor Snitker <csnitker@godaddy.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants