Mask provider secrets; a coding agent without its CLI is not Configured (QA F5, F6) - #225
Open
Broccolito wants to merge 4 commits into
Open
Mask provider secrets; a coding agent without its CLI is not Configured (QA F5, F6)#225Broccolito wants to merge 4 commits into
Broccolito wants to merge 4 commits into
Conversation
F5 of the 2026-09-10 provider QA run. The form every built-in provider
shares rendered each parameter as <input type="text"> with spellcheck on,
secrets included, so the Versa Bedrock Secret Access Key (and every
provider's API key) was on screen and readable from the DOM as it was
typed. The component already read `parameter.secret`, but only to avoid
echoing a stored value; the custom-provider form masked its key. A
divergence, not a decision.
- New `ui/secret-input.tsx` primitive: `type="password"` until an explicit
reveal toggle, with `autoComplete="off"` and `spellCheck={false}` applied
after the caller's props so neither can be switched back on. Starts
masked on every mount.
- `DefaultProviderSetupForm` renders secret parameters with it; non-secret
parameters (endpoint, region) stay readable text fields. Labels are now
associated with their fields.
- `CustomProviderForm`'s key uses the same primitive, so the two forms mask
and reveal a key identically.
The vitest locates fields by placeholder so it runs unchanged against the
old form: there the masked case fails on the `type` assertion itself and
the non-secret control passes.
F6 of the 2026-09-10 provider QA run. With CODEX_COMMAND pointed at a path that did not exist, the Codex row read "Not installed" and "Configured" on one line, and Codex stayed selectable in the Switch-models picker, where the bind then failed in `from_env`. `check_provider_configured` treated the agent's one defaulted key as configured whenever it was saved, with no reference to whether the command resolved. Daemon: - `check_provider_configured` now asks `discovery::resolve_configured` (a few `stat` calls, never a spawn) for the coding agents, which is the same lookup `/coding_agents/status` uses for "Not installed". The two answers agree by construction. - `GET /config/providers` rows carry a new `unavailable_reason`: set only when a provider the user set up cannot run, today a coding agent whose saved command does not resolve. The sentence is `CodingAgentKind::not_installed_summary`, the first sentence of the error a turn would have raised. - Sign-in is deliberately not folded in: learning it spawns the CLI, and this route runs for every provider on every settings open. Renderer: - `SwitchModelModal` keeps such a provider in its list, disabled with the reason on the row. That is react-select's own `aria-disabled` option, the private-chat pre-flight's shape one level up. A dialog that opens on one says why and will not submit. - The catalog re-reads the provider list after an explicit "Check again", and re-probes the CLIs after any change to an agent's setup. Driving the running app showed the second half is needed: correcting CODEX_COMMAND in the configure form brought the check back beside a pill still reading "Not installed". - The configure modal still offers "Remove" for a saved key whose CLI is missing. OpenAPI schema and TS client regenerated (the one new field).
…-844a47 # Conflicts: # ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx
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.
Fixes two MEDIUM findings from the 2026-09-10 provider QA run on merged
main(7c96d796), report~/biorouter-runs/test-drive/qa-e/report.md§F5 and §F6.Update: merged
origin/main(6455bc21)mainmoved 60 commits. The only conflict wasSwitchModelModal.tsx, against #222 ("the model switcher refuses a barred selection before the click", F3). The file now keeps both pre-flights:validation), and a refusal still disables the confirm before any click. The model-level refusal (blocked: a public model in a private chat) still shows beside the model field and is still the confirm'saria-describedby.providerBlocked: a provider the user set up that cannot run. It shows under the provider field at once, disables the model field, and joins the confirm'saria-describedby. On the predefined-model list, which has no provider field, it folds intoblocked.The other overlapping files merged automatically.
config_management.rsonly picked up the master-switch changes from #227 and the config-write fix from #223;ProviderDetailsgained nothing else, so the test literals still compile.CLAUDE.md,openapi.jsonandtypes.gen.tsmerged cleanly, and regenerating the schema and client from the merged tree reproduces those files exactly (below).One new assertion pins the merged contract: when the dialog opens on an unavailable provider, the confirm's
aria-describedbynames the provider-field reason, as #222 requires for its own refusal.Re-verification on the merge commit
The brief's command still stops at cargo's argument parser (one positional
TESTNAME); the same filters after--:Schema and client regenerated from the merged tree.
git statusstays clean, so the auto-mergedopenapi.jsonandtypes.gen.tsare exactly what the generator produces:⚠ The one failed suite is not a failed test, and it is not this branch.
src/utils/artifactCdnAssets.browser.test.tspasses both of its tests, then itsafterAll(await browser?.close()) hits the 30 s hook timeout. It does the same run on its own (2 passed,Hook timed out in 30000ms). The machine's load average was 355–436 at the time, from other sessions. This PR does not touch that file or anything it imports (git diff --name-only origin/main...HEAD | grep -c artifactCdnAssets→0). Of my three full runs earlier today, it passed in two; the third hit the same teardown timeout while a Rust build ran beside it. CI'sUnit tests (vitest)passed on this PR's previous head.F5: provider secrets were typed and shown in cleartext
Cause.
DefaultProviderSetupForm.tsx(the form every built-in provider shares) hard-codedtype="text"for every parameter, secrets included, with spellcheck on. It already readparameter.secret, but only to avoid echoing a stored value.CustomProviderForm.tsxmasked its key. The two had drifted apart; nobody decided on it.Fix.
ui/secret-input.tsx. It staystype="password"until an explicit reveal toggle is pressed. It forcesautoComplete="off"andspellCheck={false}after the caller's props, so a caller can't switch either back on. It starts masked on every mount.DefaultProviderSetupFormrenders secret parameters with it. Non-secret ones (endpoint, region) stay readabletype="text"fields. Labels are now associated with their fields.CustomProviderForm's key uses the same primitive, so both forms mask and reveal a key the same way.F6: a provider with a missing CLI was badged "Configured" and stayed selectable
Cause.
routes/utils.rstreated a coding agent's one required, defaulted key as "configured" whenever it was saved. It never checked whether the command resolved. SoCODEX_COMMAND=/nonexistentproduced "Codex · Not installed ✓ Configured", and the picker offered a provider whose bind would then fail infrom_env.Fix (daemon).
check_provider_configurednow asksdiscovery::resolve_configuredfor the coding agents. That is a fewstatcalls, never a spawn, and it is the same lookup/coding_agents/statususes for "Not installed". The badge and the pill now agree by construction.GET /config/providerscarry a newunavailable_reason. It is set only when a provider the user set up cannot run; today that means a coding agent whose saved command doesn't resolve. The text isCodingAgentKind::not_installed_summary, which is also the first sentence of the error a turn would have raised.discovery.rs's module header forbids. The pill shows sign-in state, and a turn that reaches a signed-out CLI fails with the vendor's own login command.Fix (renderer).
SwitchModelModalkeeps such a provider in its list, disabled with the reason on the row. It uses react-select's ownaria-disabledoption with the reason as the detail line, the same shape as the private-chat refusal, applied to a provider instead of a model. If the dialog opens on such a provider (the bound one), it shows the reason and won't submit.CODEX_COMMANDin the configure form brought back the ✓ check next to a pill that still said "Not installed", which is F6's contradiction arriving from the other direction.openapi.jsonand the TS client are regenerated; the only change is the new field.Tests: each new case fails on
main, and each has a controlmainroutes::utils::tests::a_coding_agent_whose_cli_is_missing_is_not_configured(route logic)Codex pointed at …/no-such-dir/codex must not report is_configured(EXIT=101)routes::config_management::readiness_wire_tests::*(the row the route serves)routes::utils::tests::not_configured_is_exactly_what_the_status_route_calls_not_installedprobe()says NotInstalled exactly when the check says not configuredDefaultProviderSetupForm.test.tsx: secret renders maskedExpected the element to have attribute: type="password"/Received: type="text"SwitchModelModal.unavailable.test.tsx: Codex row disabled with reasonUnable to find role="option" and name /Codex/aria-disabled="true"+ reason); Versa staysaria-disabled="false"; never-configured OpenAI still omittedUnable to find … switch-model-provider-errorchangeModelnever calledProviderCatalog.test.tsx: "Check again" finds the CLI gone, check disappearsUnable to find … Ready · signed in on your subscriptionThe F5 test finds fields by placeholder, so it runs unchanged against
main's form, and the assertion that fails there is the masking assertion itself, not the lookup.Verification (on the original head,
0873d12d)⚠ The Rust command in the brief doesn't run. Cargo accepts only one positional
TESTNAME:The equivalent with the filters passed to libtest:
The core crate's coding-agent tests (the discovery and error-text changes live there):
Runtime check (own sandboxed instance, CDP 9389, never
set_viewport)The instance ran on this branch's
biorouterd(with the test-auth seam) and renderer, in the sandbox~/biorouter-runs/f5f6-codex-secretscloned from the seed. Screenshots are in~/biorouter-runs/f5f6-codex-secrets/shots/:01-f5-bedrock-secret-masked.png: Versa Bedrock configure card with a dummy value typed into Secret Access Key, shown as dots. Live DOM: both Bedrock secretstype=password,autocomplete=off,spellcheck=false, labelled. The reveal toggle flipspassword → text → password. Endpoint and region stay readable. Cancelled, never saved (the sandboxsecrets.yamlhas no Bedrock keys).02-f6-codex-row-no-configured.png: withCODEX_COMMAND: /nonexistentin the sandbox config, the row reads "Codex · Not installed" with no ✓ Configured. Claude Code keeps its check. The inline guidance is unchanged.03-f6-picker-codex-disabled.png: Settings → Switch models. Codex isaria-disabled="true"with "Unavailable: Codex is not installed, or is not on a path Biorouter searches". Every other row is enabled. Clicking the disabled row selects nothing.04-f6-codex-after-correcting-path.png:CODEX_COMMANDcorrected tocodexthrough the Configure form. Pill and check both return together ("Ready · signed in on your subscription ✓ Configured").Afterwards
CODEX_COMMANDwas back tocodexin the sandbox,BIOROUTER_PROVIDERwas stillversa_azure, and the instance was stopped. The QA instances (CDP 9371–9376,~/biorouter-runs/qa-merged-*) were never touched, and no process was killed by pattern.Not in this PR
is_configured, for the reason above.LeadWorkerSettingsandIngestModelPickerlist only configured providers, so a not-installed agent drops out of them rather than showing disabled. The brief named the Switch-models list.ElevenLabsKeyInput,ExternalBackendSection) are alreadytype="password"but have no reveal toggle. They could adoptSecretInputlater.🤖 Generated with Claude Code