Sidecar-hosted loopback login with hub receive for Codex and XAI - #695
Merged
TheGreatAxios merged 14 commits intoSep 8, 2026
Merged
Conversation
Add the OAuthLoginStartFrame (hub to sidecar) and OAuthLoginResultFrame (sidecar to hub) arktype frames with started/completed/error outcome arms to @intx/types, and wire them through both ends of the sidecar websocket channel. The hub-side router (hub-sessions) gains requestOAuthLogin: it picks the first connected sidecar that passes a new oauthLogin.isLocalSidecar locality gate, correlates the login by requestId with a whole-login timeout and a disconnect sweep, and resolves a typed gate outcome when no connected sidecar is local — the hub never hosts a loopback listener or falls back. The sidecar-side link (hub-agent) routes oauth.login.start to a new OAuthLoginExecutor and answers every path with oauth.login.result frames, fail-closed when the host wires no executor. Ledger rows updated in the same commit.
apps/sidecar gains createOAuthLoopbackLoginService, which stages the PKCE login for codex (localhost:1455) and xai-oauth (127.0.0.1:1456) through @corbits/oauth-core's startOAuthLogin — pinned ports asserted up front from the providers' registered redirect URIs, a busy port surfacing as the typed OAuthCallbackPortInUseError, the browser never opened sidecar-side (the hub ships the authorize URL to the web UI), and the PKCE verifier never leaving the process. The service wires in as the hub link's OAuthLoginExecutor, so oauth.login.start frames run on this machine and only finished tokens cross the channel.
Add POST /connections/:connectorId/loopback under the tenant prefix (packages/connections' createOAuthLoopbackRoutes). It starts the login through the sidecar router's gated requestOAuthLogin — no local sidecar resolves the typed 409 gate outcome, never a hub-hosted listener — returns the staged authorize URL for the web UI, and persists the terminal tokens through the shared connect pipeline (secret, refreshSecret, expiresAt, and the id_token-derived accountId metadata) once the oauth.login.result frame lands, detached and reported on failure. The hub's locality gate admits only sidecars whose allocation was provisioned by the process backend: a docker/e2b sidecar's loopback opens inside the container or sandbox, never on the browsing machine. Serving-time refresh now dispatches codex/xai-oauth credentials to the provider packages' own refresh grants (carrying chatgpt-account-id forward), and the deferred connector-registry items land: the codex/xai token exchanges match the providers' 15s timeouts and the codex exchange retains the id_token's account id.
Serving-time refresh keys on the credential row's expiresAt column, but the loopback receive path filed the expiry only inside metadata — so after the access token lapsed, codex/xai-oauth credentials never refreshed and never got marked re-auth-required. The route now threads the terminal tokens' expiry through persistConnectorCredential -> ensureCredential as a first-class field so the column is written. The raw id_token also stays out of the (unencrypted) credential metadata; only the derived accountId label is kept.
A login the hub gives up on left the sidecar's pinned-port callback listener bound, so every retry of that connector failed to bind until the stale tab completed or the sidecar restarted. The hub now sends a new oauth.login.cancel frame when the whole-login timeout fires, and the sidecar link honors it — and also cancels all staged logins when the ws connection drops, since the hub will never see a terminal frame for those — via the executor's cancel handle, which closes the staged login's callback server. A post-timeout retry gets a fresh start with a new requestId (regression-tested). Vendored ledger rows and hashes updated in the same commit.
Exercise hub-link's login tracking, cancel frames, and cancel-on-disconnect over a real local ws server, and give the vendored package its first bun test script. Ledger row and tree hash updated to record the delta.
settleOAuthLogin only branched on the started arm, so both terminal arms funneled into settleOAuthLoginError and a successful oauth.login.result resolved as an error — the tokens were dropped. Branch on the outcome arm: completed resolves the pending login with its tokens, error settles as an error. Pinned with colocated router tests for the completed arm and for an error before started.
The legacy web-redirect path only folded the exchange's expiry into credentialMetadata, so OpenRouter/Hugging Face oauth_token rows kept a NULL expiresAt column and serving-time refresh skipped them. Lift the expiry onto the first-class expiresAt arg as well, keeping the metadata fold since its presence is what types the row oauth_token. Forwarded through the tenant connect wiring to persistConnectorCredential.
…connect path The shared oauth-routes callback sets the first-class expiresAt field alongside the metadata fold, but onboarding's own connectCredential wrapper dropped it, so a first-login Hugging Face connect stored a row whose expires_at column stayed NULL and serving-time refresh never saw an expiry. Thread expiresAt from the wrapper through testAndPersistCredential into persistConnectorCredential, which already writes the column on create and rotate. A pasted key still omits the key entirely.
…outcome precedes started A sidecar that answered oauth.login.result with a completed outcome without ever sending started left the caller's request promise unsettled — the HTTP caller hung until the whole-login timeout even though the login had already finished. Settle the request as an error in that ordering (the caller never saw an authorize URL) while the tokens still reach the final consumer; pinned by a no-hang test. Recorded as the CL-7508 delta in VENDORED-FROM with the tree hash updated.
Review follow-up: the happy-path callback test now records and asserts the first-class expiresAt field through onboarding's connectCredential wrapper — the exact hop that dropped the exchange's expiry — and the expiresAt docs no longer claim the metadata fold never carries it (it does, to type the row oauth_token); the field is the ride that lands the column, not the fold's rival.
Review follow-up: a completed-before-started outcome resolves a completed promise nothing consumes (the loopback route only receives it through the started arm), so the exchanged tokens are dropped and the login must run again — the comment claimed delivery; the tree hash is updated to match.
TheGreatAxios
deleted the
cl-7508-sidecar-hosted-loopback-login-hub-receive-for-codex-and-xai
branch
September 9, 2026 04:16
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.
Hosts a pinned-port loopback OAuth login in the sidecar, receives the completed login in the hub, and persists the credential. Enables Codex and XAI provider packages to authenticate via OAuth without external redirect URLs.
Includes:
All checks green.