Repository navigation
[PIR #1869] Local browser access to /ide/ on Tower: forward cookie and Open IDE link - #1872
Merged
Merged
Conversation
…ss-port Origin guard
…ORS credentials invariant
…token out of the cookie jar
…ing, never hand-build ?tkn=
amrmelsayed
marked this pull request as ready for review
November 3, 2026 02:25
…e IDE server, IDE status off the 5s poll, light prefix module
Collaborator
Author
Architect integration review (head
|
amrmelsayed
added a commit
that referenced
this pull request
Nov 3, 2026
amrmelsayed
added a commit
that referenced
this pull request
Nov 3, 2026
This was referenced Nov 3, 2026
Merged
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.
PIR Review: Local browser access to /ide/ on Tower
Fixes #1869
Summary
A local browser can now open the Codev IDE through Tower. Each workspace card on the Tower dashboard has an Open IDE row, first on the card. It calls the key-gated
POST /api/ide/session, which sets acodev-ide-sessioncookie: HttpOnly, SameSite=Strict,Path=/ide, with its value derived from the local key by HMAC. It then opens/ide/?folder=<workspace>in a new tab. The forward's single authorization check (decideForwardAuthorization) accepts that cookie only behind a cross-portOrigin/Sec-Fetch-Sitecheck; every other Tower route stays header-only.The live browser run then showed that the workbench was bypassing Tower. Its sockets dialed the IDE server's own port, and the IDE token sat in a cookie that every localhost port could read. Amendment 1 fixed both, for local requests only:
x-forwarded-hostwith Tower's own Host, so the workbench connects through Tower.vscode-tknSet-Cookie and injects the token into the workbench's configuration instead.x-forwarded-*headers are stripped.Files Changed
codev-skeleton/resources/commands/agent-farm.md(+31 / -4)codev/plans/1869-local-browser-access-to-ide-on.md(+395 / -0)codev/projects/1869-local-browser-access-to-ide-on/status.yaml(+22 / -0)codev/resources/arch.md(+1 / -1)codev/resources/commands/agent-farm.md(+31 / -4)codev/resources/lessons-learned.md(+6 / -0)codev/state/pir-1869_thread.md(+29 / -0)packages/codev/src/agent-farm/__tests__/ide-forward-integration.test.ts(+85 / -0)packages/codev/src/agent-farm/__tests__/ide-forward.test.ts(+168 / -8)packages/codev/src/agent-farm/__tests__/ide-server-preflight.test.ts(+32 / -1)packages/codev/src/agent-farm/__tests__/pir-1869-ide-cookie-ws.test.ts(+185 / -0)packages/codev/src/agent-farm/__tests__/pir-1869-ide-forward-cookie.test.ts(+215 / -0)packages/codev/src/agent-farm/__tests__/tower-routes.test.ts(+127 / -0)packages/codev/src/agent-farm/lib/ide-record.ts(+29 / -0)packages/codev/src/agent-farm/servers/ide-forward.ts(+248 / -30)packages/codev/src/agent-farm/servers/ide-server.ts(+4 / -2)packages/codev/src/agent-farm/servers/tower-routes.ts(+87 / -5)packages/codev/src/agent-farm/servers/tower-websocket.ts(+11 / -7)packages/codev/src/agent-farm/utils/server-utils.ts(+150 / -25)packages/codev/templates/tower.html(+66 / -4)Commits
41e044819chore(porch): 1869 review phase-transition475c18e37chore(porch): 1869 dev-approval gate-approved2a9a55ae2[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Show the IDE row first on each workspace carda521cfcc1chore(porch): 1869 dev-approval gate-requested95c01b116[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Thread: amendment 1 implemented1ba1d9142[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Plan amendment 1 and docs: workbench routing, token handling, never hand-build ?tkn=2d96e03f0[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Log the pathname only for refused /ide requestsb0b7f30c4[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Route the local workbench through Tower and keep the IDE token out of the cookie jar012f4d7ef[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Rotate the IDE connection token on every IDE server spawnc3a7c6ac1[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Document the Open IDE path and the /ide session cookie trust notes0d112d79f[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Open IDE link per workspace on the Tower dashboarded71b0646[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Mint endpoint POST /api/ide/session, log-safe refusals, CORS credentials invariant8e23af98f[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Strip the /ide session cookie before forwarding to the IDE server78f39a504[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Forward seam accepts the /ide session cookie behind a cross-port Origin guardba40127efchore(porch): 1869 implement phase-transitionece7d3ac3chore(porch): 1869 plan-approval gate-approved22214ea98[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Plan revised: multi-user trust wordingb0f461cbe[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Plan revised: security review S1-S4ccdfd3f7d[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Plan revised: security review M1-M41b0d2ec28[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Plan revised: cross-port cookie risk22583e81echore(porch): 1869 plan-approval gate-requested237806b1e[PIR Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869] Plan draft6bdd146c9chore(porch): 1869 init pirTest Results
pnpm build: ✓ passtsc --noEmit(packages/codev): ✓ cleanvitest(packages/codev): 7218 passed, 1 failed locally, 49 skipped. The one failure isprotocol-drift-audit.test.ts("registry unreachable"), caused by this machine's npm config (see Flaky Tests). It is unrelated, passes in CI, and was not skipped. New coverage:pir-1869-ide-forward-cookie.test.ts: the seam, the cross-port guard, the derivation, andisRequestAlloweddelegation.pir-1869-ide-cookie-ws.test.ts: the realsetupUpgradeHandlerwith realwsclients and a stub IDE server.ide-forward.test.ts,ide-forward-integration.test.ts,ide-server-preflight.test.tsandtower-routes.test.ts.Sec-Fetch-SiteWebSocket tests fail.handleRequest+setupUpgradeHandleron a :4199 harness with the real IDE server on :8200 (live Tower untouched):Open IDE opens the workbench with the Codev sidebar ("Codev: Connected"), and the tab has no
opener.After Open IDE and after a direct reload, the cookie jar holds only
codev-ide-session(127.0.0.1,/ide, HttpOnly, Strict). Novscode-tkn.Both workbench WebSockets are
ws://127.0.0.1:4199/ide/…and stay open. None target:8200.A page on another localhost port holding the cookie: its WebSocket to Tower is closed with 4401, and its credentialed fetch is blocked.
curl:
/ide//iderouteOrigin: http://localhost:3000:8200with the forward cookieOriginArchitecture Updates
COLD
codev/resources/arch.md, invariant 9 (Tower API Authentication) now records the one ambient credential:/ideacceptscodev-ide-sessiononly indecideForwardAuthorization, in the order Host → header → cookie with its Origin check.Access-Control-Allow-Credentials.x-forwarded-host, keeps the IDE token out of the cookie jar (it goes into the config instead), replaces any browser-heldvscode-tkn, and rotates the token on every spawn.x-forwarded-*headers are stripped; the tunnel path keeps the Set-Cookie.No HOT
arch-critical.mdchange: this is a subsystem-scoped auth detail, and the hot file already points into arch.md for Tower auth.Lessons Learned Updates
COLD
codev/resources/lessons-learned.md:Hostmust stampx-forwarded-host, or the backend's own URLs point past the proxy.@cluesmith:registryin the user npm config makesprotocol-drift-auditfail every time. Work around it withNPM_CONFIG_USERCONFIG=<empty file>; never edit~/.npmrc.No HOT
lessons-critical.mdchange: the existing hot lesson "verify the real user path end-to-end; derive the check from the user's action" already covers the general rule, and this PR is one more instance of it.Things to Look At During PR Review
3-way consultation (single pass): Gemini skipped; Codex and Claude returned REQUEST_CHANGES
PIR runs the consultation once, so these fixes were not independently re-reviewed. The reviewer at the
prgate is the remaining check.Codex: the forward relayed an upstream
Access-Control-Allow-Credentials(breaks the M2 invariant). Fixed.STRIP_FROM_RESPONSEinide-forward.tsdrops it from HTTP responses and from the rebuilt101, on both the local and tunnel paths. Tests: unit tests for both functions on both paths, plus an integration test where the stub upstream sends the header and Tower does not.Codex: Open IDE on a recorded-but-dead server landed on a 502, because the forward only started an asynchronous respawn. Fixed.
POST /api/ide/sessionnow awaitsensureIdeServerLivebefore minting, so it respawns exactly as the forward would. It returns 503 ("IDE server unavailable: run afx ide start") when nothing can be made live, and the dashboard shows that as an error toast. Test: a route test asserting that the liveness check runs and thatnullreturns 503 with no Set-Cookie.Claude: the dashboard polled
GET /api/ideevery 5 s, which runs a blockingexecSync lsofon Tower's event loop. Fixed.tower.htmlnow checks IDE status on load and when the tab becomes visible again, never on the 5 s poll. Verified in Playwright: over about 12 s,/api/statuswas requested 3 times and/api/ideonce.Claude:
ensureIdeConnectionTokenhas no production callers. Addressed. Its doc comment now marks it as a test seam; spawns userotateIdeConnectionToken.Claude:
server-utils.tspulledtower-client/codev-sdkin throughide-record. Fixed.IDE_DEFAULT_PREFIXandmatchIdePrefixmoved to a dependency-freelib/ide-prefix.ts, whichide-record.tsre-exports.Claude: the local
x-forwarded-protois hardcoded tohttp. No change. Behind a non-tunnel HTTPS reverse proxy the workbench would buildws://URLs. That case was already broken before this PR, and C1 forbids trusting a client-suppliedx-forwarded-proto. Supporting a local TLS proxy is outside Local browser access to /ide/ on Tower: bootstrap the forward credential for a loopback browser and add an Open IDE link per workspace #1869.Claude: the cross-port guard passes a request that carries neither
OriginnorSec-Fetch-Site. No change; this is intended. Top-level navigation needs it. WebSockets and cross-origin fetches always carryOrigin, so the M1 hole stays closed. The remaining case is a cross-port subresource GET that the page cannot read.Claude: Unit Tests was still pending on the reviewed head. It was re-checked on the final head before handoff.
The workbench HTML rewrite in
servers/ide-forward.ts(injectWorkbenchConnectionToken,relayWorkbenchHtml). Tower now rewrites one third-party HTML page.text/htmlresponse with noContent-Encoding, within 2 MB. A chunked body that outgrows the bound mid-stream is relayed unchanged.^[A-Za-z0-9-]+$.JSON.parseboth before and after the edit, and{}is handled.vscode-workbench-web-configuration, and on the client preferringoptions.connectionTokenover the cookie, which I verified in the shipped bundle. An upstream change would surface as that visible "auth mismatch", not a silent hole.The order in
decideForwardAuthorization. A valid key header wins without the Origin check; that is what keeps tunnel requests, which carry a cloudOrigin, working. Only the cookie branch carries the guard.mergeConnectionTokenCookienow REPLACES an incomingvscode-tkn. Before this change it never overwrote one. The change is intentional (C2): Tower is the sole source of the token.Rotation happens only on spawn, never on adopt (
ide-server.ts). Rotating under a live server would lock Tower out of it.The tunnel path is unchanged except C1/C2. The cloud workbench still gets the token through the server's Set-Cookie. Moving the cloud to the config-injected token is a follow-up filed by the architect.
Accepted residuals, documented in
agent-farm.md:/idecookie. The architect filed a follow-up.vscode-tknfrom a hand-built?tkn=URL keeps it until the next IDE server spawn.How to Test Locally
afx dev pir-1869. To leave the live Tower untouched, run a harness that serves this branch'shandleRequest/setupUpgradeHandleron another port, as was done for dev-approval.afx ide starthas recorded a server, the dashboard shows IDE as the first row on each running workspace card.codev-ide-session(/ide, HttpOnly, Strict), and novscode-tkn.:8200.curl http://127.0.0.1:4100/ide/returns 401.Flaky Tests
No tests were skipped. One test fails locally because of this machine's environment; it is not flaky in the usual sense.
packages/codev/src/__tests__/protocol-drift-audit.test.ts:178("the REAL default lookup is offline-tolerant AND bounded when the registry is unreachable").@cluesmith:registryin the user npm config overrides the test'snpm_config_registry, sonpm viewreaches the real registry. An empty npm cache does not help.NPM_CONFIG_USERCONFIG=<empty temp file>, an environment-only change, with no repo change and no skip.