Skip to content

fix(hub): reach the dev app across the sandbox's network namespaces on Linux - #72

Closed
brianjfox wants to merge 1 commit into
fix/agent-sandboxes-in-composefrom
fix/app-reachable-across-sandboxes
Closed

brianjfox wants to merge 1 commit into
fix/agent-sandboxes-in-composefrom
fix/app-reachable-across-sandboxes

Conversation

@brianjfox

Copy link
Copy Markdown
Contributor

Closes #71

Stacked on #66, which it shares the sandbox executor with; its base should move to main once #66 merges.

What changed

  • apps/hub/src/builds/posix-sandbox.ts: a new createAppRelay decides how the dev app is reached from outside its wrap. On macOS, where every wrap shares the host's loopback, nothing changes. On Linux, where each wrap has a loopback of its own, the app is reached through app.sock in the sockets folder the wraps and the hub share:
    • a socat inside the app's wrap serves its client port on the socket;
    • a socat in this process serves the socket on a port picked once and kept across app restarts, and the app's URL names that port;
    • every other wrap (the browser, the page watcher, and commands the hub runs such as check-app) is prefixed with a socat serving the socket on that same port, so the one URL works there too.
      The executor gains an app option, and the sockets folder is created on Linux for app workspaces as it already was for the browser. Relays stop with the app and on dispose.
  • apps/hub/src/builds/sidecar-tools.ts and apps/hub/src/live-app/host.ts pass app for app workspaces.

Why

After the first build finished, the Live Preview stayed on "Starting the app" indefinitely. The app was up, but in network namespace 4026532474, while the hub polling its URL was in 4026532382. The coder's Playwright browser runs in a third namespace and could not reach the app either. Details in #71.

How it was verified

  • tsc --noEmit -p . passes; oxlint --deny-warnings and Prettier pass on the changed files; apps/hub/src/builds and apps/hub/src/live-app tests pass.
  • End to end, twice, in a throwaway hub container (Colima, Ubuntu 24.04, arm64) with this file in place, against a copy of the app the coder built today: executor.app() returned a relayed URL, a fetch of it from outside the sandbox answered 200 within a second, and a probe command run through executor.command() fetched the same URL from inside its own wrap and also got 200. The app's own log showed its client on a different port inside the sandbox, as expected.

…n Linux

On Linux every sandboxed command runs in a network namespace of its own,
so the dev app started inside the sandbox listened on a loopback the hub
could not reach. The live-app host polled its URL forever and reported
"Starting the app", and the coder's browser and page watcher, each in a
wrap of their own, could not reach it either.

Relay the app through a Unix socket in the sockets folder the wraps and
the hub share: a socat inside the app's wrap serves its client port on
the socket, a socat in this process serves the socket on a port picked
once, and every other wrap gets a socat serving the socket on that same
port, so one URL holds for the hub, the browser, the page watcher and the
hub's own commands. The port survives restarts of the app, which the
browser outlives. On macOS, where the wraps share the host's loopback,
nothing changes.

Closes #71

@corbits-triage corbits-triage Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triage requests changes:

  • Conventions: added code matches a code pattern

@TheGreatAxios

Copy link
Copy Markdown
Contributor

#81 folded in all of #72's changes (apps/hub/src/builds/posix-sandbox.ts, apps/hub/src/builds/sidecar-tools.ts, apps/hub/src/live-app/host.ts) so the full pipeline E2E replay could run on Linux in CI, with two additions on top:

  1. Mount and PID isolation for appRelay.start(): Because sockets (/tmp/sb-browser-*) is in allowWrite inside the sandbox while appRelay.start() runs socat ... UNIX-CONNECT:${sockets}/app.sock on the host, a process inside the sandbox could replace app.sock with a symlink to a host Unix socket (for example /var/run/docker.sock) and have the host socat follow it in the host mount namespace. test(e2e): replay recorded weather app project end to end in CI #81 wraps appRelay.start()'s socat in a minimal bwrap (--new-session --die-with-parent --unshare-user --unshare-pid binding only /usr, /bin, /lib, /lib64, and ${sockets}, without --unshare-net so host loopback 127.0.0.1:${port} still works and forked socat children die with the parent).
  2. allowsLinuxUnixSockets and apply-seccomp scoping: On Linux, designer runs check-app (Bun.WebView / headless Chrome) with options.browsers set and sockets === undefined, so test(e2e): replay recorded weather app project end to end in CI #81 also enables allowAllUnixSockets: true when options.browsers is set on Linux — and wraps childCommand (posix-child.ts) before calling SandboxManager.updateConfig with allowAllUnixSockets: true, keeping apply-seccomp active on run_shell tool calls while allowing Unix sockets for sidecar-owned wrapShell calls (startApp, pageWatcher, startBrowser, check-app).

Since all of #72 is now on main via #81, this PR is superseded and can be closed.

@brianjfox

Copy link
Copy Markdown
Contributor Author

Superseded: #81 carried the app relay across the sandbox's network namespaces to main. The port range that goes with it continues in #93, rebased onto main.

@brianjfox brianjfox closed this Nov 2, 2026
@brianjfox
brianjfox deleted the fix/app-reachable-across-sandboxes branch November 4, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants