feat: ship enterprise control and desktop governance
This commit is contained in:
208
local-agent/browser-control-task-runner.mjs
Normal file
208
local-agent/browser-control-task-runner.mjs
Normal file
@@ -0,0 +1,208 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
|
||||
function parseBoolean(value) {
|
||||
return String(value || "").trim().toLowerCase() === "true";
|
||||
}
|
||||
|
||||
function parseArgs(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseTimeoutMs(value) {
|
||||
const parsed = Number.parseInt(String(value || ""), 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 45000;
|
||||
}
|
||||
|
||||
function pickConfigValue(config, key, fallback) {
|
||||
if (config && config[key] !== undefined && config[key] !== null && `${config[key]}`.trim() !== "") {
|
||||
return config[key];
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function resolveCommandArgs(command, args, cwd) {
|
||||
const runtimeName = path.basename(command || "").toLowerCase();
|
||||
const scriptRuntimes = new Set([
|
||||
"node",
|
||||
"node.exe",
|
||||
"tsx",
|
||||
"tsx.cmd",
|
||||
"bun",
|
||||
"bun.exe",
|
||||
"deno",
|
||||
"deno.exe",
|
||||
]);
|
||||
if (!scriptRuntimes.has(runtimeName) || args.length === 0) {
|
||||
return args;
|
||||
}
|
||||
const [first, ...rest] = args;
|
||||
if (!first || first.startsWith("-")) {
|
||||
return args;
|
||||
}
|
||||
return [path.isAbsolute(first) ? first : path.resolve(cwd || process.cwd(), first), ...rest];
|
||||
}
|
||||
|
||||
function parseJsonLine(rawOutput) {
|
||||
const lines = String(rawOutput || "")
|
||||
.trim()
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
return JSON.parse(lines.at(-1) || "");
|
||||
}
|
||||
|
||||
export function getBrowserControlTaskRunnerConfig(env = process.env, config = {}) {
|
||||
const enabled = parseBoolean(pickConfigValue(config, "browserControlEnabled", env.BOSS_BROWSER_CONTROL_ENABLED));
|
||||
const command = String(pickConfigValue(config, "browserControlCommand", env.BOSS_BROWSER_CONTROL_COMMAND) || "").trim() || undefined;
|
||||
const args = Array.isArray(config?.browserControlArgs)
|
||||
? config.browserControlArgs.map((item) => String(item)).filter(Boolean)
|
||||
: parseArgs(pickConfigValue(config, "browserControlArgs", env.BOSS_BROWSER_CONTROL_ARGS));
|
||||
const cwd = String(pickConfigValue(config, "browserControlWorkdir", env.BOSS_BROWSER_CONTROL_WORKDIR) || "").trim() || undefined;
|
||||
const timeoutMs = parseTimeoutMs(pickConfigValue(config, "browserControlTimeoutMs", env.BOSS_BROWSER_CONTROL_TIMEOUT_MS));
|
||||
return {
|
||||
enabled,
|
||||
command,
|
||||
args,
|
||||
cwd,
|
||||
timeoutMs,
|
||||
};
|
||||
}
|
||||
|
||||
export function canHandleBrowserControlTask(task) {
|
||||
return String(task?.taskType || "").trim() === "browser_control";
|
||||
}
|
||||
|
||||
export function buildBrowserControlTaskExecution(config, task) {
|
||||
if (!config?.enabled) {
|
||||
throw new Error("BROWSER_CONTROL_RUNTIME_DISABLED");
|
||||
}
|
||||
if (!config?.command) {
|
||||
throw new Error("BROWSER_CONTROL_COMMAND_REQUIRED");
|
||||
}
|
||||
|
||||
const cwd = config.cwd || process.cwd();
|
||||
return {
|
||||
command: config.command,
|
||||
args: resolveCommandArgs(config.command, config.args || [], cwd),
|
||||
cwd,
|
||||
timeoutMs: config.timeoutMs || 45000,
|
||||
stdinPayload: {
|
||||
requestKind: "browser_control",
|
||||
requestId: String(task?.taskId || "").trim(),
|
||||
objective: String(task?.requestText || task?.executionPrompt || "").trim(),
|
||||
context: {
|
||||
projectId: String(task?.projectId || "").trim() || undefined,
|
||||
threadId: String(task?.threadId || task?.targetThreadId || "").trim() || undefined,
|
||||
requestedBy: String(task?.requestedByAccount || task?.requestedBy || "").trim() || undefined,
|
||||
requestedAt: String(task?.requestedAt || "").trim() || undefined,
|
||||
confirmationScopeKey: String(task?.confirmationScopeKey || "").trim() || undefined,
|
||||
riskLevel: String(task?.riskLevel || "").trim() || undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function parseBrowserControlTaskResult(rawOutput) {
|
||||
const parsed = parseJsonLine(rawOutput);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new Error("INVALID_BROWSER_CONTROL_RUNTIME_PAYLOAD");
|
||||
}
|
||||
|
||||
if (parsed.status === "failed") {
|
||||
return {
|
||||
status: "failed",
|
||||
requestId: typeof parsed.requestId === "string" ? parsed.requestId.trim() || undefined : undefined,
|
||||
errorMessage:
|
||||
typeof parsed.error === "string" && parsed.error.trim()
|
||||
? parsed.error.trim()
|
||||
: "BROWSER_CONTROL_FAILED",
|
||||
};
|
||||
}
|
||||
|
||||
const replyBody =
|
||||
typeof parsed.replyBody === "string" && parsed.replyBody.trim()
|
||||
? parsed.replyBody.trim()
|
||||
: typeof parsed.summary === "string" && parsed.summary.trim()
|
||||
? parsed.summary.trim()
|
||||
: "";
|
||||
if (!replyBody) {
|
||||
throw new Error("INVALID_BROWSER_CONTROL_RUNTIME_PAYLOAD");
|
||||
}
|
||||
|
||||
return {
|
||||
status: "completed",
|
||||
requestId: typeof parsed.requestId === "string" ? parsed.requestId.trim() || undefined : undefined,
|
||||
replyBody,
|
||||
targetUrl:
|
||||
typeof parsed.targetUrl === "string" && parsed.targetUrl.trim()
|
||||
? parsed.targetUrl.trim()
|
||||
: undefined,
|
||||
executionSummary:
|
||||
typeof parsed.executionSummary === "string" && parsed.executionSummary.trim()
|
||||
? parsed.executionSummary.trim()
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function executeBrowserControlTask(task, config = {}) {
|
||||
const runnerConfig = getBrowserControlTaskRunnerConfig(process.env, config);
|
||||
if (!runnerConfig.enabled) {
|
||||
return {
|
||||
status: "failed",
|
||||
errorMessage: "BROWSER_CONTROL_RUNTIME_DISABLED",
|
||||
};
|
||||
}
|
||||
|
||||
const execution = buildBrowserControlTaskExecution(runnerConfig, task);
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(execution.command, execution.args, {
|
||||
cwd: execution.cwd,
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let timedOut = false;
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill("SIGKILL");
|
||||
}, execution.timeoutMs);
|
||||
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timer);
|
||||
if (timedOut) {
|
||||
reject(new Error("BROWSER_CONTROL_TIMEOUT"));
|
||||
return;
|
||||
}
|
||||
if (code !== 0) {
|
||||
reject(new Error(stderr.trim() || `browser control exit code ${code}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(parseBrowserControlTaskResult(stdout));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
child.stdin.write(JSON.stringify(execution.stdinPayload));
|
||||
child.stdin.end();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user