Skip to content

fix(registry): require declared agent type + auto-backfill primary_brand_domain#4235

Merged
EmmaLouise2018 merged 2 commits intomainfrom
EmmaLouise2018/registry-operator-member-visibility
May 8, 2026
Merged

fix(registry): require declared agent type + auto-backfill primary_brand_domain#4235
EmmaLouise2018 merged 2 commits intomainfrom
EmmaLouise2018/registry-operator-member-visibility

Conversation

@EmmaLouise2018
Copy link
Copy Markdown
Contributor

@EmmaLouise2018 EmmaLouise2018 commented May 8, 2026

Summary

  • save_agent (Addie MCP tool): type now required in input schema (8-value enum: brand, rights, measurement, governance, creative, sales, buying, signals). Persists on insert; updates on re-save when an owner corrects an earlier wrong type.
  • POST /api/me/agents: returns 400 type is required on missing/invalid/'unknown'. PATCH /api/me/agents/:url: returns 400 invalid_type on invalid; omission preserves existing.
  • Mutation helper backfills member_profiles.primary_brand_domain atomically with the JSONB write (under the same FOR UPDATE row lock) when the column is null AND every agent in the resulting array agrees on one hostname (after stripping www.). Conflicts are skipped — picking one would mis-key registry lookups.
  • Operator endpoint (/api/registry/operator): drops silent || "unknown" fallback. Out-of-enum values still serialize as "unknown" to preserve OperatorLookupResultSchema, but a warn-level log fires with domain, url, storedType, profileSlug so ops can catch corrupt rows.
  • OpenAPI: new MemberAgentTypeInput enum (sans 'unknown'); MemberAgentInputSchema.type now required; read-side MemberAgentSchema.type tightened from optional to required (matches what the operator route always emits).
  • Addie's intake script (behaviors.md): "do not ask about agent type" replaced with "always ask, never guess." Owner declares; if the user describes capabilities, Addie may suggest a fit but the user must confirm before save.

Why

Caught in production: a member registered an agent at https://www.harvingupta.xyz/api/mcp and saw it correctly in /dashboard/agents, but GET /api/registry/operator?domain=harvingupta.xyz returned { member: null, agents: [] }. Two compounding gaps:

  1. The agent's stored type was absent. save_agent's schema explicitly did not accept a type field — it relied on a server-side capability-snapshot resolution that silently produced no value when the probe failed. POST /api/me/agents accepted Partial<AgentConfig> and never validated type either. The operator endpoint masked the missing field with type: ac.type || "unknown", so bad data flowed straight through to the public response.
  2. primary_brand_domain was NULL. Agent registration writes member_profiles.agents JSONB but never backfilled primary_brand_domain, and the public operator lookup keys exact-match on that column. Result: a registered agent that the profile owner could see in the dashboard but no peer could discover via the registry.

Fixing both gaps at once because they're tightly coupled — the type guess and the missing brand-domain backfill were each independently sufficient to make the agent invisible.

Behavior change

POST /api/me/agents and save_agent now reject writes that don't declare type. Audit of write surfaces:

  • Dashboard "+ Register agent" (server/public/dashboard-agents.html) — redirects to Addie chat; Addie's updated intake script asks for type before calling save_agent. ✅ unaffected
  • Addie save_agent MCP tool — schema enforces type at the JSON-RPC layer; runtime check is the suspenders. ✅ unaffected for new conversations; existing prompts that paste-it-all need to include type
  • POST /api/me/agents REST — any external caller posting { url, name, visibility } without type will start getting 400. Quantify before merge: search downstream SDK callers (@adcp/client, internal storefront integrations) for unset type. None found in this repo's tests; updated 14 test sites here to declare type: 'sales'.
  • PATCH /api/me/agents/:url — omitting type preserves the existing value (no behavior change for existing callers).

Migration

