Skip to content

Commit dc0e80e

Browse files
fix(cli): capture works inside an agent's macOS sandbox (#5840)
The seatbelt launch runs Chrome as one process, where there is no GPU process to composite. Capture's flags left GPU compositing on, so every screenshot failed and heavier pages stalled until the protocol timeout. The one-process launch now also disables GPU compositing, as software capture already does.
1 parent 76ba031 commit dc0e80e

4 files changed

Lines changed: 19 additions & 10 deletions

File tree

‎packages/cli/src/browser/launch.noListen.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe("the GPU probe where nothing may listen", () => {
3030
await resolveManagedGpuMode("auto", "/chrome");
3131
expect(probe.options[0]).toMatchObject({
3232
pipe: true,
33-
args: ["--headless", "--single-process"],
33+
args: ["--headless", "--single-process", "--disable-gpu-compositing"],
3434
});
3535
expect(probe.options[1]!.pipe).toBeUndefined();
3636
expect(probe.options[1]!.args).toEqual(["--headless"]);

‎packages/cli/src/browser/sandboxDenied.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,22 @@ describe("a browser inside an agent's macOS seatbelt", () => {
5858
const { puppeteer, launches } = recordingPuppeteer([]);
5959
await launchManagedBrowser(puppeteer, { args: ["--headless"] });
6060
expect(launches).toHaveLength(1);
61-
expect(launches[0]).toMatchObject({ pipe: true, args: ["--headless", "--single-process"] });
61+
expect(launches[0]).toMatchObject({
62+
pipe: true,
63+
args: ["--headless", "--single-process", "--disable-gpu-compositing"],
64+
});
6265
});
6366

6467
it("relaunches as one process over a pipe when an unnamed sandbox refuses the mach service", async () => {
6568
vi.stubEnv("CODEX_SANDBOX", "");
6669
const { puppeteer, launches } = recordingPuppeteer([SEATBELT_LAUNCH_FAILURE]);
6770
await expect(launchManagedBrowser(puppeteer, { args: ["--headless"] })).resolves.toBeDefined();
6871
expect(launches.map((options) => options.pipe ?? false)).toEqual([false, true]);
69-
expect(launches[1]!.args).toEqual(["--headless", "--single-process"]);
72+
expect(launches[1]!.args).toEqual([
73+
"--headless",
74+
"--single-process",
75+
"--disable-gpu-compositing",
76+
]);
7077
});
7178
});
7279

‎packages/cli/src/browser/sandboxDenied.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { LaunchOptions } from "puppeteer-core";
22

33
const MACH_SERVICE_DENIED = /bootstrap_check_in\b[^\n]*Permission denied/;
44
const SINGLE_PROCESS = "--single-process";
5+
const CPU_COMPOSITING = "--disable-gpu-compositing";
56

67
export function machServiceDenied(errorMessage: string): boolean {
78
return MACH_SERVICE_DENIED.test(errorMessage);
@@ -12,14 +13,11 @@ export function inAgentSandbox(env = process.env, platform = process.platform):
1213
return platform === "darwin" && env.CODEX_SANDBOX === "seatbelt";
1314
}
1415

15-
/** The one launch a seatbelt allows: no child process can register its mach port and nothing may listen. */
16+
/** The one launch a seatbelt allows: no child process registers a mach port, nothing listens, no GPU composites. */
1617
export function oneProcessOverPipe(options: LaunchOptions): LaunchOptions {
1718
const args = options.args ?? [];
18-
return {
19-
...options,
20-
pipe: true,
21-
args: args.includes(SINGLE_PROCESS) ? args : [...args, SINGLE_PROCESS],
22-
};
19+
const missing = [SINGLE_PROCESS, CPU_COMPOSITING].filter((flag) => !args.includes(flag));
20+
return { ...options, pipe: true, args: [...args, ...missing] };
2321
}
2422

2523
export function isOneProcessOverPipe(options: LaunchOptions): boolean {

‎packages/cli/src/capture/captureCompositionFrame.browserFallback.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,11 @@ describe("openSettledCompositionPage when nothing may listen", () => {
221221
await openSettledCompositionPage(HTML, server.url, OPTIONS);
222222

223223
expect(launched().pipe).toBe(true);
224-
expect(launched().args).toEqual(["--headless", "--single-process"]);
224+
expect(launched().args).toEqual([
225+
"--headless",
226+
"--single-process",
227+
"--disable-gpu-compositing",
228+
]);
225229
expect(opened.page.setRequestInterception).toHaveBeenCalledWith(true);
226230
const onRequest = opened.page.on.mock.calls.find(([event]) => event === "request")![1];
227231
const respond = vi.fn(async (_response: { status: number; body: Buffer }) => undefined);

0 commit comments

Comments
 (0)