feat: silent pairing for local web-dev servers - #3955
Conversation
Local web dev previously required scraping a one-time, 5-minute /pair#token URL from dev-runner stdout for every fresh browser context, which made parallel-instance work and agent browser verification painful. The web app now authenticates automatically against eligible dev servers via a guarded dev-only endpoint. Server: POST /api/auth/dev-pairing-token mints a one-time administrative credential (subject dev-auto-bootstrap, hidden from pairing-link listings) only when the server is web-mode, loopback-bound (loopback-browser policy), and configured with a loopback dev URL. Requests must carry an Origin exactly matching the dev origin (survives the Vite proxy, unlike Host) and a loopback Host header (DNS-rebinding defense). Ineligible configs answer an explicit 404 — the handler is always mounted so the SPA/redirect wildcard can't shadow it. Startup: eligible dev servers now open the plain dev URL instead of minting a startup pairing credential the silent path would leave unconsumed in the URL and scrollback. Web: the auth-gate bootstrap silently tries the endpoint when unauthenticated in dev (never in hosted/desktop contexts, never when an explicit #token is present) and falls back to the pairing screen on any failure. import.meta.env.DEV keeps the request out of prod bundles. Extracted host parsing into netHost.ts (auth/http.ts importing the old helper from http.ts would have been a module cycle; the old helper also mishandled Host headers with ports). Docs: AGENTS.md browser-verification section, dev-instance auth notes in scripts.md (plus stale T3CODE_STATE_DIR fix), and a security spec in environment-auth.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
ApprovabilityVerdict: Needs human review This PR introduces a new authentication endpoint that mints administrative credentials for dev servers. An unresolved review comment identifies a potential security bypass when Vite is LAN-exposed, allowing remote Origin forgery. Auth changes with open security concerns warrant human review. You can customize Macroscope's approvability policy. Learn more. |
A prefix test also accepted DNS names like 127.attacker.example, which would pass both the devUrl eligibility check and the Host header guard (review finding from macroscope). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 929d35a. Configure here.
| return "host_not_loopback"; | ||
| } | ||
| return null; | ||
| }; |
There was a problem hiding this comment.
Proxied Origin forge bypass
Medium Severity
The Host loopback check is defeated for requests through the Vite /api proxy because changeOrigin rewrites Host to the loopback upstream. Eligibility only constrains the server bind address, not the Vite listen address. If Vite is LAN-exposed (HOST=0.0.0.0), a remote client can forge Origin to the configured devUrl origin and mint an administrative pairing credential without local filesystem access.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 929d35a. Configure here.


What
Local web-dev servers now authenticate the web UI automatically — no more scraping one-time
/pair#token=…URLs out of dev-runner stdout for every fresh browser context. This was the main DX friction for working on multiple T3 Code changes in parallel, and the reason coding agents kept giving up on browser verification.Design doc (reviewed by gpt-5.6-sol, findings incorporated): https://xaxuzu0q3o8s.postplan.dev
How
New endpoint
POST /api/auth/dev-pairing-tokenmints an ordinary one-time, 5-minute administrative pairing credential (subjectdev-auto-bootstrap, hidden from pairing-link listings) that the web app immediately exchanges through the existing browser-session flow.Security model (why this doesn't reopen the drive-by-localhost hole the pairing codes exist to close):
404 dev_pairing_not_availableunless the server is web-mode + loopback-bound (loopback-browserpolicy) + configured with a loopback dev URL. Prod, desktop, LAN-bound dev, and hostile--dev-urlvalues are all ineligible. The handler is always mounted — conditional non-registration would fall into the SPA/302 wildcard route instead of 404ing.Originmust exactly equal the dev origin (survives the Vite/apiproxy, which rewritesHostbut notOrigin— Host/CORS-only guards are defeated by the proxy path), plus a loopbackHostheader check against DNS rebinding on direct requests. JSON payload forces a CORS preflight, so unauthorized web origins can't even trigger credential issuance.Startup: eligible dev servers open the plain dev URL instead of minting a startup
/pair#tokencredential the silent path would leave unconsumed in terminal scrollback. All other startup shapes unchanged.Web: the auth-gate bootstrap tries the endpoint when unauthenticated in dev — never in hosted/desktop contexts, never when an explicit
#tokenis present (manual/remote pairing keeps precedence) — and falls back to the pairing screen silently on any failure.import.meta.env.DEVkeeps it out of prod bundles.Extracted
netHost.tshost-header parsing (the existingisLoopbackHostnamemishandlesHost: localhost:13773, and importing it intoauth/http.tswould create a module cycle).Docs: AGENTS.md "Verifying Changes in the Browser" section for agents/contributors, dev-instance auth notes in
scripts.md(+ staleT3CODE_STATE_DIRfix), security spec inenvironment-auth.md.Verification
vp check,vp run typecheckgreen; server 1427 passed, web 1288 passed, contracts 179 passednode:httprebound-Host request and admin-scope/listing-exclusion assertions), 4 startup-target tests, 5 web-gate tests (incl. explicit-token precedence and reauth retry)🤖 Generated with Claude Code
Note
High Risk
Introduces an unauthenticated endpoint that issues administrative credentials; safety depends on narrow eligibility and Origin/Host guards—misconfiguration or guard bypass would be serious.
Overview
Adds silent dev pairing so local web dev servers can authenticate the UI without manual pairing codes or scraping
/pair#tokenURLs from stdout.Server: New unauthenticated
POST /api/auth/dev-pairing-tokenmints a one-time administrative bootstrap credential (dev-auto-bootstrap, hidden from pairing-link listings). It returns 404 when the server is not web-mode + loopbackloopback-browserpolicy + loopbackdevUrl, and 403 whenOrigindoes not exactly match the dev origin orHostis not loopback. Startup opens the plain dev URL (no startup pairing token) when eligible.Web: On load in
import.meta.env.DEV, the auth gate calls the endpoint and exchanges the credential via the existing browser-session flow, with silent fallback to the pairing screen; skipped for explicit#token, desktop bridge, and hosted static apps.Shared: Loopback/host parsing moves to
netHost.ts(stricter 127.0.0.0/8 andHost: porthandling). Docs and agent guidance updated for auto-auth on localhost dev.Reviewed by Cursor Bugbot for commit 929d35a. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add silent pairing for local web dev servers via new
/api/auth/dev-pairing-tokenendpointPOST /api/auth/dev-pairing-tokenendpoint that issues a one-time administrative pairing credential for eligible local dev requests, returning 404 or 403 for ineligible configurations or invalid headers.shouldAttemptDevPairingin auth.ts, skipping the pairing screen when on a loopback host in dev mode without an explicit token.resolveStartupBrowserTargetin serverRuntimeStartup.ts now returns a plain base URL withrequiresPairing=falseinstead.startupAccess.tsloopback check is now stricter (exact IPv4 match instead ofstartsWith("127.")).listPairingLinksexcludesdev-auto-bootstrapsubjects by default, so dev pairing credentials do not appear in pairing-links listings.Macroscope summarized 929d35a.
Summary by CodeRabbit
New Features
Documentation
Tests