Repository navigation
Require a per-start token on the local bridge and supervise it - #16
Merged
Merged
Conversation
The local CLI bridge accepted any request on its loopback port, so a sandboxed Worker CLI (which shares the host network) or a DNS-rebinding page could drive it. Foreman also never noticed when the bridge died. Bridge (investigations/local-cli-uhp/server.mjs): - When LOCAL_CLI_UHP_TOKEN is set, every route requires `Authorization: Bearer <token>`, compared as SHA-256 digests with timingSafeEqual. The variable is removed from process.env at startup. Without a token it still accepts requests and warns once. - The Host header must be 127.0.0.1, localhost or [::1] on the bound port, with or without a token. - A malformed request URL returns 400 instead of crashing the bridge, and a port that cannot be bound exits 1 at once. Foreman (src/local-bridge.ts, server.ts, controller.ts): - Each per-project bridge start generates a 32-byte token. It is passed to the bridge through its environment and used by every Foreman call: UHP discovery and turns (via bearerFetch), workspace seed, overlay, snapshot and usage. The status object keeps it non-enumerable so it never appears in API responses, events or logs. - The bridge's stdout and stderr go to <state dir>/bridge.log (0600), rotated to bridge.log.1 past 5 MiB at each (re)start. - A bridge that exits after becoming ready is restarted on the same port with the same token, with backoff of 1 s doubling to 30 s. After 5 consecutive runs that each ended within 60 s of starting it gives up. A deliberate stop never triggers a restart. - workspace-setup reports the bridge as ready, restarting or unavailable with its health (last exit, restart count, log path). Reopening a project whose bridge gave up starts a fresh one. - FOREMAN_WORKSPACE_BRIDGE_TOKEN (requires FOREMAN_WORKSPACE_BRIDGE_URL) and UHP_TOKEN cover a manually started bridge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
Foreman's notes in bridge.log were fire-and-forget appends, so start() could resolve before "starting bridge" reached the file (the log rotation test failed in CI, and 4 of 24 local runs under load), two notes could land out of order, and a note about the previous run could race the rotation at restart. Notes now go through one ordered queue. launch() waits for pending notes before rotating, and the startup path awaits its own notes. The ready note is written before the bridge is published as ready, so the ready state and its health notification still happen with no await between them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
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.
The local CLI bridge accepted any request on its loopback port. A sandboxed Worker CLI shares the host network, so it could drive the bridge API directly, and so could a DNS-rebinding page. Foreman also never noticed when a bridge died.
Bridge (
investigations/local-cli-uhp/server.mjs)LOCAL_CLI_UHP_TOKENis set, every route requiresAuthorization: Bearer <token>, including discovery.timingSafeEqual.process.envat startup. The CLI sandboxes already use env allowlists and their own PID namespace, so they cannot read it.127.0.0.1,localhostor[::1]on the bound port, with or without a token. Anything else gets 403.Foreman (
src/local-bridge.ts,server.ts,controller.ts,verified-workspace.ts)bearerFetch), workspace seed, overlay, snapshot, and usage.<state dir>/bridge.log(mode 0600). At each (re)start the log is rotated tobridge.log.1if it is over 5 MiB.workspace-setupreports the bridge asready,restartingorunavailable, with its last exit, restart count and log path.FOREMAN_WORKSPACE_BRIDGE_TOKEN(requiresFOREMAN_WORKSPACE_BRIDGE_URL) andUHP_TOKENcover this case.UHP_TOKENis now also sent on UHP discovery.Tests
tests/local-bridge.test.ts(+15):stop().kill -9of the bridge process.tests/bridge-token.test.ts(new):tests/config-bridge-token.test.ts(new): config validation, which never echoes the value.Verification
pnpm test:bridge: 62/62 passing.kill -9the bridge was ready again in 1.3 s on the same port with the same token.Follow-ups, not in this PR
🤖 Generated with Claude Code
https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
Generated by Claude Code