fix(registry): reconcile agent_card identity from trusted workspaces row (internal#492) #1427
Reference in New Issue
Block a user
Delete Branch "fix/agent-card-identity-reconcile-internal-492"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Settings → Workspace Tokens returned
GET /workspaces/global/tokens → 500 {"error":"failed to list tokens"}whenever opened with no canvas node selected. Token CREATE in that view broke the same way.Root cause:
SettingsPanelpasses the literal sentinel"global"as the workspace id when no node is selected. The backend queries theuuidworkspace_idcolumn with it → Postgresinvalid input syntax for type uuid: "global"→ opaque 500.SecretsTabalready handles the sentinel (api/secrets.tsreroutes"global"→/settings/secrets);TokensTabdid not — that asymmetry was the bug.Pre-existing since 2026-04-13 — NOT a regression.
Workaround (until merged): select a workspace node before opening the tab, or use the Org API Keys tab.
Changes
Frontend (the user-visible fix) —
canvas/src/components/settings/TokensTab.tsxTokensTabis now sentinel-aware exactly likeSecretsTab. WhenworkspaceId === 'global'it no longer calls/workspaces/global/tokens— it renders a clean state ("Select a workspace node first") that points the user at the Org API Keys tab (the existing org-wide surface). No 500, no scary error UI.TokensTab's local error banner (verified in code — there is no separate error widget tied to this call). It resolves with this guard.Backend (defense-in-depth, same PR) —
workspace-server/internal/handlers/tokens.goList/Create/Revokevalidatec.Param("id")as a UUID up front and return400 {"error":"invalid workspace id"}instead of leaking a DB type error as a 500. Mirrors the existinguuid.Parseguard inhandlers/activity.go.log.Printfon theListquery-error branch — it was the only token handler silently swallowing the DB error, which is why this incident had zero log trail.Product note for CTO
There is no
/workspaces/global/tokensendpoint — workspace tokens are inherently per-workspace; the org-wide equivalent is the separate Org API Keys tab (OrgTokensTab). So unlikeSecretsTab(which reroutes to a real global-secrets endpoint), the lowest-risk safe behavior here is a disabled state + pointer to Org API Keys rather than a reroute. Flag if a different UX is wanted — this was the lowest-risk choice, not a hard product decision.Test plan
go build ./...+go vet ./internal/handlers/— cleango test ./internal/handlers/— full suite pass (incl. new non-UUID 400 table test asserting List/Create/Revoke short-circuit before any DB call)tsc --noEmit— zero errors in production (non-test) code; changed component compiles cleanvitest run src/components/settings/__tests__/— 183/183 pass, incl. new sentinel tests (no API call + Org-pointer rendered + no error banner)🤖 Generated with Claude Code
The runtime builds its AgentCard from config.name, which the CP-regenerated /configs/config.yaml sets to the raw workspace UUID — so /registry/register stored (and /.well-known/agent-card.json + peer agent_card_url served) a card with name=<uuid>, description="", role=null, even though the operator-controlled workspaces.name DB column holds the friendly name the canvas shows ("Claude Code Agent"). Fleet-wide; live registry confirmed name=UUID for ws 3b81321b while workspaces.name="Claude Code Agent". Server-side, platform-controlled repair at the register upsert: when the runtime-supplied agent_card.name is empty or equals the workspace UUID, substitute the trusted workspaces.name; default a blank description from the reconciled name; default role from workspaces.role. Gaps are only FILLED — a card already carrying a real friendly name (external channel agents) is never downgraded; malformed/edge cards are stored verbatim (no-worse-than-before). Identity stays platform-sourced from the operator-controlled DB row — the agent gains no self-edit. Works for all runtimes without touching every template or the CP generator. The WORKSPACE_ONLINE broadcast now carries the reconciled card so the canvas live-updates with the friendly name. Pure helper (agent_card_reconcile.go) is exhaustively unit-tested without DB/HTTP. Upstream CP config.yaml regeneration, the missing role key in the runtime register payload, and an editable description/skills surface are RFC-scoped in internal#492. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>core-be review
Reviewed the three-file diff — the design and implementation are both solid.
reconcileAgentCardIdentity: Pure function, no DB/HTTP/globals — correct. The "only fill gaps" contract is well-documented. The type assertion
m["name"].(string)is safe because the json package only producesbool,float64,string,[]any,map[string]any, ornilfrom unmarshalling, never rawinterface{}.Edge cases handled correctly:
nullinput (json.RawMessage(nil)) →json.Unmarshal(nil, &m)returns error → verbatim return (safe fallback){}→ unmarshals tomap[string]any{}(not nil) → all type assertions fail gracefully, no changeddbRole = ""→ thedbRole != ""guard at line ~93 prevents writing""as roleregistry.go call site (L329-348): The
reconciledCardvariable shadowingagentCardStris clean. Thelog.Printfonly fires whendid == true, avoiding log spam for the common no-op case. UsingdbName.Stringon asql.NullString(empty) gives"", which is the right sentinel.Future consolidation note (non-blocking): The
SELECT url FROM workspaces WHERE id = $1at line 396 (DB URL for Redis cache) could be combined with the reconciliationSELECT name, role FROM workspaces WHERE id = $1into oneSELECT url, name, role FROM workspaces WHERE id = $1— both run on the same cold path (first register after boot) and query the same row. The reconciliation lookup runs before the URL lookup in the current code order, so reordering + combining would save one DB round-trip on the first-boot register path. Not a blocker for this PR.No blockers. LGTM
[core-security-agent] APPROVED — pure function, no DB/HTTP/globals; platform-side DB name fills identity gaps from trusted row; OWASP X/X clean
[core-qa-agent] APPROVED — Go 14/14 pass. Fix: reconcile agent_card identity from trusted workspaces row (registry.go + agent_card_reconcile.go). e2e: N/A — platform not running locally (see CI).
Five-axis (security focus): reconcile runs AFTER C18 token auth + SSRF check; identity from trusted workspaces DB row not agent input; gap-only fill, placeholder-UUID guarded, no-clobber of real names; agent cannot self-set name/role; verbatim fallback. No over-reach. Clean.
Five-axis (SRE): pure unit-tested function (7 table cases + field-preservation); one PK SELECT per register (negligible); broadcast uses reconciled card consistently with persisted; unchanged path byte-identical. Clean.