Skip to content

Sidecar-hosted loopback login with hub receive for Codex and XAI - #695

Merged
TheGreatAxios merged 14 commits into
mainfrom
cl-7508-sidecar-hosted-loopback-login-hub-receive-for-codex-and-xai
Sep 8, 2026
Merged

Sidecar-hosted loopback login with hub receive for Codex and XAI#695
TheGreatAxios merged 14 commits into
mainfrom
cl-7508-sidecar-hosted-loopback-login-hub-receive-for-codex-and-xai

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

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:

  • Sidecar loopback login hosting and WebSocket frame plumbing
  • Hub-side receive and credential persistence
  • ExpiresAt persistence on the first-login connect path
  • Completed-before-started hang fix in vendored hub-sessions
  • Review clean-ups (hop tests, doc corrections, settle-path comment)

All checks green.

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.
@linear-code

linear-code Bot commented Sep 8, 2026

Copy link
Copy Markdown

CL-7508

@TheGreatAxios
TheGreatAxios merged commit 0ddc916 into main Sep 8, 2026
24 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-7508-sidecar-hosted-loopback-login-hub-receive-for-codex-and-xai branch September 9, 2026 04:16
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