Skip to content

feat(desktop): SSO configure gate and hidden hosted providers - #238

Merged
quickbeard merged 2 commits into
mainfrom
feat/desktop-sso-gate
Aug 17, 2026
Merged

quickbeard merged 2 commits into
mainfrom
feat/desktop-sso-gate

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Summary

Ports the CLI's first-run experience (codev configure) to the desktop app, per the approved plan:

Hiding (web + desktop). Upstream's hosted providers (opencode Zen free models, opencode-go) are filtered out client-side at the provider-normalization choke point (global-sync/utils.ts wraps its return in the fork-only packages/app/src/gateway/hidden-providers.ts). They disappear from every model picker, the connect dialog, settings, and the default-model fallback — a fresh install no longer silently chats against upstream's gateway. connected keeps the TUI's isConnectedProvider semantics: only the unauthenticated free tier is dropped, so a paid Zen key never gets gated. The composer's "unpaid" click opens the connect dialog instead of the now-empty free-models lists (the only UX seam on web); downstream hosted-provider surfaces are left byte-identical to upstream as deliberately-dead code (documented in DIVERGENCES) so syncs stay cheap.

The gate (desktop only). When the local sidecar server has no configured provider, a persistent setup dialog opens with the CLI's three options: Login with SSO, Custom provider (existing dialog), Connect provider (existing dialog). A stateless invariant (packages/app/src/components/gateway/gateway-configure-gate.tsx, twin of the TUI's gate effect) reopens it whenever the dialog stack empties while unconfigured, and closes it reactively once a provider appears — including setup completed in another window or the CLI. Activation is keyed on a new platform.gatewaySso capability, so the browser web deployment never gates.

SSO flow. Runs in the Electron main process (packages/desktop/src/main/gateway-sso/, wrapping @codev/gateway's loopback OAuth): single-flight across windows, generation-counter cancellation (login has no cancel API — stale results are dropped), status streaming over sandbox-pattern IPC, open-in-browser + copy-link + paste-callback fallback, manual API-key path when the account has no provisioned key, 3× retries, shell.openExternal, and Electron net.fetch (new optional fetchImpl on the gateway backend) so enterprise proxies/PAC apply. The API key never crosses IPC inbound. Completion mirrors the TUI exactly: saveApiKey → raw config PATCH with buildGatewayConfig → dispose → wait for netgate → seed model recents → toast. Failures toast and bounce back to the entry screen — the desktop never quits.

Dialog persistence. packages/ui's dialog context gains a per-dialog persistent flag (TUI precedent): Escape/backdrop/outside-click are inert for the marked dialog, programmatic close() works, pushed sub-dialogs stay dismissable.

Docs

  • 8 new DIVERGENCES.md rows (filter choke point + dead-surface note, dialog persistence, gate mount, platform capability, prompt-input handler swap, desktop wiring, package export, i18n)
  • 2 CHANGELOG "Unreleased" entries

Verification

  • bun typecheck: app, desktop, ui, codev-gateway, tui — all clean
  • bun test: app 674 unit + 69 browser, desktop 90 (7 new controller tests), ui 14, codev-gateway 37 — all pass
  • New tests: hidden-providers filter (5), gate predicate truth table (6), SSO controller (single-flight, cancel drops stale results, paste error contract, retry/exhaustion, auth-file base_url semantics)
  • Not yet done: the manual end-to-end pass against the live IdP (fresh-profile bun dev:desktop, browser + paste-fallback login, second-window behavior; CODEV_BYPASS_LOGIN=1 for the no-IdP loop) — please run before merging or ask me to smoke it

🤖 Generated with Claude Code

Port the CLI's first-run experience to the desktop app:

- Hide upstream's hosted providers (opencode Zen free models, opencode-go)
  from the entire app UI via a client-side filter at the provider
  normalization choke point. The connected list keeps the TUI's
  isConnectedProvider semantics so a deliberately configured paid Zen key
  still counts as configured. With nothing configured, no models are
  offered; the composer's unpaid state opens the connect dialog instead of
  the now-empty free-models lists.
- Force a setup dialog when the local server has no configured provider
  (desktop only, via a new platform.gatewaySso capability): Login with SSO,
  Custom provider, or Connect provider — a stateless invariant reopens it
  if any sub-flow is escaped, and it closes reactively once a provider
  appears (including setup finished in another window or the CLI).
- SSO runs in the Electron main process (gateway-sso controller wrapping
  @codev/gateway's loopback OAuth): single-flight across windows,
  generation-counter cancellation, paste-callback fallback, manual API-key
  path, 3x retries, shell.openExternal, and net.fetch so enterprise
  proxies/PAC apply (fetchImpl added to the gateway backend).
- Completion mirrors the TUI: saveApiKey to the shared auth file, raw
  global config PATCH with buildGatewayConfig, dispose, wait for the
  netgate provider, seed model recents.
- packages/ui dialog context gains a per-dialog persistent flag (Escape/
  backdrop inert; programmatic close and pushed sub-dialogs unaffected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@quickbeard
quickbeard force-pushed the feat/desktop-sso-gate branch from 9c04c18 to 62a0e9d Compare August 17, 2026 03:50
A sandbox guest is given its codev config once, at boot: the provider
block resolveSandboxEgress builds from the gateway credentials, injected
through CODEV_CONFIG, with the guest's file watcher off. Signing in
afterwards writes the auth file and patches the local server's config, so
Code gets the model that was just chosen while Work is left with a guest
whose config is nothing but `{ "permission": "allow" }` -- no provider, no
model, and no broker either, since a start with no credentials resolves an
empty route table and never opens one.

The configure gate makes that the guaranteed first run: sandboxes start at
launch, the gate blocks until a provider is configured, so every sandbox
is already up and credential-less by the time the login completes.

The guest cannot be told about this without restarting, so the SSO
controller takes an onConfigured callback -- fired after saveApiKey, so
readers of the auth file see the new credentials -- and main wires it to
the sandbox controller's new restartRunning(), which restarts the ready
sandboxes so their next start re-resolves egress. Nothing is interrupted:
the configure flow is only reachable while no provider is configured, so
those guests had no model to be working with in the first place.

Only sandboxes that are ready. Restarting one that is still starting would
run a second guest against the same instance disk until the first one's
stale-attempt guard caught up.

Verified against the real sandbox: with credentials on disk, a start now
injects the netgate provider at the broker stub, and the guest server
reports netgate connected with the chosen model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 3645335 into main Aug 17, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the feat/desktop-sso-gate branch August 17, 2026 04:20
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