From 57fa0ab56c81d676eb791461d695d04d652dc2a1 Mon Sep 17 00:00:00 2001 From: Jaron Kramer Date: Wed, 21 Oct 2026 15:11:28 +0200 Subject: [PATCH 1/2] fix(server): clarify browser setup when running as root --- .../src/preview/PreviewBrowserHost.test.ts | 30 +++++++++++++++++++ apps/server/src/preview/PreviewBrowserHost.ts | 2 +- docs/user/remote-access.md | 18 +++++++++-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/apps/server/src/preview/PreviewBrowserHost.test.ts b/apps/server/src/preview/PreviewBrowserHost.test.ts index b0b52a9362b9c..429a1145dcc92 100644 --- a/apps/server/src/preview/PreviewBrowserHost.test.ts +++ b/apps/server/src/preview/PreviewBrowserHost.test.ts @@ -73,6 +73,36 @@ const diagnose = (input: { }; describe("diagnoseLaunchFailure", () => { + it.effect("explains how to restart a root server without Chrome's sandbox", () => + Effect.gen(function* () { + const { error, commands } = yield* diagnose({ + platform: "linux", + uid: 0, + output: PreviewBrowserHost.NO_SANDBOX_SIGNATURE, + ldd: "", + }); + expect(error).toBeInstanceOf(PreviewBrowserHost.PreviewBrowserRootError); + expect(error?.message).toContain("T3 server is running as root"); + expect(error?.message).toContain("T3 server's environment and restart the server"); + expect(error?.message).toContain("T3CODE_SERVER_BROWSER_SANDBOX=0 t3 serve"); + expect(error?.message).toContain("Environment=T3CODE_SERVER_BROWSER_SANDBOX=0"); + expect(error?.message).toContain("this disables Chrome's sandbox"); + expect(commands).toEqual([]); + }), + ); + + it.effect("keeps host setup guidance for a nonroot sandbox failure", () => + Effect.gen(function* () { + const { error } = yield* diagnose({ + platform: "linux", + output: PreviewBrowserHost.NO_SANDBOX_SIGNATURE, + ldd: "", + }); + expect(error).toBeInstanceOf(PreviewBrowserHost.PreviewBrowserSandboxError); + expect(error?.message).toContain("Run `sudo t3 browser setup` on the host"); + }), + ); + it.effect("names every library the loader cannot find", () => Effect.gen(function* () { const { error } = yield* diagnose({ diff --git a/apps/server/src/preview/PreviewBrowserHost.ts b/apps/server/src/preview/PreviewBrowserHost.ts index 179ba9a1d2d9f..cd1c5382d3782 100644 --- a/apps/server/src/preview/PreviewBrowserHost.ts +++ b/apps/server/src/preview/PreviewBrowserHost.ts @@ -79,7 +79,7 @@ export class PreviewBrowserRootError extends Schema.TaggedError Date: Fri, 30 Oct 2026 14:24:12 +0000 Subject: [PATCH 2/2] fix(server): keep root browser guidance within MCP error limit --- .../src/mcp/PreviewAutomationBroker.test.ts | 24 +++++++++++++++---- apps/server/src/preview/PreviewBrowserHost.ts | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index 7ae8bfbefead5..8fa6a391a3e75 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -19,6 +19,7 @@ import * as Fiber from "effect/Fiber"; import * as Result from "effect/Result"; import * as Stream from "effect/Stream"; +import { PreviewBrowserRootError } from "../preview/PreviewBrowserHost.ts"; import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; const makeBroker = PreviewAutomationBroker.make.pipe(Effect.provide(NodeServices.layer)); @@ -288,9 +289,22 @@ it.effect("announces a live replacement stream before delivering requests", () = ); it.effect.each([ - { clientId: SERVER_BROWSER_AUTOMATION_CLIENT_ID, shown: true }, - { clientId: "client-1", shown: false }, -])("tells the agent why its own server browser failed ($clientId)", ({ clientId, shown }) => + { + clientId: SERVER_BROWSER_AUTOMATION_CLIENT_ID, + shown: true, + message: "page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:4719/", + }, + { + clientId: "client-1", + shown: false, + message: "page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:4719/", + }, + { + clientId: SERVER_BROWSER_AUTOMATION_CLIENT_ID, + shown: true, + message: new PreviewBrowserRootError().message, + }, +])("preserves server browser failure guidance ($clientId)", ({ clientId, shown, message }) => Effect.scoped( Effect.gen(function* () { const broker = yield* makeBroker; @@ -303,7 +317,7 @@ it.effect.each([ ok: false, error: { _tag: "PreviewAutomationExecutionError", - message: "page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:4719/", + message, }, }), ).pipe(Effect.forkScoped); @@ -312,7 +326,7 @@ it.effect.each([ .invoke({ scope, operation: "open", input: {} }) .pipe(Effect.flip); expect(error._tag).toBe("PreviewAutomationExecutionError"); - expect(error.message.includes("ERR_CONNECTION_REFUSED")).toBe(shown); + expect(error.message.includes(message)).toBe(shown); }), ), ); diff --git a/apps/server/src/preview/PreviewBrowserHost.ts b/apps/server/src/preview/PreviewBrowserHost.ts index cd1c5382d3782..afc79044eb307 100644 --- a/apps/server/src/preview/PreviewBrowserHost.ts +++ b/apps/server/src/preview/PreviewBrowserHost.ts @@ -79,7 +79,7 @@ export class PreviewBrowserRootError extends Schema.TaggedError