feat(desktop,agent-adapter): API-key / relay login on the signed-out agent card - #350
feat(desktop,agent-adapter): API-key / relay login on the signed-out agent card#350lucas77778 wants to merge 6 commits into
Conversation
…count Adds config.probe-models: the daemon reads the endpoint's own model list with a secret the client has not persisted yet, so the account forms can offer a real model picker instead of a free-text field. Bumps the wire protocol version.
The signed-out onboarding card gains an "API key" path next to the OAuth button: a dialog collects a relay key plus its base URL, detects the endpoint's models, saves it as a pool account and binds it as the agent's active one. A bound key account now clears the login cue, the way the legacy per-agent key already did.
lucas77778
left a comment
There was a problem hiding this comment.
Review findings — five issues need fixing before merge (GitHub does not allow the PR author to submit a Request changes review on their own PR):
- High:
AgentApiKeyLoginDialogallows Save before accounts/providers load;accounts ?? []andproviders ?? {}can replace existing daemon configuration with incomplete snapshots. - High:
config.probe-modelsaccepts arbitrary URLs and follows redirects, enabling daemon-side SSRF and potential cross-originx-api-keydisclosure. Restrict schemes/destinations, reject redirects, and bound successful responses. - Medium: account creation and provider binding are two non-atomic, non-idempotent writes; partial failure leaves orphan/duplicate accounts. Use one daemon-owned atomic create-and-bind operation.
- Medium: detected models remain selectable after Base URL, protocol, credential type, or secret changes, including in-flight races. Key results to the probe inputs and discard stale responses.
- Low:
providers[kind]?.apiKey !== undefinedtreats an empty legacy key as a usable injected credential and incorrectly hides the login card. Require a non-empty trimmed key.
# Conflicts: # apps/daemon/src/__tests__/config.test.ts # apps/daemon/src/provider-store.ts
|
不太对,这里最好是跳到 Settings 里面 |
|
那还是改为go to setting,因为我想的是在这里快捷填完就不用在setting里填完再跳转回new task |
Zerlight
left a comment
There was a problem hiding this comment.
Requesting changes on head 15daf721.
-
[P1] Require HTTPS before sending credentials.
resolvePublicEndpointaccepts publichttp:URLs, andrequestPublicModelListthen sendsAuthorizationorx-api-keyover plaintext HTTP. Private/loopback HTTP is already rejected, so this only enables remote cleartext credential transmission. -
[P1] Reject unsupported agent/protocol combinations. The dialog offers every
AccountProtocolSchemaoption for every agent, while the capability matrix says, for example, that Codex supports only OpenAI Chat/Responses.accountBindingpersists any combination without validation. Selecting Codex + Anthropic therefore succeeds, suppresses the onboarding cue, and leaves session startup with an unusable binding. Filter the UI and validate again at the daemon boundary. -
[P2] Surface save failures. The submit handler stores failures under React Hook Form's
rooterror, butrhfErrorsToFormErrorsexplicitly dropsrootand the dialog renders no separate root error. A rejected valid submission silently re-enables the button without telling the user what failed. -
[P2] Honor the advertised
x-api-keycredential type. The UI labelsapi-keyas “API key (x-api-key)”, but every OpenAI-shaped request converts both credential variants toAuthorization: Bearer. OpenAI-compatible relays that requirex-api-keycannot be detected or used. Either send the selected header or remove/restrict the unsupported choice. -
[P2] Do not allow a save to outlive dismissal. Cancel, Escape, and backdrop dismissal remain enabled while
save.isMutating. Closing the dialog does not cancel or invalidate the request, so the account may still be created after an apparent cancellation; the stale completion can also call the sharedcloseaction after another agent's dialog has opened. -
[P2] Make create-and-bind atomic across the vault and
config.json.saveProviderConfigurationmutates both secret namespaces viareplaceAllbefore writing the JSON file. If the file write fails, the caller sees a failure while new/orphaned secrets remain; a stable-ID credential update can take effect after restart despite the failed operation. -
[P3] Preserve bounded non-2xx error details. Error responses are destroyed as soon as they exceed 200 bytes, so the later
.slice(0, ERROR_BODY_LIMIT)path is unreachable for a 201-byte vendor response. The UI receives only “response exceeded 200 bytes” instead of the intended status and truncated vendor reason. -
[P3] Implement
config.probe-modelsin the dev mock. The client sends a correlated request, but the mock switch has no matching case and its default branch silently drops the frame. “Detect models” therefore remains pending forever in mock mode. -
[P3] Retry all validated DNS addresses.
resolvePublicEndpointvalidates every result but returns onlyaddresses[0]. A dual-stack or multi-A endpoint fails when the first address is unreachable even if another validated address works.
Additional merge blockers:
- The latest PR discussion agreed to navigate to Settings, but the current head still mounts and opens the modal.
- The PR currently conflicts with
masterinworkbench.tsxandwire/message.ts. CurrentmasterisWIRE_PROTOCOL_VERSION = 66/MIN_COMPATIBLE_WIRE_VERSION = 66; after replaying this additive wire operation, the resolved values should be wire67and minimum compatible66. - CI is still red. The failure appears to be an unrelated flaky daemon runtime PID/port fixture, and 98 targeted tests passed locally, but the merge gate still needs a green rerun.
The earlier self-review items covering save-before-load overwrites, redirect SSRF, stale model detection, and whitespace-only legacy keys appear fixed in this head.
Closes CODE-344.
What
The signed-out agent card only offered the CLI's OAuth login. It now also offers "Use an API key", which opens a dialog for a relay/gateway key:
x-api-keyvsBearer) / secret / default model, with the protocol pre-set to the agent's native one (claude-code → Anthropic, codex → OpenAI Chat).activeAccountId— the credential/base-URL injection at session start already existed for every adapter (ANTHROPIC_BASE_URL,OPENAI_BASE_URL, opencodeprovider.options, pi runtime key).Bug fixed along the way: the login cue was only suppressed by the legacy
providers[kind].apiKey, so a bound key account left the "needs sign-in" card up even though the daemon injects the key at spawn. Now any bound account carrying a key/token clears it; anoauthaccount (no secret — it delegates back to the CLI store) still does not.New wire op
config.probe-models(+.result) — the client cannot read a relay's model list itself: the renderer's CSP blocks remote fetches, and only the daemon holds the secret. Existing paths could not serve it either: claude-code'sstartCatalogreturns a static empty list, opencode'sprovider.listignores the injected credential, andagent.catalogreads only saved config.Addressing follows each protocol's base-URL convention — Anthropic-shaped URLs are bare origins (
{base}/v1/models,x-api-key+anthropic-version), OpenAI-shaped ones already end in/v1({base}/models,Bearer). Bare-array responses (common in relays) are accepted alongside the vendors'{data}envelope.WIRE_PROTOCOL_VERSION63 → 64 (Invariant 1): rebuild and restart the daemon together with every client.Verification
pnpm check:ci+pnpm test(pre-existing, unrelated failure:worktree-service.test.tstemp-path comparison on macOS, fixed on another branch).config.probe-modelsthrough the real router/handler against a local HTTP relay — both the model list and the 401-with-reason path.Scope note
Only claude-code and codex have an auth probe, so they are the kinds that show a signed-out card today; opencode/pi/grok-build surface no such card, and the entry appears automatically if they gain one. Relay accounts for those agents are still reachable through Settings → Providers.