Skip to content

fix(keys): unify provider list — harness add-key menu was missing custom - #185

Open
juniperbevensee wants to merge 1 commit into
mainfrom
dev/juniperbevensee/hsm-key-provider-parity
Open

fix(keys): unify provider list — harness add-key menu was missing custom#185
juniperbevensee wants to merge 1 commit into
mainfrom
dev/juniperbevensee/hsm-key-provider-parity

Conversation

@juniperbevensee

Copy link
Copy Markdown
Collaborator

The bug

Adding a key from inside a single harness (/harnesses/[id] → Keys) offered a different provider list than the global /keys page. Most visibly, it had no custom option — 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_PROVIDERS in two files with nothing linking them. They drifted in both directions:

Missing from harness menu Missing from global keys menu
google, custom zai, signal, mattermost, aws-bedrock, google-cloud, helius, coingecko, dehashed, opencorporates, capsolver, open-measures, pexels

Nothing server-side was rejecting customPOST /api/keys does no provider validation and KeyInput.provider is a plain string. The option was simply absent from that array.

The fix

  • New lib/key-providers.ts — one list, plus providerOptions() carrying over the unknown-provider escape hatch the keys page already had (a ?request= prefill would otherwise silently vanish from the harness dropdown).
  • Both pages import it; both local consts deleted.
  • Env-var field added to the harness form. custom needs it and only the global page had it: 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. It 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. (It does contain custom, which is probably what made the inconsistency look stranger than it was.)

Testing

New lib/__tests__/key-providers.test.ts pins the parity itself, since a redeclared local list is exactly how this broke. It fails if either page declares its own KEY_PROVIDERS, stops importing the shared module, or drops the custom env-var field.

  • Verified red against a deliberately reintroduced local const, green after.
  • tsc --noEmit clean.
  • Full suite: 874 passed / 87 files.

Note for a follow-up (not fixed here)

Both google and google-cloud are now offered. google-cloud maps to GOOGLE_CLOUD_API_KEY; bare google has no mapping and falls through to GOOGLE_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

… 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).
@juniperbevensee

Copy link
Copy Markdown
Collaborator Author

Independent audit — not mergeable as-is (2 blocking)

Mechanics are clean: tsc --noEmit passes on the branch and on a trial merge with current main; no rebase needed (clean ort merge); full real-tree suite 922 passed / 88 files merged vs 911 / 87 baseline — exactly the new file, no regressions. The new tests are genuinely non-vacuous: four separate source mutations (reintroduce a page-local list; delete the env-var input; keep the input but drop the submit-body envVar line; make providerOptions ignore the escape hatch) each correctly fail a test.

BLOCKING

B1 — google is offered in the menu but writes a garbage env var.
lib/key-providers.ts:24 offers google; PROVIDER_TO_VAR (lib/services/keys.ts:353-374) has no google entry, so resolveEnvVar falls to the hints.name branch — and the harness form only sends envVar when provider === 'custom' (harnesses/[id]/page.tsx:238). Reproduced against the real service:

provider=google, name="Team Key"  ->  TEAM_KEY_API_KEY=<key>
provider=google, no name          ->  GOOGLE_API_KEY=<key>

"e.g. Team Key" is the form's own placeholder. The runtime does read GOOGLE_API_KEY (hermes-agent-mt: hermes_cli/auth.py:240, tools/tts_tool.py:1809), so the operator adds a working key and the agent gets nothing. This is the same class as the BRAVE_API_KEY vs BRAVE_SEARCH_API_KEY bug fixed in #189 today. Aggravating: this PR newly exposes google on the harness page, and the PR body describes the fallthrough as reaching GOOGLE_API_KEY — true only when Name is left blank.

One-line fix: google: 'GOOGLE_API_KEY'. That mapping already exists twice in this repo (agent-deploy-templates.ts:58, model-catalog.ts:81).

B2 — this is a third hand-maintained copy, not a source of truth.
The write-path authority is PROVIDER_TO_VAR; KEY_PROVIDERS is a hand-typed array with nothing tying it to that map. Seven provider enumerations exist after this PR. The new tests pin parity between the two pages but never cross-check the menu against the write path — and that drift is exactly what B1 is. Deriving KEY_PROVIDERS from Object.keys(PROVIDER_TO_VAR) makes B1 unwritable.

SHOULD-FIX

  • opencorporates name mismatch — writes OPENCORPORATES_API_KEY; runtime reads OPENCORPORATES_API_TOKEN (optional-skills/research/osint-investigation/scripts/fetch_opencorporates.py:176). Another Brave-class bug; worth fixing here.
  • Five more providers write vars nothing readshelius, coingecko, dehashed, open-measures, pexels. Pre-existing on the harness page, but newly exposed on /keys, and a test now enshrines them as a regression guard while the PR claims a completeness audit.
  • google-cloud reaches only docker/deploy/litellm-config.yaml, not the agent. With B1 that gives the dropdown two Google entries, neither working for an agent.
  • Env-var field gate uses the wrong predicateprovider === 'custom' should be !PROVIDER_TO_VAR[provider]. Note providerOptions deliberately appends unknown providers from a ?request= prefill, and those are precisely the ones needing the field but unable to get it (?request=hedra&name=Hedra Prod -> HEDRA_PROD_API_KEY).
  • Interaction with fix(deploy): correct the Google MCP wiring for newly created agents #189 (merged today)fix(deploy): correct the Google MCP wiring for newly created agents #189 shipped the working Google path (GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET for google-multiplayer-mcp). This menu offers google, google-cloud and gemini, none of which reach it. An operator asked for "Google access" now has three wrong options and no right one.

Secret handling — clean

No findings beyond the orphaning issue filed separately. Input is type="password"/autoComplete="off", POST /api/keys strips encryptedValue, no console.* in changed components, and the new env-var field is correctly type="text" (it holds a name, not a secret).

Suggested shape

Export the provider→var map from lib/key-providers.ts, have KeysService import it, derive KEY_PROVIDERS as [...Object.keys(PROVIDER_TO_VAR), 'custom'], add google: 'GOOGLE_API_KEY' and opencorporates: 'OPENCORPORATES_API_TOKEN', and gate the env-var field on !PROVIDER_TO_VAR[provider]. That turns a PR claiming one source of truth into one that has it, and makes the dead-provider list a deliberate pruning decision rather than an unexamined carry-over.

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