fix(keys): unify provider list — harness add-key menu was missing custom - #185
fix(keys): unify provider list — harness add-key menu was missing custom#185juniperbevensee wants to merge 1 commit into
custom#185Conversation
… custom The "add key" dropdown existed twice: KEY_PROVIDERS in app/(dashboard)/keys/page.tsx and a second, unrelated const of the same name in harnesses/[id]/page.tsx. Nothing linked them, so they drifted in both directions — the harness-scoped menu lacked `google` and `custom`, while the global menu lacked the twelve service providers (zai, signal, mattermost, aws-bedrock, google-cloud, helius, coingecko, dehashed, opencorporates, capsolver, open-measures, pexels). The reported symptom was the missing `custom`: a harness could not be given a key for any provider HSM doesn't know by name. Nothing server-side was stopping it — POST /api/keys does no provider validation and KeyInput.provider is a plain string — the option simply was not in that array. Extract both into lib/key-providers.ts as the single list, plus providerOptions() carrying over the unknown-provider escape hatch the keys page had (a ?request= prefill would otherwise vanish from the harness dropdown). `custom` also needs the env-var field, which the harness form never had: with no PROVIDER_TO_VAR entry, resolveEnvVar falls back to the key's name and then to a useless CUSTOM_API_KEY, so a custom key added from a harness would have landed in the wrong variable. The field now renders there too, clears when the provider changes away from custom, and is only sent for custom. MODEL_PROVIDERS is deliberately untouched — that drives the model cascade editor, not credential storage. Tests pin the parity itself, since a redeclared local list is how this broke: they fail if either page declares its own KEY_PROVIDERS, stops importing the shared module, or drops the custom env-var field. Verified red against a reintroduced local const. Full suite green (874).
Independent audit — not mergeable as-is (2 blocking)Mechanics are clean: BLOCKINGB1 —
One-line fix: B2 — this is a third hand-maintained copy, not a source of truth. SHOULD-FIX
Secret handling — cleanNo findings beyond the orphaning issue filed separately. Input is Suggested shapeExport the provider→var map from |
The bug
Adding a key from inside a single harness (
/harnesses/[id]→ Keys) offered a different provider list than the global/keyspage. Most visibly, it had nocustomoption — so a harness could not be given a key for any provider HSM doesn't know by name.Why
The list existed twice, as two unrelated
const KEY_PROVIDERSin two files with nothing linking them. They drifted in both directions:google,customzai,signal,mattermost,aws-bedrock,google-cloud,helius,coingecko,dehashed,opencorporates,capsolver,open-measures,pexelsNothing server-side was rejecting
custom—POST /api/keysdoes no provider validation andKeyInput.provideris a plainstring. The option was simply absent from that array.The fix
lib/key-providers.ts— one list, plusproviderOptions()carrying over the unknown-provider escape hatch the keys page already had (a?request=prefill would otherwise silently vanish from the harness dropdown).customneeds it and only the global page had it: with noPROVIDER_TO_VARentry,resolveEnvVarfalls back to the key's name and then to a uselessCUSTOM_API_KEY, so a custom key added from a harness would have landed in the wrong variable. It clears when the provider changes away fromcustom, and is only sent forcustom.MODEL_PROVIDERSis deliberately untouched — that drives the model cascade editor, not credential storage. (It does containcustom, which is probably what made the inconsistency look stranger than it was.)Testing
New
lib/__tests__/key-providers.test.tspins the parity itself, since a redeclared local list is exactly how this broke. It fails if either page declares its ownKEY_PROVIDERS, stops importing the shared module, or drops the custom env-var field.tsc --noEmitclean.Note for a follow-up (not fixed here)
Both
googleandgoogle-cloudare now offered.google-cloudmaps toGOOGLE_CLOUD_API_KEY; baregooglehas no mapping and falls through toGOOGLE_API_KEY. Distinct and defensible, but the two are easy to confuse in the dropdown — worth a label or a merge later.🤖 Generated with Claude Code