Existing rows handled by a manual migration on the prod pod (deliberately out of release_command so it doesn't auto-fire on deploy):

  • Backfilled primary_brand_domain from the unanimous agent hostname.
  • Defaulted any agent with missing/invalid type to sales (per owner instruction).

The bulk-default-to-sales is a guess applied at migration time — the very pattern this PR forbids for new writes. Tracked as a follow-up: surface a "your agent's type was bulk-set, confirm or correct" banner on /dashboard/agents for the affected cohort. See #4236.

What changed (quantified)

  • 8 valid AgentType values enforced at input; 'unknown' reserved for server-side smuggle protection only.
  • 3 write surfaces tightened: POST /api/me/agents, PATCH /api/me/agents/:url, save_agent MCP tool.
  • 1 read surface tightened: MemberAgentSchema.type from optional → required (matches operator route behavior).
  • 1 silent || fallback removed; replaced with a warn-level structured log on corrupt rows.
  • 14 existing test POSTs updated to declare type (covers both member-agents-api and member-agents-auto-bootstrap integration suites).
  • 7 new integration tests pinning the new contract (see test plan).
  • 6 source files + 1 changeset + 2 test files = 9 files changed.

Out of scope

Test plan

  • tsc --noEmit -p server/tsconfig.json — clean
  • vitest run server/tests/unit — only 5 pre-existing failures (taxonomy enum sync, missing storyboard yaml); confirmed against main by stashing changes; none touch modified files
  • Build passes (npm run build)
  • Updated 14 existing integration test POSTs to declare type: 'sales' so they exercise the new gate cleanly rather than tripping it
  • Added 7 new integration tests in tests/integration/member-agents-api.test.ts:
    • POST 400 when type is missing
    • POST 400 when type is 'unknown' (reserved server-side)
    • POST 400 when type is out-of-enum (e.g. legacy 'seller')
    • PATCH 400 invalid_type, omission preserves existing, valid swap updates
    • POST backfills primary_brand_domain from agent hostname when null + unanimous
    • POST does NOT backfill when agents disagree on hostname
    • POST does NOT overwrite primary_brand_domain when already set
  • Integration tests in CI (require live Postgres): tests/integration/member-agents-api.test.ts, tests/integration/agent-visibility-e2e.test.ts, tests/integration/member-agents-auto-bootstrap.test.ts
  • Smoke after deploy: register a fresh agent through Addie, confirm GET /api/registry/operator?domain=<owner-brand> returns the agent
  • Smoke after deploy: POST /api/me/agents with no type returns 400 type is required

…and_domain

Make agent type a required, owner-declared field at every registration
surface, and auto-populate primary_brand_domain when an agent is registered
against a profile that has none. Prevents the "registered agent invisible
in /api/registry/operator" failure mode where a profile had a typed agent
in dashboard but registry lookup returned member: null, agents: [].

- save_agent (Addie): require type input enum (8 values, no 'unknown'),
  persist on insert and update on re-save. Behaviors.md intake now asks for
  type before save_agent — replaces the old "do not ask about agent type"
  rule.
- POST /api/me/agents: 400 on missing/invalid/'unknown' type. PATCH
  validates type when present (omission preserves existing).
- Mutation helper backfills primary_brand_domain atomically when null AND
  every agent agrees on the same hostname (after stripping www.). Conflicts
  are skipped — picking one would mis-key registry lookups.
- Operator endpoint drops the silent || "unknown" fallback; out-of-enum
  values still serialize as "unknown" to keep the schema contract but a
  warn-level log fires so corrupt rows are visible.
- OpenAPI: new MemberAgentTypeInput enum (sans 'unknown') marks type
  required on POST input; descriptions corrected.
…ad schema

Addresses Brian's review on #4235.

- Add 7 integration tests in member-agents-api.test.ts pinning the new
  contract: POST 400 on missing/unknown/out-of-enum type, PATCH
  invalid_type with omission-preserves-existing, primary_brand_domain
  auto-backfill on null + unanimous hostname, no backfill on conflict,
  no overwrite when already set.
- Update 14 existing POST sites in member-agents-api.test.ts and
  member-agents-auto-bootstrap.test.ts to declare type: 'sales' so they
  exercise the new gate cleanly rather than tripping it.
- Tighten MemberAgentSchema.type from optional to required so the OpenAPI
  read shape matches what the operator route always emits.

Follow-ups filed: #4236 (surface bulk-defaulted sales type to owners),
#4237 (corrupt-row diagnostics sentinel on operator response).
@EmmaLouise2018 EmmaLouise2018 merged commit b9b189f into main May 8, 2026
14 checks passed
@EmmaLouise2018 EmmaLouise2018 deleted the EmmaLouise2018/registry-operator-member-visibility branch May 8, 2026 15:36
EmmaLouise2018 added a commit that referenced this pull request May 8, 2026
…e seed + read-side CTE)

Agents registered via POST /api/me/agents or save_agent live in
member_profiles.agents JSONB but never landed an agent_registry_metadata
row. The compliance heartbeat's known_agents CTE unioned only
discovered_agents and agent_registry_metadata, so member-registered
agents were invisible to the heartbeat — agent_compliance_status stayed
empty, /api/registry/agents/<url>/compliance returned status:"unknown"
forever, regardless of the 12h cycle.

