Skip to content

fix(agents): hide per-run agent API keys from key-management surfaces#1924

Merged
canalesb93 merged 1 commit into
mainfrom
fix/agent-run-key-ux
Jun 30, 2026
Merged

fix(agents): hide per-run agent API keys from key-management surfaces#1924
canalesb93 merged 1 commit into
mainfrom
fix/agent-run-key-ux

Conversation

@canalesb93

Copy link
Copy Markdown
Owner

Problem

Every workflow run mints a scoped API key (agent:<slug>:<runID>) and revokes it in a defer. The mint-and-revoke itself is good design — least-privilege, per-run attribution, no long-lived agent secret to harvest (see .claude/rules/agents.md "Mint-and-revoke"). But the plumbing leaked into the user-facing key list: ephemeral keys flickered into /settings/api-keys mid-run and piled up under "revoked", reading as unexplained key activity the user never created and can't meaningfully manage.

Fix

Filter agent machinery out of ListApiKeys — the single chokepoint behind the admin UI, REST GET /api/v1/api-keys, and breadbox keys list. An agent's identity + run history already surface on the Workflows / agent-runs pages.

The filter is structural, not actor_type-based, and that distinction is load-bearing:

The actor_type column was added with DEFAULT 'agent' (migration 20260512061200). So every key created before that migration — including legitimate user keys — was backfilled to actor_type='agent'. A naive WHERE actor_type <> 'agent' filter would hide those still-valid user credentials from every management surface while leaving them live and un-revocable.

Instead we match the two positively-identifiable machine shapes:

  • client_fingerprint IS NOT NULL — auto-managed MCP-client identity rows.
  • per-run keys — workflow_id set (modern) or the agent:<slug>:<runID> name (run keys minted before workflow_id existed). The name match mirrors ParseAgentKeySlug.
SELECT * FROM api_keys
WHERE client_fingerprint IS NULL
  AND workflow_id IS NULL
  AND name NOT LIKE 'agent:%:%'
ORDER BY created_at DESC;

Deliberately not done

No hard-delete prune of old revoked agent keys. I'd floated this for unbounded api_keys growth, but the activity feed re-resolves historical agent avatars from the live api_keys row by id (feed.go -> ResolveAgentSlugForActor / GetAgentIdentityByApiKeyID). Deleting would degrade past attribution. The revoked rows are deliberately load-bearing; growth is the deliberate cost of durable identity.

⚠️ Pre-existing landmine surfaced (separate decision)

The same actor_type DEFAULT 'agent' mislabel means CleanupOrphanedAgentApiKeys (the startup sweep) auto-revokes legacy user keys stamped agent after 1h. On any instance that had user API keys before 20260512 (possibly prod), those may already have been silently revoked. This PR's structural filter keeps such keys visible/revocable, but does not un-revoke or re-label them — that's a credential-semantics change on prod data that deserves your explicit call. Happy to follow up with a corrective backfill migration if you want it.

Tests

  • New TestListAPIKeys_ExcludesAgentKeys — asserts run-key-shaped keys and MCP-client rows are hidden, while user, system, and the legacy-mislabeled-user-key (actor_type='agent', normal name) stay visible. That last case is the regression guard for the trap above.
  • Fixed TestOrchestratorRunNow_{MintRevokeRoundTrip,TripleConcurrency} — their mint-revoke assertions scanned ListAPIKeys and went vacuous once agent keys were filtered out. They now query api_keys directly and assert the key was both minted and revoked (stronger than before).

go build/go vet/touched-package integration tests green. (Pre-existing series_tags test-DB schema drift from another worktree is unrelated.)

Review

Authored with a high-effort workflow-backed code review (8 finders + adversarial verify) — it caught the actor_type DEFAULT regression, which this final version fixes structurally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TAhDs9z68QbpRk19xxA55L

Every workflow run mints a scoped API key (agent:<slug>:<runID>) and
revokes it in a defer. That's correct for least-privilege + per-run
attribution, but the ephemeral keys flickered into /settings/api-keys
mid-run and piled up under "revoked", reading as unexplained key activity.

Filter agent machinery out of ListApiKeys — the single chokepoint behind
the admin UI, REST GET /api/v1/api-keys, and `breadbox keys list`.

The filter is STRUCTURAL, not actor_type-based. The actor_type column
defaults to 'agent' (migration 20260512061200), so keys created before
that migration — including legitimate user keys — carry actor_type='agent'.
Filtering on `actor_type <> 'agent'` would hide those still-valid user
credentials from every management surface. Instead match the positively
identifiable machine shapes: client_fingerprint (MCP-client identity rows)
and per-run keys (workflow_id set, or the agent:<slug>:<runID> name).

Deliberately NOT pruning old revoked agent keys: the activity feed
re-resolves historical agent avatars from live api_keys rows by id
(ResolveAgentSlugForActor), so deleting would degrade past attribution.

Tests:
- New TestListAPIKeys_ExcludesAgentKeys, including the legacy-mislabeled-
  user-key regression guard (actor_type='agent' but normal name → visible).
- Fix TestOrchestratorRunNow_{MintRevokeRoundTrip,TripleConcurrency}: their
  mint-revoke assertions scanned ListAPIKeys and went vacuous once agent
  keys were filtered out; they now query api_keys directly and assert the
  key was both minted and revoked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAhDs9z68QbpRk19xxA55L
@canalesb93 canalesb93 merged commit 3f6c0b2 into main Jun 30, 2026
8 checks passed
@canalesb93 canalesb93 deleted the fix/agent-run-key-ux branch June 30, 2026 16:29
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