c0f6f9ef2a
* Add `BrowserTool` * add some logging * go with npm install -g * remove dep changes since the switch to npm install -g * tweak * tweak * tweak * tweak * tweak timeout * tweak * remove logs * skill investigation doc * wip * wip * tweak * lock agent-browser version * tweak * logs * logs * more logs * more debug stuff * try this * try this * try this * fix PATH * try this * tweak * tweak * tweak * update wiki entries * update wiki once again * lint fix --------- Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { dirname } from "node:path";
|
|
import type { ToolState } from "../mcp/server.ts";
|
|
import { log } from "./cli.ts";
|
|
import { filterEnv } from "./secrets.ts";
|
|
import { getDevDependencyVersion } from "./version.ts";
|
|
|
|
// agent-browser already discovers chrome via `which` and the playwright cache as fallbacks,
|
|
// so this list only needs to cover the GHA ubuntu-latest runner where we know the exact path.
|
|
const CHROME_PATHS = ["/usr/bin/google-chrome-stable"];
|
|
|
|
let systemChromePath: string | undefined;
|
|
|
|
function findSystemChromePath(): string | undefined {
|
|
if (typeof systemChromePath === "string") {
|
|
// return cached result but normalize to undefined if empty
|
|
return systemChromePath || undefined;
|
|
}
|
|
for (const p of CHROME_PATHS) {
|
|
if (existsSync(p)) {
|
|
systemChromePath = p;
|
|
log.info(`found system chrome: ${p}`);
|
|
return p;
|
|
}
|
|
}
|
|
// set to an empty string to indicate no system chrome found
|
|
// and to avoid repeated lookups
|
|
systemChromePath = "";
|
|
log.info(`no system chrome found (checked: ${CHROME_PATHS.join(", ")})`);
|
|
}
|
|
|
|
function buildEnv(): Record<string, string> {
|
|
const env: Record<string, string> = { ...filterEnv() };
|
|
const chromePath = findSystemChromePath();
|
|
if (chromePath) {
|
|
env.AGENT_BROWSER_EXECUTABLE_PATH = chromePath;
|
|
}
|
|
return env;
|
|
}
|
|
|
|
/**
|
|
* ensure the agent-browser daemon is running by issuing a real command.
|
|
*
|
|
* agent-browser is stateful — it manages a persistent browser process via a
|
|
* daemon that communicates over a Unix socket. we start the daemon here,
|
|
* outside of ShellTool, because ShellTool's child process lifecycle would
|
|
* kill it between invocations and the daemon must survive across calls.
|
|
*
|
|
* despite ShellTool commands running inside unshare-sandboxed namespaces,
|
|
* they can still reach this daemon because the Unix socket is discoverable
|
|
* regardless of unshare's PID/mount isolation. starting the daemon in the
|
|
* host namespace keeps it alive while sandboxed shells come and go.
|
|
*
|
|
* agent-browser auto-starts its daemon on the first CLI invocation and
|
|
* keeps it alive via the socket for subsequent commands.
|
|
* we run `open about:blank` as the seed command to trigger this.
|
|
* idempotent — only runs once.
|
|
*/
|
|
export function ensureBrowserDaemon(toolState: ToolState): string | undefined {
|
|
if (toolState.browserDaemon) {
|
|
return toolState.browserDaemon.error;
|
|
}
|
|
|
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
|
log.info(`installing agent-browser@${agentBrowserVersion}...`);
|
|
const install = spawnSync("npm", ["install", "-g", `agent-browser@${agentBrowserVersion}`], {
|
|
stdio: "pipe",
|
|
encoding: "utf-8",
|
|
});
|
|
if (install.status !== 0) {
|
|
const error = `agent-browser install failed: ${(install.stderr || install.stdout || "unknown error").trim()}`;
|
|
log.error(error);
|
|
toolState.browserDaemon = { error };
|
|
return error;
|
|
}
|
|
log.info("agent-browser installed");
|
|
|
|
let binDir: string;
|
|
try {
|
|
const binPath = execFileSync("which", ["agent-browser"], { encoding: "utf-8" }).trim();
|
|
binDir = dirname(binPath);
|
|
log.info(`agent-browser binary: ${binPath}`);
|
|
} catch {
|
|
const error = "agent-browser binary not found in PATH after install";
|
|
log.error(error);
|
|
toolState.browserDaemon = { error };
|
|
return error;
|
|
}
|
|
|
|
const env = buildEnv();
|
|
|
|
// `open about:blank` triggers daemon auto-start and returns once the daemon + browser are ready
|
|
log.info("starting browser daemon...");
|
|
const seed = spawnSync("agent-browser", ["open", "about:blank"], {
|
|
env,
|
|
stdio: "pipe",
|
|
encoding: "utf-8",
|
|
timeout: 30_000,
|
|
});
|
|
|
|
if (seed.status !== 0) {
|
|
const output = (seed.stderr || seed.stdout || "unknown error").trim();
|
|
const error = `agent-browser open about:blank failed (exit=${seed.status}): ${output}`;
|
|
log.error(error);
|
|
toolState.browserDaemon = { error };
|
|
return error;
|
|
}
|
|
log.debug(`seed command done (exit=0): ${(seed.stdout || "").trim()}`);
|
|
|
|
toolState.browserDaemon = { binDir };
|
|
log.info("browser daemon ready");
|
|
}
|
|
|
|
export function closeBrowserDaemon(toolState: ToolState): void {
|
|
if (!toolState.browserDaemon?.binDir) {
|
|
delete toolState.browserDaemon;
|
|
return;
|
|
}
|
|
delete toolState.browserDaemon;
|
|
|
|
try {
|
|
log.info("closing browser daemon...");
|
|
spawnSync("agent-browser", ["close"], {
|
|
env: filterEnv(),
|
|
stdio: "pipe",
|
|
timeout: 10_000,
|
|
});
|
|
log.info("browser daemon closed");
|
|
} catch {
|
|
// best-effort
|
|
}
|
|
}
|