Fix shape — both write-side and read-side, mirrors PR #4235.

- Write-side: applyMemberAgentMutation and save_agent both upsert
  agent_registry_metadata atomically with the JSONB write. ON CONFLICT
  DO NOTHING preserves owner-customized lifecycle / interval / opt-out.
- Read-side defense in depth: known_agents CTE in getAgentsDueForCheck
  gains a third leg unioning member_profiles.agents URLs. ORDER BY adds
  agent_url tiebreaker.

3 new integration tests pin the contract: POST seeds metadata when none
exists, POST preserves customized metadata on re-register,
getAgentsDueForCheck picks up an agent that lives only in
member_profiles.agents (read-side CTE in isolation).

Manual migration run on the pod backfills agent_registry_metadata rows
for existing member-profile agents that have no metadata row.
bokelley pushed a commit that referenced this pull request May 8, 2026
Generated build artifacts reflecting schema changes already merged to main
(#4235 agent type required, onboarding schema addition). Committed to keep
dist/schemas in sync with source.

https://claude.ai/code/session_01DafX6UtByExTqbPcJHXFnT
bokelley pushed a commit that referenced this pull request May 9, 2026
…e seed + read-side CTE) (#4252)

Agents registered via POST /api/me/agents or save_agent live in
member_profiles.agents JSONB but never landed an agent_registry_metadata
row. The compliance heartbeat's known_agents CTE unioned only
discovered_agents and agent_registry_metadata, so member-registered
agents were invisible to the heartbeat — agent_compliance_status stayed
empty, /api/registry/agents/<url>/compliance returned status:"unknown"
forever, regardless of the 12h cycle.

Fix shape — both write-side and read-side, mirrors PR #4235.

- Write-side: applyMemberAgentMutation and save_agent both upsert
  agent_registry_metadata atomically with the JSONB write. ON CONFLICT
  DO NOTHING preserves owner-customized lifecycle / interval / opt-out.
- Read-side defense in depth: known_agents CTE in getAgentsDueForCheck
  gains a third leg unioning member_profiles.agents URLs. ORDER BY adds
  agent_url tiebreaker.

3 new integration tests pin the contract: POST seeds metadata when none
exists, POST preserves customized metadata on re-register,
getAgentsDueForCheck picks up an agent that lives only in
member_profiles.agents (read-side CTE in isolation).

Manual migration run on the pod backfills agent_registry_metadata rows
for existing member-profile agents that have no metadata row.
bokelley added a commit that referenced this pull request May 9, 2026
… eval → dashboard, requeue endpoint (#4265)

* fix(compliance): heartbeat pre-stamp TTL, dry_run correctness, manual eval → dashboard status, requeue endpoint

Fixes three bugs reported in #4253 (comply re-runner stale status) plus adds
a member-facing requeue workaround:

1. compliance-heartbeat: pre-stamp uses NOW()+30min lock TTL instead of NOW()
   so a mid-loop process crash re-queues within 30 min rather than blocking
   for the full check_interval (default 12 h).
2. complianceResultToDbInput: remove hardcoded dry_run:true; heartbeat paths
   now set dry_run:false explicitly so scheduled runs are marked authoritative.
3. evaluate_agent_quality: call complianceDb.recordComplianceRun() after
   agentContextDb.recordTest() so manual runs update the dashboard comply
   status immediately (dry_run:false so they count alongside heartbeat runs).
4. New POST /api/registry/agents/{url}/monitoring/requeue endpoint + dashboard
   "Requeue comply" button: clears last_checked_at so the agent is picked up
   on the next heartbeat cycle (~1 hour). Owner-auth + 60s per-agent rate limit.

Refs #4253

https://claude.ai/code/session_01DafX6UtByExTqbPcJHXFnT

* fix(compliance): requeueForHeartbeat upsert for first-run agents

Bare UPDATE silently no-ops when agent has no existing row in
agent_compliance_status. Switch to INSERT ... ON CONFLICT DO UPDATE
so the requeue endpoint works even for agents that have never been
through the heartbeat cycle.

https://claude.ai/code/session_01DafX6UtByExTqbPcJHXFnT

* chore(dist): compile member-agents and onboarding OpenAPI schemas

Generated build artifacts reflecting schema changes already merged to main
(#4235 agent type required, onboarding schema addition). Committed to keep
dist/schemas in sync with source.

https://claude.ai/code/session_01DafX6UtByExTqbPcJHXFnT

---------

Co-authored-by: Claude <noreply@anthropic.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.

1 participant