Skip to content

fix(registry): canonicalize agent_url on the registered path (#3573)#4551

Open
brandonling27 wants to merge 1 commit into
adcontextprotocol:mainfrom
brandonling27:fix/3573-canonicalize-agent-url-registered-path
Open

fix(registry): canonicalize agent_url on the registered path (#3573)#4551
brandonling27 wants to merge 1 commit into
adcontextprotocol:mainfrom
brandonling27:fix/3573-canonicalize-agent-url-registered-path

Conversation

@brandonling27
Copy link
Copy Markdown

@brandonling27 brandonling27 commented May 14, 2026

Summary

Closes #3573.

The crawler/discovered path already canonicalized agent_url via canonicalizeAgentUrl (write site in crawler.ts:reconcileLegacyAdagentsAgents, read-side SQL safety net in federated-index-db.ts). The member-profile registered path did not — POST/PATCH/DELETE on /api/me/agents, the bulk PUT /api/me/member-profile, and Addie's save_agent/remove_saved_agent all wrote raw URLs into member_profiles.agents JSONB and agent_registry_metadata. Two writes for the same logical agent differing only in case or trailing slash landed as separate rows, and the JS map joins in FederatedIndexService.lookupDomain / listAllAgents enriched on raw string equality — so the member badge silently dropped off any discovered authorization whose URL differed from the registered one only in case or trailing slash.

This change is forward-only, application-layer only (no DB migration, per #3573's "out of scope"). Canonicalize at every member-side write boundary; canonicalize both the map key and the lookup key in every JS read site so legacy non-canonical rows continue to match canonical inputs until the member next saves.

Write sites (canonicalize, reject ?/#, 400 on null)

  • POST/PATCH/DELETE in server/src/routes/member-agents.ts (handler boundary, mirroring the precedent at routes/registry-api.ts:7370-7377)
  • Bulk PUT /api/me/member-profile in server/src/routes/member-profiles.ts
  • Addie save_agent and remove_saved_agent in server/src/addie/mcp/member-tools.ts
  • Defense-in-depth in applyMemberAgentMutation's agent_registry_metadata seed loop so any future writer that forgets is still caught

Read sites (canonical key with ?? raw fallback)

  • FederatedIndexService.listAllAgents map key
  • FederatedIndexService.lookupDomain map key plus the two lookup sites (auth.agent_url and claim.discovered_by_agent)
  • FederatedIndexService.getAllAgentDomainPairs (bulk snapshot reader)

Tests

  • server/tests/integration/member-agents-api.test.ts — POST collapses mixed-case/trailing-slash to canonical; idempotency across forms yields one JSONB row + one agent_registry_metadata row; PATCH/DELETE match canonical rows from non-canonical url-encoded paths; PATCH accepts case/slash-only body.url differences; 400 on query/fragment/wildcard.
  • server/tests/integration/registry-crawler-cache.test.tsPin 1: registered https://agent.example/ + discovered https://agent.example collapse with member-badge enrichment. Pin 2: scheme mismatch (http:// vs https://) intentionally does NOT collapse — different security posture per fix(registry): canonicalize agent_url before registered/discovered collapse (trailing slash + scheme) #3573.

Trade-offs

  • Path case is lowercased (existing canonicalizeAgentUrl behavior) rather than scheme+host-only as fix(registry): canonicalize agent_url before registered/discovered collapse (trailing slash + scheme) #3573 phrased it. Chosen for consistency with the discovered side; switching both sides to a stricter canonicalizer would have a much larger blast radius (every existing caller in registry-api.ts, the SQL safety net in federated-index-db.ts, etc.).
  • No backfill. Existing non-canonical rows in member_profiles.agents and agent_registry_metadata persist until the member next saves. Read-side canonicalization in federated-index.ts papers over this for the registry surface; direct readers of agent_registry_metadata (compliance heartbeat, lifecycle dashboards) will continue to see un-collapsed legacy rows until they're rewritten. The issue explicitly defers cleanup ("write a one-off cleanup if a sweep finds collisions").
  • brand.json will publish the canonical form for any public agent whose stored URL gets rewritten on next save. Believed harmless (same URL, normalized) but worth flagging for downstream consumers.

Test plan

  • npm run typecheck — passes locally
  • DATABASE_URL=… npx vitest run server/tests/integration/member-agents-api.test.ts — new canonicalization cases
  • DATABASE_URL=… npx vitest run server/tests/integration/registry-crawler-cache.test.ts — Pin 1 and Pin 2
  • Manual: POST https://Example.com/Agent/ to /api/me/agents; confirm response and DB row both show https://example.com/agent. Crawl a fixture authorizing https://EXAMPLE.com/agent; confirm lookupDomain returns the member badge on the discovered authorization.

🤖 Generated with Claude Code

…le) path

Closes adcontextprotocol#3573.

The crawler/discovered path already routed agent_url through
`canonicalizeAgentUrl` (write site in `crawler.ts:reconcileLegacyAdagentsAgents`,
read-side SQL safety net in `federated-index-db.ts`). The member-profile
registered path did not — POST/PATCH/DELETE on `/api/me/agents`, the bulk
`PUT /api/me/member-profile` handler, and Addie's `save_agent` all wrote
raw URLs into `member_profiles.agents` JSONB and `agent_registry_metadata`.
Two writes for the same logical agent differing only in case or trailing
slash landed as separate rows, and the JS map joins in
`FederatedIndexService.lookupDomain` / `listAllAgents` enriched on raw
string equality — so the member badge silently dropped off any discovered
authorization whose URL differed from the registered one only in case or
trailing slash.

Forward-only application-layer fix (no DB migration, per adcontextprotocol#3573's "out of
scope"). Canonicalize at every member-side write boundary; canonicalize
both the map key and the lookup key in every JS read site so legacy
non-canonical rows continue to match canonical inputs until the member
next saves.

Write sites (canonicalize, reject ?/#, 400 on null):
- POST/PATCH/DELETE in `routes/member-agents.ts` (handler boundary)
- bulk `PUT /api/me/member-profile` in `routes/member-profiles.ts`
- Addie `save_agent` and `remove_saved_agent` in
  `addie/mcp/member-tools.ts`
- defense-in-depth in `applyMemberAgentMutation`'s seed loop so any
  future writer that forgets is still caught

Read sites (canonical key with `?? raw` fallback):
- `FederatedIndexService.listAllAgents` map key
- `FederatedIndexService.lookupDomain` map key plus the two lookup
  sites (`auth.agent_url` and `claim.discovered_by_agent`)
- `FederatedIndexService.getAllAgentDomainPairs` (bulk snapshot reader)

Tests:
- `member-agents-api.test.ts` — POST collapses mixed-case/trailing-slash
  to canonical; idempotency across forms yields one JSONB row + one
  metadata row; PATCH/DELETE match canonical rows from non-canonical
  url-encoded paths; PATCH accepts case/slash-only body.url differences;
  400 on query/fragment/wildcard.
- `registry-crawler-cache.test.ts` — Pin 1: registered
  `https://agent.example/` + discovered `https://agent.example` collapse
  with member-badge enrichment. Pin 2: scheme mismatch (http vs https)
  intentionally does NOT collapse — different security posture per
  adcontextprotocol#3573.

Trade-offs:
- Path-case is lowercased (existing `canonicalizeAgentUrl` behavior)
  rather than scheme+host-only as adcontextprotocol#3573 phrased it — chosen for
  consistency with the discovered side; switching both sides to a
  stricter canonicalizer would have a much larger blast radius.
- Existing non-canonical rows in `member_profiles.agents` and
  `agent_registry_metadata` are not backfilled. Read-side
  canonicalization in `federated-index.ts` papers over this for the
  registry surface; direct readers of `agent_registry_metadata`
  (heartbeat, dashboards) will continue to see un-collapsed legacy rows
  until the member next saves.

Verification:
- `npm run typecheck` ✓
- Affected unit tests ✓
- Integration tests require a running Postgres; run with
  `DATABASE_URL=… npx vitest run server/tests/integration/member-agents-api.test.ts server/tests/integration/registry-crawler-cache.test.ts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aao-ipr-bot
Copy link
Copy Markdown

aao-ipr-bot Bot commented May 14, 2026

IPR Policy Agreement Required

@brandonling27 — thanks for the contribution. Before this PR can be merged, the AgenticAdvertising.Org IPR Policy requires your agreement.

To agree, post a new comment on this PR with the exact phrase:

I have read the IPR Policy

Your signature is recorded once and covers all contributions to AAO repositories. See signatures/README.md for what gets recorded and why.

@brandonling27
Copy link
Copy Markdown
Author

I have read the IPR Policy

@aao-ipr-bot
Copy link
Copy Markdown

aao-ipr-bot Bot commented May 14, 2026

IPR Policy — signed

Thanks, @brandonling27. Your agreement to the IPR Policy is recorded at signatures/ipr-signatures.json and applies to all AAO repositories.

aao-ipr-bot Bot pushed a commit that referenced this pull request May 14, 2026
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.

fix(registry): canonicalize agent_url before registered/discovered collapse (trailing slash + scheme)

2 participants