Repository navigation
feat(desktop): SSO configure gate and hidden hosted providers - #238
Merged
Merged
Conversation
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
force-pushed
the
feat/desktop-sso-gate
branch
from
August 17, 2026 03:50
9c04c18 to
62a0e9d
Compare
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>
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.
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 (
opencodeZen free models,opencode-go) are filtered out client-side at the provider-normalization choke point (global-sync/utils.tswraps its return in the fork-onlypackages/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.connectedkeeps the TUI'sisConnectedProvidersemantics: 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 newplatform.gatewaySsocapability, 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 Electronnet.fetch(new optionalfetchImplon the gateway backend) so enterprise proxies/PAC apply. The API key never crosses IPC inbound. Completion mirrors the TUI exactly:saveApiKey→ raw config PATCH withbuildGatewayConfig→ dispose → wait fornetgate→ 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-dialogpersistentflag (TUI precedent): Escape/backdrop/outside-click are inert for the marked dialog, programmaticclose()works, pushed sub-dialogs stay dismissable.Docs
Verification
bun typecheck: app, desktop, ui, codev-gateway, tui — all cleanbun test: app 674 unit + 69 browser, desktop 90 (7 new controller tests), ui 14, codev-gateway 37 — all passbun dev:desktop, browser + paste-fallback login, second-window behavior;CODEV_BYPASS_LOGIN=1for the no-IdP loop) — please run before merging or ask me to smoke it🤖 Generated with Claude Code