Discover local t3 serve instances for explicit pairing#4474
Discover local t3 serve instances for explicit pairing#4474colonelpanic8 wants to merge 10 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review This PR introduces a significant new feature: local server discovery and pairing with a new 'client-only' desktop mode. It includes new authentication mechanisms (filesystem-based proof of identity), runtime behavior changes, and security-sensitive code in the auth package. An unresolved review comment also identifies a potential bug in error handling. You can customize Macroscope's approvability policy. Learn more. |
c3ff84a to
8f3ac9f
Compare
8f3ac9f to
836a24f
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9d90bf8. Configure here.
| yield* installDesktopIpcHandlers(); | ||
| yield* logBootstrapInfo("bootstrap ipc handlers registered"); | ||
| if (!(yield* Ref.get(state.quitting))) { | ||
| yield* handleClientOnlyRendererReady(desktopWindow.handleRendererReady); |
There was a problem hiding this comment.
Client-only window errors quit app
Medium Severity
In client-only bootstrap, handleClientOnlyRendererReady logs then re-fails main-window creation, so bootstrap hits fatal startup and quits. The prior Effect.catch only logged and let startup finish, leaving backendReadyRef set so activate could retry—matching managed mode’s swallowed handleBackendReady path.
Reviewed by Cursor Bugbot for commit 9d90bf8. Configure here.
Let the desktop app run as a pure client for remote or independently managed backends, instead of always owning a local backend process. - Add a persisted `managed` / `client-only` backend mode with a launch-only `--backend-mode` override, exposed over IPC. - In client-only mode, bootstrap skips the backend pool entirely and serves the renderer from packaged static assets (or the dev proxy), opening the window on renderer readiness rather than backend readiness, and skipping backend shutdown on quit. - `ElectronProtocol` takes an explicit `source: "proxy" | "static"`. Static serving adds path-safe file resolution, SPA fallback, MIME types, CSP and HEAD support. - Web routing treats client-only like hosted static, and local environment bootstraps are empty, so a client-only desktop is never stranded on a dead primary-backend state. Co-Authored-By: Claude <noreply@anthropic.com>
Let a Linux desktop client find a loopback `t3 serve` running as the same user and pair with it, without copying a URL out of the terminal. - A headless server bound to loopback publishes an owner-only (0700 directory, 0600 file) JSON advertisement under XDG_RUNTIME_DIR, written atomically, rotated before the credential expires, and removed on shutdown. The advertisement owns its own pairing credential rather than the one printed to the terminal. - The desktop scans that directory and validates ownership, permissions, size, path containment, timestamps, canonical loopback URLs and environment identity before presenting a candidate. - Pairing requires an explicit Pair action, re-reads the advertisement first so a rotated credential is used, saves to the encrypted connection catalog, and never takes ownership of the server process. Co-Authored-By: Claude <noreply@anthropic.com>
…redential The advertisement previously carried a live admin-scoped pairing URL and rotated it every ~4 minutes, so a credential was continuously at rest in XDG_RUNTIME_DIR for the whole uptime of `t3 serve`. Same-UID access was already game over for code execution, but the admin scopes went further: AuthAccessWriteScope mints durable credentials and AuthRelayWriteScope relay-links the environment, turning a passive one-shot file read into persistent, off-machine access. Advertise presence instead, and prove same-user at pair time: - The record is now credential-free: instanceId, pid, startedAt, httpBaseUrl, environmentId, label. Published once, removed on shutdown. The rotation loop, the shutdown revoke and the tracked credential are all gone. - New unauthenticated `POST /api/auth/local-pair`. The caller writes a 256-bit nonce to a 0600 file in the owner-private challenge directory and presents the path; the server canonicalizes both paths, requires containment, requires a regular 0600 file owned by its own uid, and compares contents to the nonce in constant time. Only then does it issue a credential, in the response. Every check fails closed and all rejections collapse to one `invalid_credential`. - The desktop performs the handshake in the main process behind a new `pairLocalServer(instanceId)` IPC and deletes the challenge file in an `ensuring` block. The renderer now receives presence records only, and a credential exactly once, after the user clicks Pair. Because advertisements no longer expire, a record left by a killed process is rejected by the environment-identity probe instead. Co-Authored-By: Claude <noreply@anthropic.com>
The sidebar decided a thread or project was remote by comparing its environment against the primary environment id. A client-only desktop (and the hosted static web app) never registers a primary, so that id is always null and every row read as local: no cloud/server icon, and no environment name on the project header. Make the comparison explicit about the two different meanings of a null primary id. `isRemoteEnvironmentId` now takes an `ownsLocalEnvironment` flag: when the app can never serve an environment from its own backend, every environment is remote; when it can, a null primary id still means "the local backend has not registered yet" and nothing is flagged remote. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9d90bf8 to
ec578bf
Compare


What Changed
Let a Linux desktop client find a loopback
t3 serverunning as the same user and pair with it, without copying a URL out of the terminal.XDG_RUNTIME_DIR, written atomically via temp+rename, published once, and removed on shutdown. It contains no credential.startedAtsanity, canonical loopback URLs, and environment identity before presenting a candidate.pairLocalServer(instanceId)IPC, so the renderer receives presence records only — and a credential exactly once, after the click, for the server that was clicked.Pairing design — no credential at rest
The advertisement carries no credential. It is presence metadata only:
instanceId,pid,startedAt,httpBaseUrl,environmentId,label.A client proves it is the same local user at pair time, because
$XDG_RUNTIME_DIRbeing 0700 already answers that question:$XDG_RUNTIME_DIR/t3code/challenges/.{ instanceId, challengePath, nonce }to the unauthenticated/api/auth/local-pair.instanceId,realPathon both the directory and the supplied path with containment enforced, a regular 0600 file owned by its own uid and under the size cap, and contents equal to the nonce compared in constant time.ensuringblock, success or failure.A different-uid attacker cannot create a file in a directory they do not own, and cannot name an existing one because they would have to supply contents they cannot read. Every check fails closed, and all rejections collapse to a single
invalid_credentialso the response cannot be used to probe which one tripped. Rejection reasons are logged at debug level only.The challenge directory is deliberately separate from the advertisement directory, so the server never reads a caller-named path out of the directory it publishes into. The endpoint is closed entirely unless discovery is active, which means non-Linux, non-headless, and non-loopback servers do not expose it at all.
Why this shape. An earlier revision stored a live pairing URL in the advertisement and rotated it every ~4 minutes. Same-UID file access is already sufficient for code execution, so that was defensible — but the credential was
AuthAdministrativeScopes, and admin addsAuthAccessWriteScope(mint durable credentials with caller-chosen scopes, revoke other sessions) andAuthRelayWriteScope(relay-link the environment). That turned a passive one-shot file read into persistent, off-machine access, which is exactly the containment the loopback-only argument was supposed to provide.Removing the credential from disk also deleted the rotation loop, the shutdown revoke, and the tracked-credential state — so a class of liveness bugs went with it.
Known lower-severity items, not addressed here
stat/realPathandreadFileString, on both the advertisement read and the challenge read. Crosses no privilege boundary: winning it needs write access inside a 0700 directory, i.e. the same UID. Now largely uninteresting on the advertisement side, since winning that race yields a hostname and a label rather than a credential.open+fstat+readAllocis the correct fix and is expressible in this Effect version, thoughrealPathcontainment would still be needed sinceOpenFlaghas noO_NOFOLLOW.slicecap would be cheap insurance.kill -9. Bounded in practice by tmpfs clearing at logout, and expired records are filtered before the network probe, so they cost only astat+ read.kill -9'd process is rejected by the probe failing rather than by a timestamp.t3 servebefore merge. This revision reworked a flow that was previously hand-tested end to end in Electron — in particular the challenge file cleanup on failure paths, and that the endpoint is genuinely unreachable on a non-loopback bind.Checklist
Post-rebase review-fix verification: formatting and lint across all 55 changed TS/TSX files; 5 affected-package typechecks; 18 focused test files, 130 tests; server route-layer smoke test
corepack pnpm exec vp check(0 errors; 11 pre-existing unrelated warnings)corepack pnpm exec vp run --filter @t3tools/contracts --filter @t3tools/shared --filter t3 --filter @t3tools/desktop --filter @t3tools/web typecheck— cleanCI=true vitest run apps/desktop/src apps/web/src/components/settings apps/web/src/environments apps/server/src/startupAccess.test.ts apps/server/src/localServerAdvertisement.test.ts packages/shared/src/localServerDiscovery.test.ts— 66 files, 449 testsLoopback URL validation exercised against
0x7f.1,127.1,2130706433,[::ffff:127.0.0.1], credentials-in-authority, and trailing-dot formsRegression test locks in that a discovered pairing URL with a query string is rejected (
readHostedPairingRequesthonours ahost=param)Note
Add local
t3 serveinstance discovery and explicit pairing to desktop settingsDesktopLocalServerDiscoveryservice that reads filesystem advertisement files written by headlesst3 serveinstances (Linux, loopback-only) and exposesdiscoverandpairLocalServeroperations via IPC to the renderer.POST /api/auth/local-pairendpoint on the server that validates a filesystem-based pairing challenge (nonce file, ownership, containment, timing-safe compare) and issues a pairing credential without requiring a bearer token.DesktopBackendModeservice and settings field (managed|client-only) with IPC get/set, app relaunch on mode change, and rollback on relaunch failure.ConnectionsSettingsUI to show a 'Desktop application' backend-mode selector and an 'Available on this computer' section listing discovered servers with Pair / Pair again actions.client-onlymode the desktop skips starting a local backend, serves the renderer from packaged static assets via a newsource:'static'branch inElectronProtocol, and returns empty lists from local-environment IPC calls.Macroscope summarized ec578bf.
Note
High Risk
Introduces unauthenticated local pairing and credential issuance gated on filesystem proofs, plus a new desktop startup mode that changes how the renderer and backends are loaded—both are security- and reliability-sensitive.
Overview
Local
t3 servediscovery and pairing lets a Linux desktop client find headless loopback servers for the same user and connect after an explicit Pair click, without copying a terminal URL. Headless servers publish credential-free presence JSON underXDG_RUNTIME_DIR; the desktop scans, validates ownership/permissions/loopback/identity, and runs the handshake in the main process viadiscoverLocalServers/pairLocalServerIPC so the renderer never holds ambient secrets. Pairing writes a private nonce file, POSTs to new/api/auth/local-pair, and only then receives a pairing URL; the server validates loopback, instance match, canonical challenge paths, and constant-time nonce compare before issuing credentials.Desktop backend mode (
managed|client-only) is persisted in settings, overridable via--backend-mode, latched at startup (invalid CLI → fatal error), and changeable from settings with relaunch and rollback on failure. In client-only, bootstrap skips the backend pool, registers IPC, opens the window onhandleRendererReady, and serves the UI viaElectronProtocolsource: "static"(packaged client) or dev proxy; shutdown skips stopping backends.Renderer and UI updates include environment presence scope (remote vs local labeling), Connections logic for Pair / Pair again candidates, and empty local bootstraps in client-only mode.
Reviewed by Cursor Bugbot for commit ec578bf. Bugbot is set up for automated code reviews on this repo. Configure here.