From 7907c78979f339c00161c0a37b2269e5317f359d Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:03:39 +1100 Subject: [PATCH] fix(server): browser setup advice names the server's T3 home Co-Authored-By: Claude Opus 5.5 --- apps/server/src/htmlRender/HtmlRender.test.ts | 29 +++++++++++++++++++ apps/server/src/htmlRender/HtmlRender.ts | 6 +++- apps/server/src/preview/ServerBrowser.ts | 6 +++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/server/src/htmlRender/HtmlRender.test.ts b/apps/server/src/htmlRender/HtmlRender.test.ts index 1916327672516..7d555c46a4e6c 100644 --- a/apps/server/src/htmlRender/HtmlRender.test.ts +++ b/apps/server/src/htmlRender/HtmlRender.test.ts @@ -22,6 +22,7 @@ import * as ServerConfig from "../config.ts"; import * as HtmlRender from "./HtmlRender.ts"; import { HtmlRenderBrowserError } from "./headlessChrome.ts"; import * as PreviewBrowser from "../preview/PreviewBrowser.ts"; +import * as PreviewBrowserHost from "../preview/PreviewBrowserHost.ts"; const TEST_BROWSER_ENV = "T3CODE_TEST_HEADLESS_SHELL"; @@ -365,6 +366,34 @@ fs.createReadStream(null, { fd: 3 }).on("end", quit).on("data", (chunk) => { expect(Option.getOrElse(exitedBy, () => "killed by a signal")).toBe("asked to close"); }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ); + + it.live("names this server's home in the browser setup command", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const directory = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-fake-browser-", + }); + const executable = path.join(directory, "chrome-headless-shell"); + yield* fileSystem.writeFileString( + executable, + `#!${process.execPath}\nprocess.stderr.write(${JSON.stringify(PreviewBrowserHost.NO_SANDBOX_SIGNATURE)});\nprocess.exit(1);\n`, + { mode: 0o755 }, + ); + + const { error, baseDir } = yield* Effect.gen(function* () { + const { baseDir } = yield* ServerConfig.ServerConfig; + const htmlRender = yield* HtmlRender.HtmlRender; + const error = yield* htmlRender.preview({ html: "

x

" }).pipe(Effect.flip); + return { error, baseDir }; + }).pipe( + Effect.provide(layerHtmlRender(executable)), + Effect.provideService(HostProcess.UserId, 1000), + ); + expect(error).toBeInstanceOf(PreviewBrowserHost.PreviewBrowserSandboxError); + expect(error.message).toContain(`browser setup --base-dir ${baseDir}`); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); }, ); diff --git a/apps/server/src/htmlRender/HtmlRender.ts b/apps/server/src/htmlRender/HtmlRender.ts index 44ea627ceab72..d1330cc679ab5 100644 --- a/apps/server/src/htmlRender/HtmlRender.ts +++ b/apps/server/src/htmlRender/HtmlRender.ts @@ -340,7 +340,11 @@ const make = Effect.gen(function* () { const browsers = yield* Semaphore.make(MAX_CONCURRENT_BROWSERS); // Keep Chrome sandboxed unless explicitly disabled; root must opt out to run. const noSandbox = PreviewBrowserHost.sandboxDisabled(yield* HostProcess.Environment); - const setupCommand = yield* resolveRootCliCommand(PreviewBrowserHost.SETUP_SUBCOMMAND); + // sudo resets the environment, so the command has to name this server's home. + const setupCommand = yield* resolveRootCliCommand(PreviewBrowserHost.SETUP_SUBCOMMAND, [ + "--base-dir", + config.baseDir, + ]); const launching = ( executable: string, diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 6257a686764f1..44d28805587bb 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -469,7 +469,11 @@ const make = Effect.gen(function* () { const runFork = Effect.runForkWith(yield* Effect.context()); const runPromise = Effect.runPromiseWith(yield* Effect.context()); const launchServices = yield* Effect.context(); - const setupCommand = yield* resolveRootCliCommand(PreviewBrowserHost.SETUP_SUBCOMMAND); + // sudo resets the environment, so the command has to name this server's home. + const setupCommand = yield* resolveRootCliCommand(PreviewBrowserHost.SETUP_SUBCOMMAND, [ + "--base-dir", + config.baseDir, + ]); const agentScope = yield* AgentScope; const launchersDir = NodePath.join(config.stateDir, "server-browser", "launchers");