feat: ship enterprise control and desktop governance
This commit is contained in:
253
scripts/browser-control-smoke.mjs
Normal file
253
scripts/browser-control-smoke.mjs
Normal file
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
|
||||
function writeJson(payload) {
|
||||
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
||||
}
|
||||
|
||||
async function readStdin() {
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
||||
}
|
||||
return chunks.join("").trim();
|
||||
}
|
||||
|
||||
function normalizePayload(raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "INVALID_BROWSER_CONTROL_PAYLOAD: expected object",
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
payload: parsed,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
error: "INVALID_BROWSER_CONTROL_PAYLOAD: invalid json",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function extractTargetUrl(objective) {
|
||||
const match = String(objective || "").match(/https?:\/\/[^\s,。;、))]+/i);
|
||||
return match?.[0] || undefined;
|
||||
}
|
||||
|
||||
async function writeArtifact(payload) {
|
||||
const artifactDir = String(process.env.BOSS_CONTROL_ARTIFACT_DIR || "").trim();
|
||||
if (!artifactDir) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const requestId =
|
||||
typeof payload.requestId === "string" && payload.requestId.trim()
|
||||
? payload.requestId.trim()
|
||||
: `browser-${Date.now()}`;
|
||||
const artifactPath = path.join(artifactDir, `${requestId}.json`);
|
||||
await writeFile(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||||
return [
|
||||
{
|
||||
kind: "json",
|
||||
path: artifactPath,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function parseArgs(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseArgsJson(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.map((item) => String(item)).filter(Boolean) : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getBrowserAutomationMode() {
|
||||
const raw = String(process.env.BOSS_BROWSER_AUTOMATION_MODE || "").trim().toLowerCase();
|
||||
if (raw === "off" || raw === "fetch" || raw === "playwright" || raw === "auto") {
|
||||
return raw;
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
function resolveCodexHome() {
|
||||
return String(process.env.CODEX_HOME || "").trim() || path.join(process.env.HOME || "", ".codex");
|
||||
}
|
||||
|
||||
function resolveBundledPlaywrightCommand() {
|
||||
const wrapper = path.join(resolveCodexHome(), "skills", "playwright", "scripts", "playwright_cli.sh");
|
||||
return existsSync(wrapper) ? wrapper : undefined;
|
||||
}
|
||||
|
||||
function resolveBrowserAutomationArgs(command, commandArgs, targetUrl, requestId) {
|
||||
const args = [...commandArgs];
|
||||
const session =
|
||||
String(process.env.BOSS_BROWSER_AUTOMATION_SESSION || "").trim() ||
|
||||
String(requestId || "").trim();
|
||||
if (session) {
|
||||
args.push("--session", session);
|
||||
}
|
||||
args.push(command, targetUrl);
|
||||
return args;
|
||||
}
|
||||
|
||||
async function runCommand(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
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", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(stderr.trim() || `browser open exit code ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runBrowserAutomation(targetUrl, requestId) {
|
||||
const command =
|
||||
String(process.env.BOSS_BROWSER_AUTOMATION_COMMAND || "").trim() || resolveBundledPlaywrightCommand();
|
||||
if (!command || !targetUrl) {
|
||||
return undefined;
|
||||
}
|
||||
const prefixArgs =
|
||||
parseArgsJson(process.env.BOSS_BROWSER_AUTOMATION_ARGS_JSON) ??
|
||||
parseArgs(process.env.BOSS_BROWSER_AUTOMATION_ARGS);
|
||||
await runCommand(command, resolveBrowserAutomationArgs("open", prefixArgs, targetUrl, requestId));
|
||||
const title = await runCommand(
|
||||
command,
|
||||
resolveBrowserAutomationArgs("eval", prefixArgs, "document.title", requestId),
|
||||
);
|
||||
return title.stdout || undefined;
|
||||
}
|
||||
|
||||
async function inspectPageTitle(targetUrl) {
|
||||
if (!targetUrl) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
redirect: "follow",
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
if (!contentType.includes("text/html")) {
|
||||
return undefined;
|
||||
}
|
||||
const html = await response.text();
|
||||
const title = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]?.replace(/\s+/g, " ").trim();
|
||||
return title || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const raw = await readStdin();
|
||||
const normalized = normalizePayload(raw);
|
||||
|
||||
if (!normalized.ok) {
|
||||
writeJson({
|
||||
status: "failed",
|
||||
error: normalized.error,
|
||||
});
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const payload = normalized.payload;
|
||||
const currentRequestId = typeof payload.requestId === "string" ? payload.requestId.trim() : "";
|
||||
const objective =
|
||||
typeof payload.objective === "string" && payload.objective.trim()
|
||||
? payload.objective.trim()
|
||||
: "浏览器控制 smoke 链路正常";
|
||||
const targetUrl = extractTargetUrl(objective);
|
||||
const riskLevel =
|
||||
typeof payload.context?.riskLevel === "string" && payload.context.riskLevel.trim()
|
||||
? payload.context.riskLevel.trim()
|
||||
: "unknown";
|
||||
const dryRun = payload.context?.dryRun === true;
|
||||
let action = targetUrl ? "open_url" : "browser_smoke";
|
||||
const configuredAutomationMode = getBrowserAutomationMode();
|
||||
const automationMode =
|
||||
configuredAutomationMode === "auto"
|
||||
? String(process.env.BOSS_BROWSER_AUTOMATION_COMMAND || "").trim() || resolveBundledPlaywrightCommand()
|
||||
? "playwright"
|
||||
: "fetch"
|
||||
: configuredAutomationMode;
|
||||
const automatedTitle =
|
||||
targetUrl && !dryRun && automationMode === "playwright"
|
||||
? await runBrowserAutomation(targetUrl, currentRequestId)
|
||||
: undefined;
|
||||
const pageTitle =
|
||||
automatedTitle ||
|
||||
(automationMode !== "off" ? await inspectPageTitle(targetUrl) : undefined);
|
||||
if (targetUrl && !dryRun) {
|
||||
if (automationMode === "playwright" && automatedTitle) {
|
||||
action = "browser_automation_executed";
|
||||
} else {
|
||||
const command = String(process.env.BOSS_BROWSER_OPEN_COMMAND || "").trim() || "open";
|
||||
const prefixArgs =
|
||||
parseArgsJson(process.env.BOSS_BROWSER_OPEN_ARGS_JSON) ??
|
||||
parseArgs(process.env.BOSS_BROWSER_OPEN_ARGS);
|
||||
await runCommand(command, [...prefixArgs, targetUrl]);
|
||||
action = "open_url_executed";
|
||||
}
|
||||
}
|
||||
const artifacts = await writeArtifact({
|
||||
requestKind: payload.requestKind,
|
||||
requestId: payload.requestId,
|
||||
action,
|
||||
objective,
|
||||
targetUrl,
|
||||
dryRun,
|
||||
riskLevel,
|
||||
capturedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
writeJson({
|
||||
status: "completed",
|
||||
requestId: typeof payload.requestId === "string" ? payload.requestId : undefined,
|
||||
replyBody: pageTitle
|
||||
? `浏览器控制已完成:${objective}。页面标题:${pageTitle}`
|
||||
: `浏览器控制已完成:${objective}`,
|
||||
executionSummary: pageTitle
|
||||
? `${action} completed (risk=${riskLevel}, title=${pageTitle})`
|
||||
: `${action} completed (risk=${riskLevel})`,
|
||||
targetUrl,
|
||||
artifacts,
|
||||
});
|
||||
Reference in New Issue
Block a user