From f0efc881271c7a6f224480297f21b8c62294173e Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Mon, 2 Nov 2026 02:10:31 +0000 Subject: [PATCH 1/4] fix(hub): resolve the sandbox child's workspace packages from source The hub runs with --conditions=intx-src, which resolves the vendored workspace packages to their TypeScript source, but it launched the sandbox child that performs every file read and write without it. The child then followed the packages' default export to a built dist that ships nowhere, and every designer and coder tool call failed with "Cannot find module '@intx/tools-posix'". --- apps/hub/src/builds/posix-sandbox.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/hub/src/builds/posix-sandbox.ts b/apps/hub/src/builds/posix-sandbox.ts index e20bce39..5568f7b2 100644 --- a/apps/hub/src/builds/posix-sandbox.ts +++ b/apps/hub/src/builds/posix-sandbox.ts @@ -328,7 +328,9 @@ export async function createPosixBuildExecutor( ); initialized = true; stage = "wrap"; - const childCommand = `exec ${[executable, "--no-env-file", layout.childPath, cwd].map(quote).join(" ")}`; + // The child resolves the workspace packages from source like the hub does; + // the vendored packages ship no built dist. + const childCommand = `exec ${[executable, "--no-env-file", "--conditions=intx-src", layout.childPath, cwd].map(quote).join(" ")}`; const wrapped = await SandboxManager.wrapWithSandbox( childCommand, "/bin/sh", From c039bb0d46362d8b4a9d15e1b1881f30c15a1b4d Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Mon, 2 Nov 2026 02:10:31 +0000 Subject: [PATCH 2/4] fix(hub): let the design check run on arm64 Linux and say what went wrong Chrome for Testing now publishes a headless shell for 64-bit ARM Linux, so install it there too; the hub assumed no such build existed and installed nothing on an arm64 host, such as the VM Colima runs. With no browser installed, check-app crashed scanning the missing $HOME/browsers folder instead of reporting the browser rule, and the sandbox command runner discarded stderr, so a designer saw an empty checklist and a failed status with no reason. Treat the missing folder as "browser not installed", and keep a failed command's stderr in its output. --- apps/hub/src/builds/posix-sandbox.ts | 20 ++++++++++++++------ apps/hub/src/builds/starter.ts | 5 +++-- workflows/coder/check/check-app.ts | 3 ++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/hub/src/builds/posix-sandbox.ts b/apps/hub/src/builds/posix-sandbox.ts index 5568f7b2..5967d171 100644 --- a/apps/hub/src/builds/posix-sandbox.ts +++ b/apps/hub/src/builds/posix-sandbox.ts @@ -781,7 +781,7 @@ export async function createPosixBuildExecutor( env: childEnv, stdin: "ignore", stdout: "pipe", - stderr: "ignore", + stderr: "pipe", detached: process.platform !== "linux", }, ); @@ -790,11 +790,19 @@ export async function createPosixBuildExecutor( const timer = setTimeout(stop, limits.commandTimeoutMs); signal.addEventListener("abort", stop, { once: true }); try { - const output = await new Response(child.stdout).text(); - return { - exitCode: await child.exited, - output: output.replaceAll(cwd + sep, ""), - }; + const [output, errors] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + const exitCode = await child.exited; + // A command that died before printing its checklist would otherwise fail with nothing said. + const text = + exitCode === 0 + ? output + : [output, errors.slice(-2000)] + .filter((part) => part.trim()) + .join("\n"); + return { exitCode, output: text.replaceAll(cwd + sep, "") }; } finally { clearTimeout(timer); signal.removeEventListener("abort", stop); diff --git a/apps/hub/src/builds/starter.ts b/apps/hub/src/builds/starter.ts index 74b52f31..626f7388 100644 --- a/apps/hub/src/builds/starter.ts +++ b/apps/hub/src/builds/starter.ts @@ -42,6 +42,7 @@ const CHROME_VERSION = "156.0.8078.4"; const CHROME_PLATFORMS: Record = { "darwin-arm64": "mac-arm64", "darwin-x64": "mac-x64", + "linux-arm64": "linux-arm64", "linux-x64": "linux64", }; const CHROME_PLATFORM = CHROME_PLATFORMS[`${process.platform}-${process.arch}`]; @@ -55,8 +56,8 @@ const exists = (path: string) => ); // One Chrome for every sandbox, unpacked once beside the hub's other caches and only ever read by -// the sandboxes. Chrome for Testing has no build for some hosts (linux-arm64); there no browser is -// installed and check-app reports it missing. +// the sandboxes. On a host Chrome for Testing has no build for, no browser is installed and +// check-app reports it missing. const BROWSER_CACHE = join( homedir(), ".cache", diff --git a/workflows/coder/check/check-app.ts b/workflows/coder/check/check-app.ts index b3d57881..d5bd3de8 100644 --- a/workflows/coder/check/check-app.ts +++ b/workflows/coder/check/check-app.ts @@ -572,12 +572,13 @@ if (staticRoot) { // The sandbox setup unpacks Chrome's headless shell under $HOME/browsers. A sandboxed process may not // register Mach services, so Chrome runs as one process with its own sandbox off. +// With no browser installed the folder is not there at all, which is the same finding. const [chrome] = await Array.fromAsync( new Bun.Glob("chrome-headless-shell-*/chrome-headless-shell").scan({ cwd: join(homedir(), "browsers"), absolute: true, }), -); +).catch(() => [] as string[]); if (!chrome) { stop(); fail("browser", "nothing under $HOME/browsers"); From 464ac2931f70c11f1041606259d6116cbccfa885 Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Mon, 2 Nov 2026 02:10:31 +0000 Subject: [PATCH 3/4] fix(hub): make the sandbox's TMPDIR before a command runs srt sets TMPDIR to /tmp/claude for every sandboxed command but never creates that folder inside the sandbox's own /tmp. Bun's WebView needs a temp folder for Chrome's profile, so check-app failed with "Failed to spawn Chrome" before Chrome ran. The command shim now creates TMPDIR first. --- apps/hub/src/builds/posix-sandbox.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/hub/src/builds/posix-sandbox.ts b/apps/hub/src/builds/posix-sandbox.ts index 5967d171..e3171096 100644 --- a/apps/hub/src/builds/posix-sandbox.ts +++ b/apps/hub/src/builds/posix-sandbox.ts @@ -236,7 +236,12 @@ export async function createPosixBuildExecutor( `command_build_failed:${name} ${built.logs.join("; ")}`, ); const shim = join(shimDirectory, name); - await writeFile(shim, `#!/bin/sh\nexec ${quote(binary)} "$@"\n`); + // srt points TMPDIR at a folder it does not make inside the sandbox's own /tmp, and Bun's + // WebView cannot start Chrome without a temp folder. + await writeFile( + shim, + `#!/bin/sh\n[ -z "$TMPDIR" ] || mkdir -p -- "$TMPDIR"\nexec ${quote(binary)} "$@"\n`, + ); await chmod(shim, 0o755); } const pageWatcher = join(shimDirectory, "watch-page.ts"); From a9a0560e606f5d3e9ee4cf11109fb900ddc92397 Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Mon, 2 Nov 2026 02:10:31 +0000 Subject: [PATCH 4/4] fix(hub): let sandboxed commands reach srt's proxy bridge sockets On Linux srt relays the sandbox's proxy ports to the host over Unix sockets in the host's temp folder and binds them into the sandbox before the hub's default-deny read policy lands, which wipes them. Every connection through the proxy was reset, so a coder's workspace setup failed to download any package. After initializing the sandbox, name the bridge sockets in allowRead so srt binds them back over the deny. --- apps/hub/src/builds/posix-sandbox.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/hub/src/builds/posix-sandbox.ts b/apps/hub/src/builds/posix-sandbox.ts index e3171096..ceeb5984 100644 --- a/apps/hub/src/builds/posix-sandbox.ts +++ b/apps/hub/src/builds/posix-sandbox.ts @@ -332,6 +332,26 @@ export async function createPosixBuildExecutor( : undefined, ); initialized = true; + // On Linux srt relays the sandbox's proxy ports to the host over Unix sockets in the host's temp + // folder, which the default-deny read policy hides; srt binds them before that deny lands, so + // every connection through the proxy was reset. Named in allowRead, they are bound back over it. + if (process.platform === "linux") { + await SandboxManager.waitForNetworkInitialization(); + const bridges = new Set( + [ + SandboxManager.getLinuxHttpSocketPath(), + SandboxManager.getLinuxSocksSocketPath(), + ].filter((path): path is string => path !== undefined), + ); + if (bridges.size) + SandboxManager.updateConfig({ + ...policy, + filesystem: { + ...policy.filesystem, + allowRead: [...(policy.filesystem.allowRead ?? []), ...bridges], + }, + }); + } stage = "wrap"; // The child resolves the workspace packages from source like the hub does; // the vendored packages ship no built dist.