fix(agents): hide per-run agent API keys from key-management surfaces#1924
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every workflow run mints a scoped API key (
agent:<slug>:<runID>) and revokes it in adefer. 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-keysmid-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, RESTGET /api/v1/api-keys, andbreadbox 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:Instead we match the two positively-identifiable machine shapes:
client_fingerprint IS NOT NULL— auto-managed MCP-client identity rows.workflow_idset (modern) or theagent:<slug>:<runID>name (run keys minted beforeworkflow_idexisted). The name match mirrorsParseAgentKeySlug.Deliberately not done
No hard-delete prune of old revoked agent keys. I'd floated this for unbounded
api_keysgrowth, but the activity feed re-resolves historical agent avatars from the liveapi_keysrow 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.The same
actor_type DEFAULT 'agent'mislabel meansCleanupOrphanedAgentApiKeys(the startup sweep) auto-revokes legacy user keys stampedagentafter 1h. On any instance that had user API keys before20260512(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
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.TestOrchestratorRunNow_{MintRevokeRoundTrip,TripleConcurrency}— their mint-revoke assertions scannedListAPIKeysand went vacuous once agent keys were filtered out. They now queryapi_keysdirectly and assert the key was both minted and revoked (stronger than before).go build/go vet/touched-package integration tests green. (Pre-existingseries_tagstest-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 DEFAULTregression, which this final version fixes structurally.🤖 Generated with Claude Code
https://claude.ai/code/session_01TAhDs9z68QbpRk19xxA55L