feat: harden enterprise control plane

This commit is contained in:
AI Bot
2026-05-17 02:20:08 +08:00
parent 67511c31f4
commit e1aed590f8
112 changed files with 10977 additions and 2004 deletions

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { Client } from "pg";
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
@@ -7,6 +7,8 @@ import process from "node:process";
const snapshotKey = process.env.BOSS_STATE_POSTGRES_KEY?.trim() || "default";
const defaultStateFile = process.env.BOSS_STATE_FILE || path.join(process.cwd(), "data", "boss-state.json");
const defaultBackupDir = process.env.BOSS_STATE_BACKUP_DIR || path.join(path.dirname(defaultStateFile), "backups");
const defaultSchemaFile = path.join(process.cwd(), "scripts", "postgres-state-schema.sql");
const postgresTable = "boss_state_snapshots";
function usage() {
return [
@@ -14,13 +16,16 @@ function usage() {
"",
"Commands:",
" describe",
" validate-schema [--schema <file>]",
" backup-file --input <file> [--output <file>] [--dry-run]",
" export-file --input <file> --output <file> [--dry-run]",
" migrate-file-to-postgres --input <file> [--dry-run]",
" export-postgres-backup --output <file> [--dry-run]",
" restore-postgres-backup --input <file> [--dry-run]",
" rollback-postgres-to-file --output <file> [--dry-run]",
"",
"Environment:",
" BOSS_STATE_FILE, BOSS_DATABASE_URL, BOSS_STATE_POSTGRES_KEY, BOSS_STATE_BACKUP_DIR",
" BOSS_STATE_FILE, BOSS_STATE_STORE, BOSS_DATABASE_URL, BOSS_STATE_POSTGRES_KEY, BOSS_STATE_BACKUP_DIR",
].join("\n");
}
@@ -43,6 +48,11 @@ function parseArgs(argv) {
index += 1;
continue;
}
if (item === "--schema") {
options.schema = items[index + 1];
index += 1;
continue;
}
throw new Error(`UNKNOWN_OPTION:${item}`);
}
return options;
@@ -56,6 +66,30 @@ function timestampSegment() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function sha256(text) {
return createHash("sha256").update(text).digest("hex");
}
function postgresModeEnabled() {
return process.env.BOSS_STATE_STORE?.trim().toLowerCase() === "postgres";
}
function postgresConfigured() {
return Boolean(process.env.BOSS_DATABASE_URL?.trim());
}
function requirePostgresMode() {
if (!postgresModeEnabled()) {
throw new Error("BOSS_STATE_STORE_POSTGRES_REQUIRED");
}
}
function requirePostgresDatabaseUrl() {
if (!postgresConfigured()) {
throw new Error("BOSS_DATABASE_URL_REQUIRED");
}
}
function validateStateText(text, source) {
const parsed = JSON.parse(text);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -70,6 +104,37 @@ async function readStateText(filePath) {
return text;
}
function validatePostgresSchemaText(text, source) {
const compact = text.replace(/\s+/g, " ").toLowerCase();
const required = [
[/create table if not exists boss_state_snapshots/, "table"],
[/snapshot_key\s+text\s+primary key/, "snapshot_key_primary_key"],
[/state\s+jsonb\s+not null/, "state_jsonb"],
[/created_at\s+timestamptz\s+not null\s+default now\(\)/, "created_at"],
[/updated_at\s+timestamptz\s+not null\s+default now\(\)/, "updated_at"],
[/create index if not exists boss_state_snapshots_updated_at_idx/, "updated_at_index"],
];
const missing = required.filter(([pattern]) => !pattern.test(compact)).map(([, name]) => name);
if (missing.length > 0) {
throw new Error(`POSTGRES_SCHEMA_INVALID:${source}:${missing.join(",")}`);
}
return {
ok: true,
source,
table: postgresTable,
sha256: sha256(text),
};
}
async function validatePostgresSchema(options) {
const schema = path.resolve(options.schema || defaultSchemaFile);
const text = await fs.readFile(schema, "utf8");
return {
action: "validate-schema",
...validatePostgresSchemaText(text, schema),
};
}
async function ensurePostgresSchema(client) {
await client.query(`
CREATE TABLE IF NOT EXISTS boss_state_snapshots (
@@ -79,13 +144,17 @@ async function ensurePostgresSchema(client) {
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`);
await client.query(`
CREATE INDEX IF NOT EXISTS boss_state_snapshots_updated_at_idx
ON boss_state_snapshots (updated_at DESC)
`);
}
async function withPostgres(handler) {
const connectionString = process.env.BOSS_DATABASE_URL?.trim();
if (!connectionString) {
throw new Error("BOSS_DATABASE_URL_REQUIRED");
}
requirePostgresMode();
requirePostgresDatabaseUrl();
const connectionString = process.env.BOSS_DATABASE_URL.trim();
const { Client } = await import("pg");
const client = new Client({ connectionString });
await client.connect();
try {
@@ -135,8 +204,11 @@ async function exportFile(options) {
}
async function migrateFileToPostgres(options) {
requirePostgresMode();
requirePostgresDatabaseUrl();
const source = path.resolve(options.input || defaultStateFile);
const text = await readStateText(source);
const schema = await validatePostgresSchema({ schema: options.schema });
if (!options.dryRun) {
await withPostgres(async (client) => {
await ensurePostgresSchema(client);
@@ -157,11 +229,18 @@ async function migrateFileToPostgres(options) {
dryRun: options.dryRun,
source,
snapshotKey,
postgresConfigured: postgresConfigured(),
wouldConnect: !options.dryRun,
schemaValid: schema.ok,
schemaSha256: schema.sha256,
bytes: Buffer.byteLength(text),
stateSha256: sha256(text),
};
}
async function rollbackPostgresToFile(options) {
requirePostgresMode();
requirePostgresDatabaseUrl();
const output = path.resolve(options.output || defaultStateFile);
if (options.dryRun) {
return {
@@ -170,6 +249,8 @@ async function rollbackPostgresToFile(options) {
dryRun: true,
output,
snapshotKey,
postgresConfigured: postgresConfigured(),
wouldConnect: false,
};
}
const text = await withPostgres(async (client) => {
@@ -190,6 +271,115 @@ async function rollbackPostgresToFile(options) {
output,
snapshotKey,
bytes: Buffer.byteLength(text),
stateSha256: sha256(text),
};
}
function normalizeBackupPayload(text, source) {
const parsed = validateStateText(text, source);
if (parsed.metadata?.format === "boss-state-postgres-backup/v1" && parsed.state) {
return {
metadata: parsed.metadata,
state: parsed.state,
stateText: JSON.stringify(parsed.state, null, 2),
bundled: true,
};
}
return {
metadata: null,
state: parsed,
stateText: JSON.stringify(parsed, null, 2),
bundled: false,
};
}
async function exportPostgresBackup(options) {
requirePostgresMode();
const output = path.resolve(options.output || path.join(defaultBackupDir, `boss-postgres-state-${timestampSegment()}.json`));
if (options.dryRun) {
requirePostgresDatabaseUrl();
return {
ok: true,
action: "export-postgres-backup",
dryRun: true,
output,
snapshotKey,
postgresConfigured: true,
wouldConnect: false,
};
}
const stateText = await withPostgres(async (client) => {
await ensurePostgresSchema(client);
const result = await client.query("SELECT state FROM boss_state_snapshots WHERE snapshot_key = $1", [snapshotKey]);
const state = result.rows[0]?.state;
if (!state) {
throw new Error("BOSS_POSTGRES_STATE_NOT_FOUND");
}
return JSON.stringify(state, null, 2);
});
const state = validateStateText(stateText, `${postgresTable}:${snapshotKey}`);
const backup = {
metadata: {
format: "boss-state-postgres-backup/v1",
exportedAt: new Date().toISOString(),
snapshotKey,
table: postgresTable,
stateSha256: sha256(stateText),
stateBytes: Buffer.byteLength(stateText),
},
state,
};
const backupText = `${JSON.stringify(backup, null, 2)}\n`;
await fs.mkdir(path.dirname(output), { recursive: true });
await fs.writeFile(output, backupText, "utf8");
return {
ok: true,
action: "export-postgres-backup",
dryRun: false,
output,
snapshotKey,
bytes: Buffer.byteLength(backupText),
stateSha256: backup.metadata.stateSha256,
};
}
async function restorePostgresBackup(options) {
requirePostgresMode();
requirePostgresDatabaseUrl();
if (!options.input) {
throw new Error("INPUT_REQUIRED");
}
const source = path.resolve(options.input);
const text = await fs.readFile(source, "utf8");
const backup = normalizeBackupPayload(text, source);
validateStateText(backup.stateText, source);
if (!options.dryRun) {
await withPostgres(async (client) => {
await ensurePostgresSchema(client);
await client.query(
`
INSERT INTO boss_state_snapshots (snapshot_key, state, updated_at)
VALUES ($1, $2::jsonb, now())
ON CONFLICT (snapshot_key)
DO UPDATE SET state = EXCLUDED.state, updated_at = now()
`,
[snapshotKey, backup.stateText],
);
});
}
return {
ok: true,
action: "restore-postgres-backup",
dryRun: options.dryRun,
source,
snapshotKey,
postgresConfigured: postgresConfigured(),
wouldConnect: !options.dryRun,
bundled: backup.bundled,
bytes: Buffer.byteLength(backup.stateText),
stateSha256: sha256(backup.stateText),
};
}
@@ -200,14 +390,17 @@ async function main() {
jsonOut({
ok: true,
action: "describe",
mode: process.env.BOSS_STATE_STORE === "postgres" ? "postgres" : "file",
mode: postgresModeEnabled() ? "postgres" : "file",
stateFile: path.resolve(defaultStateFile),
backupDir: path.resolve(defaultBackupDir),
postgresConfigured: Boolean(process.env.BOSS_DATABASE_URL?.trim()),
postgresTable: "boss_state_snapshots",
postgresConfigured: postgresConfigured(),
postgresTable,
snapshotKey,
});
return;
case "validate-schema":
jsonOut(await validatePostgresSchema(options));
return;
case "backup-file":
jsonOut(await backupFile(options));
return;
@@ -217,6 +410,12 @@ async function main() {
case "migrate-file-to-postgres":
jsonOut(await migrateFileToPostgres(options));
return;
case "export-postgres-backup":
jsonOut(await exportPostgresBackup(options));
return;
case "restore-postgres-backup":
jsonOut(await restorePostgresBackup(options));
return;
case "rollback-postgres-to-file":
jsonOut(await rollbackPostgresToFile(options));
return;

View File

@@ -39,8 +39,53 @@ function normalizePayload(raw) {
}
function extractTargetUrl(objective) {
const match = String(objective || "").match(/https?:\/\/[^\s、)]+/i);
return match?.[0] || undefined;
const text = String(objective || "");
const fullUrl = text.match(/https?:\/\/[^\s、)]+/i)?.[0];
if (fullUrl) {
return fullUrl;
}
const bareDomain = text.match(
/(?:访问|打开|进入|看一下|visit|open)?\s*((?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?:\/[^\s、)]*)?)/i,
)?.[1];
return bareDomain ? `https://${bareDomain}` : undefined;
}
function cleanupSearchQuery(value) {
return String(value || "")
.replace(/打开\s*(?:youtube|油管)/gi, " ")
.replace(/(?:youtube|油管)/gi, " ")
.replace(/用浏览器打开/gi, " ")
.replace(/打开浏览器/gi, " ")
.replace(/找一个|找一下|搜索|搜一下|搜|播放/gi, " ")
.replace(/的\s*mv/gi, " MV")
.replace(/mv/gi, " MV")
.replace(/[,。;;、]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function deriveYouTubeSearchUrl(objective) {
const text = String(objective || "").trim();
if (!/(youtube|油管)/i.test(text)) {
return undefined;
}
const queryPatterns = [
/(?:找一个|找一下|搜索|搜一下|搜|播放)\s*([^,。;;]+)/i,
/(?:youtube|油管)\s*([^,。;;]+)/i,
];
for (const pattern of queryPatterns) {
const query = cleanupSearchQuery(text.match(pattern)?.[1]);
if (query) {
return `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`;
}
}
const fallbackQuery = cleanupSearchQuery(text);
return fallbackQuery
? `https://www.youtube.com/results?search_query=${encodeURIComponent(fallbackQuery)}`
: "https://www.youtube.com";
}
async function writeArtifact(payload) {
@@ -84,6 +129,55 @@ function parseArgsJson(value) {
}
}
function detectRequestedBrowserApp(objective) {
const text = String(objective || "").toLowerCase();
if (text.includes("chrome") || text.includes("谷歌浏览器")) {
return "Google Chrome";
}
if (text.includes("safari")) {
return "Safari";
}
return undefined;
}
function resolveBrowserOpenArgs(command, objective) {
const configured =
parseArgsJson(process.env.BOSS_BROWSER_OPEN_ARGS_JSON) ??
parseArgs(process.env.BOSS_BROWSER_OPEN_ARGS);
if (configured.length > 0) {
return configured;
}
const requestedApp = detectRequestedBrowserApp(objective);
const commandName = path.basename(command || "").toLowerCase();
return requestedApp && commandName === "open" ? ["-a", requestedApp] : [];
}
function escapeAppleScriptString(value) {
return String(value || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
async function openTargetUrl(targetUrl, objective) {
const commandOverride = String(process.env.BOSS_BROWSER_OPEN_COMMAND || "").trim();
const command = commandOverride || "open";
const requestedApp = detectRequestedBrowserApp(objective);
if (!commandOverride && process.platform === "darwin" && requestedApp) {
const app = escapeAppleScriptString(requestedApp);
const url = escapeAppleScriptString(targetUrl);
const script = [
`tell application "${app}" to activate`,
`tell application "${app}" to open location "${url}"`,
].join("\n");
await runCommand("osascript", ["-e", script]);
return "osascript_open_url_executed";
}
const prefixArgs = resolveBrowserOpenArgs(command, objective);
await runCommand(command, [...prefixArgs, targetUrl]);
return "open_url_executed";
}
function getBrowserAutomationMode() {
const raw = String(process.env.BOSS_BROWSER_AUTOMATION_MODE || "").trim().toLowerCase();
if (raw === "off" || raw === "fetch" || raw === "playwright" || raw === "auto") {
@@ -92,6 +186,11 @@ function getBrowserAutomationMode() {
return "auto";
}
function shouldOpenVisibleBrowserAfterAutomation() {
const raw = String(process.env.BOSS_BROWSER_VISIBLE_OPEN_AFTER_AUTOMATION || "").trim().toLowerCase();
return !["0", "false", "off", "no"].includes(raw);
}
function resolveCodexHome() {
return String(process.env.CODEX_HOME || "").trim() || path.join(process.env.HOME || "", ".codex");
}
@@ -195,7 +294,7 @@ const objective =
typeof payload.objective === "string" && payload.objective.trim()
? payload.objective.trim()
: "浏览器控制 smoke 链路正常";
const targetUrl = extractTargetUrl(objective);
const targetUrl = extractTargetUrl(objective) || deriveYouTubeSearchUrl(objective);
const riskLevel =
typeof payload.context?.riskLevel === "string" && payload.context.riskLevel.trim()
? payload.context.riskLevel.trim()
@@ -209,23 +308,27 @@ const automationMode =
? "playwright"
: "fetch"
: configuredAutomationMode;
const automatedTitle =
targetUrl && !dryRun && automationMode === "playwright"
? await runBrowserAutomation(targetUrl, currentRequestId)
: undefined;
let automationError;
let automatedTitle;
if (targetUrl && !dryRun && automationMode === "playwright") {
try {
automatedTitle = await runBrowserAutomation(targetUrl, currentRequestId);
} catch (error) {
automationError = error instanceof Error ? error.message : String(error);
}
}
const pageTitle =
automatedTitle ||
(automationMode !== "off" ? await inspectPageTitle(targetUrl) : undefined);
if (targetUrl && !dryRun) {
if (automationMode === "playwright" && automatedTitle) {
action = "browser_automation_executed";
if (shouldOpenVisibleBrowserAfterAutomation()) {
const visibleAction = await openTargetUrl(targetUrl, objective);
action = `${action}+${visibleAction}`;
}
} 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";
action = await openTargetUrl(targetUrl, objective);
}
}
const artifacts = await writeArtifact({
@@ -246,8 +349,8 @@ writeJson({
? `浏览器控制已完成:${objective}。页面标题:${pageTitle}`
: `浏览器控制已完成:${objective}`,
executionSummary: pageTitle
? `${action} completed (risk=${riskLevel}, title=${pageTitle})`
: `${action} completed (risk=${riskLevel})`,
? `${action} completed (risk=${riskLevel}, title=${pageTitle}${automationError ? ", automationFallback=true" : ""})`
: `${action} completed (risk=${riskLevel}${automationError ? ", automationFallback=true" : ""})`,
targetUrl,
artifacts,
});

55
scripts/build-boss-agent-mac-app.sh Normal file → Executable file
View File

@@ -11,6 +11,11 @@ BINARY_PATH="$MACOS_DIR/boss-agent"
ICONSET_DIR="$RESOURCES_DIR/BossAgent.iconset"
ICON_PATH="$RESOURCES_DIR/BossAgent.icns"
SIGNING_IDENTITY="${BOSS_AGENT_CODESIGN_IDENTITY:-}"
NOTARIZE="${BOSS_AGENT_NOTARIZE:-0}"
NOTARY_PROFILE="${BOSS_AGENT_NOTARY_PROFILE:-}"
NOTARY_APPLE_ID="${BOSS_AGENT_NOTARY_APPLE_ID:-}"
NOTARY_TEAM_ID="${BOSS_AGENT_NOTARY_TEAM_ID:-}"
NOTARY_PASSWORD="${BOSS_AGENT_NOTARY_PASSWORD:-}"
if ! command -v swiftc >/dev/null 2>&1; then
echo "swiftc not found. Install Xcode Command Line Tools first." >&2
@@ -23,13 +28,24 @@ if ! command -v iconutil >/dev/null 2>&1; then
fi
if [[ -z "$SIGNING_IDENTITY" ]] && command -v security >/dev/null 2>&1; then
SIGNING_IDENTITY="$(
security find-identity -v -p codesigning 2>/dev/null \
| awk -F'"' '/"Apple Development:|Developer ID Application:|Mac Developer:|Boss Agent/ { print $2; exit }'
)"
if [[ "$NOTARIZE" == "1" ]]; then
SIGNING_IDENTITY="$(
security find-identity -v -p codesigning 2>/dev/null \
| awk -F'"' '/"Developer ID Application:/ { print $2; exit }'
)"
else
SIGNING_IDENTITY="$(
security find-identity -v -p codesigning 2>/dev/null \
| awk -F'"' '/"Apple Development:|Developer ID Application:|Mac Developer:|Boss Agent/ { print $2; exit }'
)"
fi
fi
if [[ -z "$SIGNING_IDENTITY" ]]; then
if [[ "$NOTARIZE" == "1" ]]; then
echo "boss-agent: BOSS_AGENT_NOTARIZE=1 requires a Developer ID Application signing identity." >&2
exit 1
fi
SIGNING_IDENTITY="-"
echo "boss-agent: no stable code signing identity found; falling back to ad-hoc signing." >&2
else
@@ -172,5 +188,34 @@ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
PLIST
plutil -lint "$CONTENTS_DIR/Info.plist" >/dev/null
codesign --force --deep --timestamp=none --sign "$SIGNING_IDENTITY" "$APP_DIR" >/dev/null
if [[ "$NOTARIZE" == "1" ]]; then
if ! command -v xcrun >/dev/null 2>&1; then
echo "boss-agent: xcrun is required for notarization." >&2
exit 1
fi
codesign --force --deep --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$APP_DIR" >/dev/null
NOTARY_ZIP="$ROOT_DIR/dist/boss-agent-notary.zip"
rm -f "$NOTARY_ZIP"
(
cd "$ROOT_DIR/dist"
ditto -c -k --keepParent "boss-agent.app" "$NOTARY_ZIP"
)
NOTARY_ARGS=()
if [[ -n "$NOTARY_PROFILE" ]]; then
NOTARY_ARGS=(--keychain-profile "$NOTARY_PROFILE")
elif [[ -n "$NOTARY_APPLE_ID" && -n "$NOTARY_TEAM_ID" && -n "$NOTARY_PASSWORD" ]]; then
NOTARY_ARGS=(--apple-id "$NOTARY_APPLE_ID" --team-id "$NOTARY_TEAM_ID" --password "$NOTARY_PASSWORD")
else
echo "boss-agent: notarization requires BOSS_AGENT_NOTARY_PROFILE or Apple ID/team/password env vars." >&2
exit 1
fi
xcrun notarytool submit "$NOTARY_ZIP" "${NOTARY_ARGS[@]}" --wait >/dev/null
xcrun stapler staple "$APP_DIR" >/dev/null
rm -f "$NOTARY_ZIP"
else
codesign --force --deep --timestamp=none --sign "$SIGNING_IDENTITY" "$APP_DIR" >/dev/null
fi
echo "$APP_DIR"

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env node
import { fileURLToPath } from "node:url";
import path from "node:path";
import {
executeCodexAppServerTask,
getCodexAppServerRunnerConfig,
} from "../local-agent/codex-app-server-runner.mjs";
const DEFAULT_TIMEOUT_MS = 120_000;
function normalizeText(value) {
return String(value || "").trim();
}
function parseArgs(value) {
const args = String(value || "")
.trim()
.split(/\s+/)
.filter(Boolean);
return args.length > 0 ? args : undefined;
}
function parseArgsJson(value) {
const raw = normalizeText(value);
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 parseTimeoutMs(value) {
const parsed = Number.parseInt(String(value || ""), 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
}
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 parseJsonPayload(raw) {
try {
const parsed = JSON.parse(String(raw || "{}"));
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
} catch {
return {};
}
}
function detectTargetApp(objective) {
const text = normalizeText(objective).toLowerCase();
const candidates = [
["Codex", ["codex"]],
["Google Chrome", ["chrome", "google chrome", "谷歌"]],
["Safari", ["safari"]],
["QQ", ["qq"]],
["微信", ["微信", "wechat"]],
["飞书", ["飞书", "lark", "feishu"]],
["Telegram", ["telegram", "tg"]],
["Finder", ["finder", "访达"]],
["系统设置", ["系统设置", "system settings", "settings"]],
];
const matched = candidates.find(([, aliases]) => aliases.some((alias) => text.includes(alias)));
return matched?.[0];
}
function buildComputerUsePrompt(payload) {
const objective = normalizeText(payload.objective);
const targetApp = detectTargetApp(objective);
return [
"你是 Boss 的 Codex Computer Use 执行器,正在被要求控制当前这台 macOS 电脑。",
"请优先使用 Codex 自带的 Computer Use / Browser / Desktop 能力完成用户目标。",
"只执行当前目标直接需要的动作;不要扩展需求,不要改动无关文件。",
"遇到发送、提交、删除、支付、授权等高风险动作时,必须停下来要求用户确认,不要静默点击。",
targetApp ? `目标应用:${targetApp}` : "目标应用:如果用户没有明确说明,请先根据目标判断最小必要应用。",
`用户目标:${objective}`,
"完成后用中文返回简短小结,说明已做的动作、结果和是否需要用户下一步确认。",
]
.filter(Boolean)
.join("\n");
}
function buildRunnerConfig(env, payload) {
const cwd =
normalizeText(env.BOSS_CODEX_COMPUTER_USE_WORKDIR) ||
normalizeText(payload?.context?.projectCwd) ||
process.cwd();
return getCodexAppServerRunnerConfig(env, {
codexAppServerEnabled: true,
codexAppServerCommand: normalizeText(env.BOSS_CODEX_COMPUTER_USE_CODEX_COMMAND) || "codex",
codexAppServerArgs:
parseArgsJson(env.BOSS_CODEX_COMPUTER_USE_CODEX_ARGS_JSON) ??
parseArgs(env.BOSS_CODEX_COMPUTER_USE_CODEX_ARGS) ??
["app-server"],
codexAppServerWorkdir: cwd,
codexAppServerTimeoutMs: parseTimeoutMs(env.BOSS_CODEX_COMPUTER_USE_TIMEOUT_MS),
codexAppServerClientName: "boss_codex_computer_use",
codexAppServerClientTitle: "Boss Codex Computer Use",
codexAppServerClientVersion: "0.1.0",
masterAgentWorkdir: cwd,
masterAgentModel: normalizeText(env.BOSS_CODEX_COMPUTER_USE_MODEL),
});
}
export async function runCodexComputerUseTask(payload, options = {}) {
const env = options.env || process.env;
const requestId = normalizeText(payload?.requestId);
const objective = normalizeText(payload?.objective);
if (!objective) {
return {
status: "failed",
requestId: requestId || undefined,
error: "CODEX_COMPUTER_USE_OBJECTIVE_REQUIRED",
computerUseProvider: "codex-computer-use",
};
}
const runnerConfig = buildRunnerConfig(env, payload);
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: requestId || "codex-computer-use",
taskType: "conversation_reply",
targetCodexThreadRef: normalizeText(payload?.context?.codexComputerUseThreadId),
targetCodexFolderRef: runnerConfig.cwd,
executionPrompt: buildComputerUsePrompt(payload),
});
if (result.status !== "completed") {
return {
status: "failed",
requestId: requestId || undefined,
error: result.errorMessage || "CODEX_COMPUTER_USE_FAILED",
detail: result.stderr,
computerUseProvider: "codex-computer-use",
};
}
return {
status: "completed",
requestId: requestId || undefined,
replyBody: result.replyBody || "Codex Computer Use 已完成本轮桌面控制任务。",
targetApp: detectTargetApp(objective),
executionSummary: "codex app-server computer use",
computerUseProvider: "codex-computer-use",
};
}
async function main() {
const raw = await readStdin();
const payload = parseJsonPayload(raw);
const result = await runCodexComputerUseTask(payload, {
env: process.env,
cwd: process.cwd(),
});
writeJson(result);
}
const currentFile = fileURLToPath(import.meta.url);
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(currentFile)) {
main().catch((error) => {
writeJson({
status: "failed",
error: error instanceof Error ? error.message : String(error),
computerUseProvider: "codex-computer-use",
});
process.exitCode = 1;
});
}

View File

@@ -64,6 +64,26 @@ function detectTargetApp(objective) {
return undefined;
}
function resolvePlatformAppName(targetApp) {
if (process.platform === "darwin" && targetApp === "Chrome") {
return "Google Chrome";
}
return targetApp;
}
function isBrowserApp(targetApp) {
return ["Chrome", "Google Chrome", "Safari"].includes(String(targetApp || ""));
}
function extractTargetUrl(objective) {
const text = String(objective || "");
const quotedText = extractQuotedText(text);
if (/^https?:\/\//i.test(String(quotedText || ""))) {
return quotedText;
}
return text.match(/https?:\/\/[^\s、)"”]+/i)?.[0];
}
function detectDesktopAction(objective) {
const text = String(objective || "").toLowerCase();
if (text.includes("系统设置") || text.includes("settings")) {
@@ -342,6 +362,25 @@ async function runAppleScript(targetApp, objective) {
return `osascript activated ${targetApp}`;
}
async function runOpenApp(targetApp) {
const command = String(process.env.BOSS_COMPUTER_USE_OPEN_APP_COMMAND || "").trim() || "open";
const prefixArgs = resolveOpenAppPrefixArgs(command);
await runCommand(command, [...prefixArgs, targetApp]);
return `open activated ${targetApp}`;
}
async function runOpenBrowserUrl(targetApp, targetUrl) {
const command = String(process.env.BOSS_COMPUTER_USE_OPEN_APP_COMMAND || "").trim() || "open";
const prefixArgs = resolveOpenAppPrefixArgs(command);
const commandName = path.basename(command || "").toLowerCase();
const args =
commandName === "open" || prefixArgs.includes("-a")
? [...prefixArgs, targetApp, targetUrl]
: [...prefixArgs, targetUrl];
await runCommand(command, args);
return `open url in ${targetApp}`;
}
const raw = await readStdin();
const normalized = normalizePayload(raw);
@@ -359,6 +398,8 @@ const objective =
? payload.objective.trim()
: "桌面控制 smoke 链路正常";
const targetApp = detectTargetApp(objective);
const automationTargetApp = resolvePlatformAppName(targetApp);
const targetUrl = extractTargetUrl(objective);
const desktopAction = detectDesktopAction(objective);
const riskLevel =
typeof payload.context?.riskLevel === "string" && payload.context.riskLevel.trim()
@@ -387,13 +428,14 @@ const configuredMode = getDesktopAutomationMode();
const automationMode =
configuredMode === "auto" ? (process.platform === "darwin" ? "osascript" : "open") : configuredMode;
if (targetApp && !dryRun) {
if (automationMode === "osascript") {
await runAppleScript(targetApp, objective);
if (targetUrl && isBrowserApp(automationTargetApp)) {
await runOpenBrowserUrl(automationTargetApp, targetUrl);
action = `${desktopAction}_url_executed`;
} else if (automationMode === "osascript") {
await runAppleScript(automationTargetApp, objective);
action = `${desktopAction}_executed`;
} else if (automationMode !== "off") {
const command = String(process.env.BOSS_COMPUTER_USE_OPEN_APP_COMMAND || "").trim() || "open";
const prefixArgs = resolveOpenAppPrefixArgs(command);
await runCommand(command, [...prefixArgs, targetApp]);
await runOpenApp(automationTargetApp);
action = `${desktopAction}_executed`;
}
}
@@ -404,6 +446,8 @@ const artifacts = await writeArtifact({
action,
objective,
targetApp,
automationTargetApp,
targetUrl,
typedText: extractQuotedText(objective),
dryRun,
riskLevel,
@@ -420,6 +464,8 @@ writeJson({
dialogGuardState.decision?.disposition ? `, dialogGuard=${dialogGuardState.decision.disposition}` : ""
})`,
targetApp,
automationTargetApp,
targetUrl,
typedText: extractQuotedText(objective),
dialogGuard: dialogGuardState.decision
? {

View File

@@ -0,0 +1,528 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { access } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import path from "node:path";
const DEFAULT_CUA_TIMEOUT_MS = 45000;
const TARGET_APPS = [
{
label: "Google Chrome",
name: "Google Chrome",
bundleId: "com.google.Chrome",
browser: true,
aliases: ["chrome", "google chrome", "谷歌浏览器", "谷歌"],
},
{
label: "Safari",
name: "Safari",
bundleId: "com.apple.Safari",
browser: true,
aliases: ["safari"],
},
{
label: "QQ",
name: "QQ",
aliases: ["qq"],
},
{
label: "微信",
name: "微信",
aliases: ["微信", "wechat"],
},
{
label: "飞书",
name: "飞书",
aliases: ["飞书", "lark", "feishu"],
},
{
label: "Telegram",
name: "Telegram",
aliases: ["telegram", "tg"],
},
{
label: "Finder",
name: "Finder",
bundleId: "com.apple.finder",
aliases: ["finder", "访达"],
},
{
label: "系统设置",
name: "System Settings",
bundleId: "com.apple.systempreferences",
aliases: ["系统设置", "system settings", "settings"],
},
{
label: "终端",
name: "Terminal",
bundleId: "com.apple.Terminal",
aliases: ["terminal", "终端"],
},
{
label: "Codex",
name: "Codex",
aliases: ["codex"],
},
];
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 parseJsonPayload(raw) {
try {
const parsed = JSON.parse(String(raw || "{}"));
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
} catch {
return {};
}
}
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 parseTimeoutMs(value) {
const parsed = Number.parseInt(String(value || ""), 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CUA_TIMEOUT_MS;
}
function normalizeText(value) {
return String(value || "").trim();
}
function normalizePlatform(value) {
const platform = normalizeText(value).toLowerCase();
return !platform || platform === "macos" || platform === "darwin" ? "macos" : platform;
}
function normalizeProvider(value) {
const provider = normalizeText(value);
return provider || "cua-driver-computer-use";
}
export function detectCuaTargetApp(objective) {
const text = normalizeText(objective).toLowerCase();
if (!text) return undefined;
return TARGET_APPS.find((candidate) =>
candidate.aliases.some((alias) => text.includes(alias.toLowerCase())),
);
}
function extractTargetUrl(objective) {
const text = normalizeText(objective);
return text.match(/https?:\/\/[^\s、)"”]+/i)?.[0];
}
function extractQuotedText(objective) {
const text = normalizeText(objective);
const patterns = [
/[“"]([^“”"]+)[”"]/,
/[「『]([^」』]+)[」』]/,
/输入[:]\s*([^\n。;]+)/,
/打字[:]\s*([^\n。;]+)/,
];
for (const pattern of patterns) {
const match = text.match(pattern);
const value = match?.[1]?.trim();
if (value) return value;
}
return undefined;
}
export function isSubmitLikeObjective(objective) {
const text = normalizeText(objective).toLowerCase();
return [
"发送",
"提交",
"发出去",
"回车发送",
"删除",
"购买",
"下单",
"支付",
"转账",
"send",
"submit",
"delete",
"purchase",
"pay",
].some((keyword) => text.includes(keyword));
}
function isSubmitAllowed(env, payload) {
if (String(env.BOSS_CUA_ALLOW_SUBMIT || "").trim().toLowerCase() === "true") {
return true;
}
return payload?.context?.desktopActionConfirmed === true || payload?.desktopActionConfirmed === true;
}
export function buildCuaLaunchArgs(targetApp, objective) {
if (!targetApp) return {};
const launchArgs = targetApp.bundleId ? { bundle_id: targetApp.bundleId } : { name: targetApp.name };
const url = extractTargetUrl(objective);
if (targetApp.browser) {
launchArgs.urls = [url || "about:blank"];
} else if (url) {
launchArgs.urls = [url];
}
return launchArgs;
}
function selectWindow(windows) {
const candidates = Array.isArray(windows) ? windows : [];
return (
candidates.find((window) => window?.is_on_screen === true && window?.on_current_space !== false) ||
candidates.find((window) => window?.on_current_space !== false) ||
candidates[0]
);
}
function getPid(launchResult) {
const value = launchResult?.structured?.pid ?? launchResult?.structured?.process_id ?? launchResult?.pid;
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}
function getWindowId(window) {
const value = window?.window_id ?? window?.id;
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}
function extractTextContent(parsed, raw) {
if (Array.isArray(parsed?.content)) {
return parsed.content
.map((item) => (typeof item?.text === "string" ? item.text.trim() : ""))
.filter(Boolean)
.join("\n")
.trim();
}
if (typeof parsed?.text === "string") return parsed.text.trim();
if (typeof raw === "string") return raw.trim();
return "";
}
function normalizeCuaToolOutput(rawOutput) {
const raw = String(rawOutput || "").trim();
if (!raw) {
return { raw: "", text: "", structured: {} };
}
try {
const parsed = JSON.parse(raw);
const structured =
parsed?.structuredContent && typeof parsed.structuredContent === "object"
? parsed.structuredContent
: parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed
: {};
return {
raw,
text: extractTextContent(parsed, raw),
structured,
isError: parsed?.isError === true,
};
} catch {
return {
raw,
text: raw,
structured: {},
isError: false,
};
}
}
function buildExecutableCandidates(command, env, cwd) {
const normalizedCommand = normalizeText(command);
if (!normalizedCommand) return [];
if (normalizedCommand.includes("/") || path.isAbsolute(normalizedCommand)) {
return [path.isAbsolute(normalizedCommand) ? normalizedCommand : path.resolve(cwd || process.cwd(), normalizedCommand)];
}
const pathCandidates = String(env.PATH || "")
.split(path.delimiter)
.filter(Boolean)
.map((item) => path.join(item, normalizedCommand));
const home = normalizeText(env.HOME);
return [
...pathCandidates,
home ? path.join(home, ".local", "bin", normalizedCommand) : undefined,
path.join("/usr/local/bin", normalizedCommand),
path.join("/opt/homebrew/bin", normalizedCommand),
normalizedCommand === "cua-driver" ? "/Applications/CuaDriver.app/Contents/MacOS/cua-driver" : undefined,
].filter(Boolean);
}
async function resolveExecutableCommand(command, env, cwd) {
for (const candidate of buildExecutableCandidates(command, env, cwd)) {
try {
await access(candidate);
return candidate;
} catch {
// Try the next well-known install location.
}
}
throw new Error("CUA_DRIVER_COMMAND_NOT_FOUND");
}
async function callCuaTool(toolName, args, options) {
const env = options.env || process.env;
const command = await resolveExecutableCommand(
normalizeText(env.BOSS_CUA_DRIVER_COMMAND) || "cua-driver",
env,
options.cwd || process.cwd(),
);
const prefixArgs = parseArgsJson(env.BOSS_CUA_DRIVER_ARGS_JSON) ?? parseArgs(env.BOSS_CUA_DRIVER_ARGS);
const timeoutMs = parseTimeoutMs(env.BOSS_CUA_DRIVER_TIMEOUT_MS);
const childArgs = [...prefixArgs, "call", toolName, JSON.stringify(args || {}), "--raw", "--compact"];
return new Promise((resolve, reject) => {
const child = spawn(command, childArgs, {
cwd: options.cwd || process.cwd(),
env: {
...process.env,
...env,
},
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
child.kill("SIGKILL");
}, 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);
if (error?.code === "ENOENT") {
reject(new Error("CUA_DRIVER_COMMAND_NOT_FOUND"));
return;
}
reject(error);
});
child.on("close", (code) => {
clearTimeout(timer);
if (timedOut) {
reject(new Error("CUA_DRIVER_TIMEOUT"));
return;
}
if (code !== 0) {
const detail = stderr.trim() || stdout.trim() || `cua-driver exit code ${code}`;
reject(new Error(`CUA_DRIVER_TOOL_FAILED: ${toolName}: ${detail}`));
return;
}
const result = normalizeCuaToolOutput(stdout);
if (result.isError) {
reject(new Error(`CUA_DRIVER_TOOL_ERROR: ${toolName}: ${result.text || result.raw}`));
return;
}
resolve(result);
});
});
}
function matchesTargetApp(app, targetApp) {
const bundleId = normalizeText(app?.bundle_id).toLowerCase();
const name = normalizeText(app?.name).toLowerCase();
const targetBundleId = normalizeText(targetApp?.bundleId).toLowerCase();
const targetName = normalizeText(targetApp?.name).toLowerCase();
const targetLabel = normalizeText(targetApp?.label).toLowerCase();
if (targetBundleId && bundleId === targetBundleId) return true;
if (targetName && name === targetName) return true;
if (targetLabel && name === targetLabel) return true;
return targetApp?.aliases?.some((alias) => name.includes(alias.toLowerCase())) === true;
}
function selectRunningApp(apps, targetApp) {
const candidates = Array.isArray(apps) ? apps : [];
return candidates.find((app) => app?.running === true && matchesTargetApp(app, targetApp));
}
async function resolveTargetAppSession(targetApp, objective, options, toolTrace) {
try {
const launchResult = await callCuaTool("launch_app", buildCuaLaunchArgs(targetApp, objective), options);
toolTrace.push("launch_app");
const pid = getPid(launchResult);
return {
pid,
window: selectWindow(launchResult.structured?.windows),
sourceText: launchResult.text || launchResult.raw,
};
} catch (error) {
toolTrace.push("launch_app_failed");
const appsResult = await callCuaTool("list_apps", {}, options);
toolTrace.push("list_apps");
const runningApp = selectRunningApp(appsResult.structured?.apps, targetApp);
const pid = getPid({ structured: runningApp });
if (!pid) {
throw error;
}
const windowsResult = await callCuaTool("list_windows", { pid }, options);
toolTrace.push("list_windows");
return {
pid,
window: selectWindow(windowsResult.structured?.windows),
sourceText: windowsResult.text || appsResult.text || error?.message,
};
}
}
function buildConfirmationResult(payload, targetApp) {
return {
status: "needs_user_action",
requestId: normalizeText(payload.requestId) || undefined,
kind: "desktop_submit_confirmation_required",
risk: "high",
summary: "这条指令会在桌面应用里发送、提交或删除内容,需要你先确认。",
recommendedAction: "allow_once",
availableActions: ["allow_once", "deny"],
platform: "macos",
appName: targetApp?.label || targetApp?.name,
};
}
function buildFailure(requestId, error, detail) {
return {
status: "failed",
requestId: normalizeText(requestId) || undefined,
error,
detail: normalizeText(detail) || undefined,
};
}
export async function runCuaDriverComputerUseTask(payload, options = {}) {
const env = options.env || process.env;
const requestId = normalizeText(payload?.requestId);
const objective = normalizeText(payload?.objective);
const platform = normalizePlatform(payload?.platform || payload?.context?.controlPlatform);
const provider = normalizeProvider(payload?.provider || payload?.context?.computerUseProvider);
if (platform !== "macos") {
return buildFailure(requestId, "UNSUPPORTED_CONTROL_PLATFORM");
}
if (provider !== "cua-driver-computer-use") {
return buildFailure(requestId, "UNSUPPORTED_COMPUTER_USE_PROVIDER");
}
if (!objective) {
return buildFailure(requestId, "CUA_OBJECTIVE_REQUIRED");
}
const targetApp = detectCuaTargetApp(objective);
if (!targetApp) {
return buildFailure(
requestId,
"CUA_TARGET_APP_REQUIRED",
"请在指令里明确要控制的 macOS 应用,例如 Chrome、Safari、QQ、微信、Finder 或系统设置。",
);
}
if (isSubmitLikeObjective(objective) && !isSubmitAllowed(env, payload)) {
return buildConfirmationResult(payload, targetApp);
}
const toolTrace = [];
try {
const targetSession = await resolveTargetAppSession(targetApp, objective, {
...options,
env,
}, toolTrace);
const pid = targetSession.pid;
if (!pid) {
return buildFailure(requestId, "CUA_TARGET_PID_NOT_FOUND", targetSession.sourceText);
}
let window = targetSession.window;
if (!window) {
const windowsResult = await callCuaTool("list_windows", { pid }, { ...options, env });
toolTrace.push("list_windows");
window = selectWindow(windowsResult.structured?.windows);
}
const windowId = getWindowId(window);
if (!windowId) {
return buildFailure(requestId, "CUA_TARGET_WINDOW_NOT_FOUND", targetSession.sourceText);
}
const beforeState = await callCuaTool("get_window_state", { pid, window_id: windowId }, { ...options, env });
toolTrace.push("get_window_state");
const typedText = extractQuotedText(objective);
if (typedText) {
await callCuaTool("type_text", { pid, window_id: windowId, text: typedText, delay_ms: 20 }, { ...options, env });
toolTrace.push("type_text");
if (isSubmitLikeObjective(objective) && isSubmitAllowed(env, payload)) {
await callCuaTool("press_key", { pid, window_id: windowId, key: "return" }, { ...options, env });
toolTrace.push("press_key");
}
await callCuaTool("get_window_state", { pid, window_id: windowId }, { ...options, env });
toolTrace.push("get_window_state");
}
const observation = beforeState.text ? `窗口观测:${beforeState.text.split(/\r?\n/)[0]}` : "已完成窗口观测。";
const actionSummary = typedText ? `并已向目标应用写入 ${typedText.length} 个字符。` : "已打开并读取目标窗口。";
return {
status: "completed",
requestId: requestId || undefined,
replyBody: `已通过 Cua Driver 接入 ${targetApp.label}${actionSummary}${observation}`,
targetApp: targetApp.label,
executionSummary: toolTrace.join(" -> "),
};
} catch (error) {
return buildFailure(requestId, error?.message || "CUA_DRIVER_EXECUTION_FAILED");
}
}
async function main() {
const raw = await readStdin();
const payload = parseJsonPayload(raw);
const result = await runCuaDriverComputerUseTask(payload, {
env: process.env,
cwd: process.cwd(),
});
writeJson(result);
}
const currentFile = fileURLToPath(import.meta.url);
if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
main().catch((error) => {
writeJson({
status: "failed",
error: error?.message || "CUA_DRIVER_RUNTIME_FAILED",
});
});
}

View File

@@ -1,32 +1,118 @@
#!/bin/zsh
set -euo pipefail
PLIST_SOURCE="/Users/kris/code/boss/deployment/launchd/com.hyzq.boss.local-agent.plist"
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
PLIST_SOURCE="$ROOT_DIR/deployment/launchd/com.hyzq.boss.local-agent.plist"
PLIST_TARGET="$HOME/Library/LaunchAgents/com.hyzq.boss.local-agent.plist"
BRIDGE_PLIST_SOURCE="/Users/kris/code/boss/deployment/launchd/com.hyzq.boss.codex-desktop-bridge.plist"
BRIDGE_PLIST_SOURCE="$ROOT_DIR/deployment/launchd/com.hyzq.boss.codex-desktop-bridge.plist"
BRIDGE_PLIST_TARGET="$HOME/Library/LaunchAgents/com.hyzq.boss.codex-desktop-bridge.plist"
CONFIG_PATH="${1:-/Users/kris/code/boss/local-agent/config.cloud.json}"
CONFIG_SOURCE_ARG="${1:-}"
if [[ "$CONFIG_PATH" != /* ]]; then
CONFIG_PATH="/Users/kris/code/boss/${CONFIG_PATH}"
config_has_device_identity() {
python3 - "$1" <<'PY'
import json
from pathlib import Path
import sys
try:
config = json.loads(Path(sys.argv[1]).read_text())
except Exception:
raise SystemExit(1)
raise SystemExit(0 if config.get("deviceId") and config.get("token") else 1)
PY
}
resolve_default_config_source() {
local ACTIVE_CONFIG_PATH=""
local default_config_path="$ROOT_DIR/local-agent/config.cloud.json"
if [[ -f "$PLIST_TARGET" ]]; then
ACTIVE_CONFIG_PATH="$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments:2' "$PLIST_TARGET" 2>/dev/null || true)"
if [[ -n "$ACTIVE_CONFIG_PATH" && "$ACTIVE_CONFIG_PATH" == "$ROOT_DIR/local-agent/"*.json && -f "$ACTIVE_CONFIG_PATH" ]]; then
printf '%s\n' "$ACTIVE_CONFIG_PATH"
return 0
fi
fi
local custom_config=""
local custom_name=""
for custom_config in "$ROOT_DIR"/local-agent/config*.json(N); do
custom_name="$(basename "$custom_config")"
case "$custom_name" in
config.installed.json|config.cloud.json|config.example.json)
continue
;;
esac
if config_has_device_identity "$custom_config"; then
printf '%s\n' "$custom_config"
return 0
fi
done
if [[ -f "$ROOT_DIR/local-agent/config.installed.json" ]]; then
printf '%s\n' "$ROOT_DIR/local-agent/config.installed.json"
return 0
fi
printf '%s\n' "$default_config_path"
}
if [[ -n "$CONFIG_SOURCE_ARG" ]]; then
CONFIG_SOURCE_PATH="$CONFIG_SOURCE_ARG"
else
CONFIG_SOURCE_PATH="$(resolve_default_config_source)"
fi
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "Config file not found: $CONFIG_PATH" >&2
if [[ "$CONFIG_SOURCE_PATH" != /* ]]; then
CONFIG_SOURCE_PATH="$ROOT_DIR/${CONFIG_SOURCE_PATH}"
fi
if [[ ! -f "$CONFIG_SOURCE_PATH" ]]; then
echo "Config file not found: $CONFIG_SOURCE_PATH" >&2
exit 1
fi
CONFIG_PATH="$ROOT_DIR/local-agent/config.installed.json"
python3 - <<'PY' "$CONFIG_SOURCE_PATH" "$CONFIG_PATH" "$ROOT_DIR"
import json
from pathlib import Path
import sys
source_path = Path(sys.argv[1])
target_path = Path(sys.argv[2])
root_dir = sys.argv[3]
config = json.loads(source_path.read_text())
for key in (
"masterAgentWorkdir",
"codexAppServerWorkdir",
"codexComputerUseWorkdir",
"browserControlWorkdir",
"computerUseWorkdir",
"codexDesktopRefreshWorkdir",
"omxWorkdir",
):
config[key] = root_dir
target_path.write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n")
PY
mkdir -p "$HOME/Library/LaunchAgents"
cp "$BRIDGE_PLIST_SOURCE" "$BRIDGE_PLIST_TARGET"
cp "$PLIST_SOURCE" "$PLIST_TARGET"
python3 - <<'PY' "$PLIST_TARGET" "$CONFIG_PATH"
python3 - <<'PY' "$PLIST_TARGET" "$BRIDGE_PLIST_TARGET" "$CONFIG_PATH" "$ROOT_DIR"
from pathlib import Path
import sys
plist_path = Path(sys.argv[1])
config_path = sys.argv[2]
text = plist_path.read_text()
plist_path.write_text(text.replace("__BOSS_AGENT_CONFIG__", config_path))
local_plist_path = Path(sys.argv[1])
bridge_plist_path = Path(sys.argv[2])
config_path = sys.argv[3]
root_dir = sys.argv[4]
for plist_path in (local_plist_path, bridge_plist_path):
text = plist_path.read_text()
text = text.replace("__BOSS_AGENT_CONFIG__", config_path)
text = text.replace("__BOSS_AGENT_ROOT__", root_dir)
# Keep older generated plists installable if a package contains a pre-placeholder file.
text = text.replace("/Users/kris/code/boss", root_dir)
plist_path.write_text(text)
PY
plutil -lint "$PLIST_TARGET" >/dev/null
plutil -lint "$BRIDGE_PLIST_TARGET" >/dev/null

View File

@@ -0,0 +1,311 @@
#!/bin/zsh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
VERSION="${BOSS_AGENT_PACKAGE_VERSION:-$(date +%Y%m%d%H%M%S)}"
PACKAGE_NAME="boss-agent-mac-runtime-${VERSION}"
DIST_DIR="$ROOT_DIR/dist"
STAGE_DIR="$DIST_DIR/$PACKAGE_NAME"
RUNTIME_DIR="$STAGE_DIR/runtime"
ARCHIVE_PATH="$DIST_DIR/${PACKAGE_NAME}.zip"
rm -rf "$STAGE_DIR" "$ARCHIVE_PATH"
"$ROOT_DIR/scripts/build-boss-agent-mac-app.sh" >/dev/null
mkdir -p "$RUNTIME_DIR/scripts" "$RUNTIME_DIR/local-agent" "$RUNTIME_DIR/deployment/launchd"
cp -R "$ROOT_DIR/dist/boss-agent.app" "$STAGE_DIR/boss-agent.app"
rsync -a "$ROOT_DIR/local-agent/" "$RUNTIME_DIR/local-agent/"
cp "$ROOT_DIR/scripts/start-local-agent.sh" "$RUNTIME_DIR/scripts/start-local-agent.sh"
cp "$ROOT_DIR/scripts/install-local-launchagent.sh" "$RUNTIME_DIR/scripts/install-local-launchagent.sh"
cp "$ROOT_DIR/scripts/browser-control-smoke.mjs" "$RUNTIME_DIR/scripts/browser-control-smoke.mjs"
cp "$ROOT_DIR/scripts/codex-computer-use-runtime.mjs" "$RUNTIME_DIR/scripts/codex-computer-use-runtime.mjs"
cp "$ROOT_DIR/scripts/computer-use-smoke.mjs" "$RUNTIME_DIR/scripts/computer-use-smoke.mjs"
cp "$ROOT_DIR/scripts/cua-driver-computer-use-runtime.mjs" "$RUNTIME_DIR/scripts/cua-driver-computer-use-runtime.mjs"
cp "$ROOT_DIR/scripts/codex-desktop-refresh-hint.mjs" "$RUNTIME_DIR/scripts/codex-desktop-refresh-hint.mjs"
cp "$ROOT_DIR/scripts/codex-desktop-refresh-bridge-daemon.mjs" "$RUNTIME_DIR/scripts/codex-desktop-refresh-bridge-daemon.mjs"
cp "$ROOT_DIR/deployment/launchd/com.hyzq.boss.local-agent.plist" "$RUNTIME_DIR/deployment/launchd/com.hyzq.boss.local-agent.plist"
cp "$ROOT_DIR/deployment/launchd/com.hyzq.boss.codex-desktop-bridge.plist" "$RUNTIME_DIR/deployment/launchd/com.hyzq.boss.codex-desktop-bridge.plist"
node - <<'NODE' "$RUNTIME_DIR" "$VERSION"
const fs = require("fs");
const path = require("path");
const runtimeDir = process.argv[2];
const version = process.argv[3];
for (const name of ["config.cloud.json", "config.example.json"]) {
const configPath = path.join(runtimeDir, "local-agent", name);
if (!fs.existsSync(configPath)) continue;
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
config.bossAgentOtaEnabled = true;
config.bossAgentVersion = version;
config.bossAgentOtaAutoInstall = false;
config.bossAgentOtaCheckIntervalMs = config.bossAgentOtaCheckIntervalMs || 300000;
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
}
NODE
cat > "$RUNTIME_DIR/package.json" <<'JSON'
{
"name": "boss-agent-runtime",
"version": "0.1.0",
"private": true,
"dependencies": {
"qrcode": "^1.5.4"
}
}
JSON
node - <<'NODE' "$ROOT_DIR" "$RUNTIME_DIR"
const fs = require("fs");
const path = require("path");
const rootDir = process.argv[2];
const runtimeDir = process.argv[3];
const sourceModules = path.join(rootDir, "node_modules");
const targetModules = path.join(runtimeDir, "node_modules");
const seen = new Set();
function readPackage(name) {
return JSON.parse(fs.readFileSync(path.join(sourceModules, name, "package.json"), "utf8"));
}
function walk(name) {
if (seen.has(name)) return;
seen.add(name);
const pkg = readPackage(name);
for (const dep of Object.keys(pkg.dependencies || {})) {
walk(dep);
}
}
walk("qrcode");
fs.mkdirSync(targetModules, { recursive: true });
for (const name of seen) {
fs.cpSync(path.join(sourceModules, name), path.join(targetModules, name), {
recursive: true,
dereference: true,
filter: (source) => !/\/(test|tests|example|examples|\.github)\b/.test(source),
});
}
NODE
cat > "$STAGE_DIR/install.command" <<'SH'
#!/bin/zsh
set -euo pipefail
SOURCE_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_ROOT="${BOSS_AGENT_INSTALL_ROOT:-$HOME/boss-agent/current}"
APP_TARGET_DIR="${BOSS_AGENT_APP_TARGET_DIR:-$HOME/Applications}"
APP_TARGET="$APP_TARGET_DIR/boss-agent.app"
CONFIG_BACKUP_DIR=""
ACTIVE_CONFIG_BASENAME="config.cloud.json"
ACTIVE_PLIST="$HOME/Library/LaunchAgents/com.hyzq.boss.local-agent.plist"
NODE_BIN="${BOSS_NODE_BIN:-}"
if [[ -f "$ACTIVE_PLIST" ]]; then
ACTIVE_CONFIG_PATH="$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments:2' "$ACTIVE_PLIST" 2>/dev/null || true)"
if [[ -n "$ACTIVE_CONFIG_PATH" && "$ACTIVE_CONFIG_PATH" == "$INSTALL_ROOT/local-agent/"*.json ]]; then
ACTIVE_CONFIG_BASENAME="$(basename "$ACTIVE_CONFIG_PATH")"
fi
fi
if [[ -z "$NODE_BIN" ]]; then
NODE_BIN="$(command -v node 2>/dev/null || true)"
fi
if [[ -z "$NODE_BIN" ]]; then
NODE_CANDIDATES=("$HOME"/.boss-runtime/node-*/bin/node(N) /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node)
for candidate in "${NODE_CANDIDATES[@]}"; do
if [[ -x "$candidate" ]]; then
NODE_BIN="$candidate"
break
fi
done
fi
if [[ -z "$NODE_BIN" || ! -x "$NODE_BIN" ]]; then
echo "Node.js is required. Install Node.js 22 or newer first." >&2
exit 1
fi
NODE_MAJOR="$("$NODE_BIN" -p 'Number(process.versions.node.split(".")[0])')"
if [[ "$NODE_MAJOR" -lt 22 ]]; then
echo "Node.js 22 or newer is required. Current: $("$NODE_BIN" -v)" >&2
exit 1
fi
EXISTING_CONFIGS=("$INSTALL_ROOT"/local-agent/config*.json(N))
if [[ "${#EXISTING_CONFIGS[@]}" -gt 0 ]]; then
CONFIG_BACKUP_DIR="$(mktemp -d)"
cp "${EXISTING_CONFIGS[@]}" "$CONFIG_BACKUP_DIR"/
fi
mkdir -p "$(dirname "$INSTALL_ROOT")" "$APP_TARGET_DIR"
rsync -a --delete "$SOURCE_DIR/runtime/" "$INSTALL_ROOT/"
if [[ -n "$CONFIG_BACKUP_DIR" && -d "$CONFIG_BACKUP_DIR" ]]; then
cp "$CONFIG_BACKUP_DIR"/config*.json "$INSTALL_ROOT/local-agent/"
rm -rf "$CONFIG_BACKUP_DIR"
fi
PACKAGE_VERSION="__BOSS_AGENT_PACKAGE_VERSION__"
"$NODE_BIN" - <<'NODE' "$INSTALL_ROOT/local-agent" "$INSTALL_ROOT" "$PACKAGE_VERSION"
const fs = require("fs");
const os = require("os");
const path = require("path");
const configDir = process.argv[2];
const installRoot = process.argv[3];
const version = process.argv[4];
const home = os.homedir();
const configPaths = fs.readdirSync(configDir)
.filter((name) => /^config.*\.json$/.test(name))
.map((name) => path.join(configDir, name));
for (const configPath of configPaths) {
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
const nodeCommand = config.computerUseCommand && path.isAbsolute(config.computerUseCommand)
? config.computerUseCommand
: "node";
config.bossAgentOtaEnabled = true;
config.bossAgentVersion = version;
config.bossAgentInstallRoot = installRoot;
config.bossAgentOtaDownloadDir = path.join(home, "boss-agent", "updates");
config.bossAgentOtaCheckIntervalMs = config.bossAgentOtaCheckIntervalMs || 300000;
config.bossAgentOtaAutoInstall = false;
config.skillsDir = config.skillsDir || path.join(home, ".codex", "skills");
config.codexAppServerEnabled = config.codexAppServerEnabled !== false;
config.codexAppServerCommand = config.codexAppServerCommand || "codex";
config.codexAppServerArgs = Array.isArray(config.codexAppServerArgs) ? config.codexAppServerArgs : ["app-server"];
config.codexAppServerTimeoutMs = config.codexAppServerTimeoutMs || 120000;
config.codexAppServerFallbackToCli = config.codexAppServerFallbackToCli !== false;
config.codexComputerUseEnabled = config.codexComputerUseEnabled !== false;
config.codexComputerUseCommand = config.codexComputerUseCommand || nodeCommand;
config.codexComputerUseArgs = Array.isArray(config.codexComputerUseArgs)
? config.codexComputerUseArgs
: ["scripts/codex-computer-use-runtime.mjs"];
config.codexComputerUseTimeoutMs = config.codexComputerUseTimeoutMs || 120000;
config.codexComputerUseFallbackToCua = config.codexComputerUseFallbackToCua !== false;
if (!Array.isArray(config.computerUseArgs) || config.computerUseArgs.length === 0 || config.computerUseArgs.includes("scripts/computer-use-smoke.mjs")) {
config.computerUseArgs = ["scripts/cua-driver-computer-use-runtime.mjs"];
}
config.cuaDriverCommand = config.cuaDriverCommand || "cua-driver";
config.cuaDriverArgs = Array.isArray(config.cuaDriverArgs) ? config.cuaDriverArgs : [];
config.cuaDriverTimeoutMs = config.cuaDriverTimeoutMs || 45000;
for (const key of [
"masterAgentWorkdir",
"codexAppServerWorkdir",
"codexComputerUseWorkdir",
"browserControlWorkdir",
"computerUseWorkdir",
"codexDesktopRefreshWorkdir",
"omxWorkdir"
]) {
config[key] = installRoot;
}
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
}
NODE
if [[ "$ACTIVE_CONFIG_BASENAME" == "config.installed.json" || "$ACTIVE_CONFIG_BASENAME" == "config.cloud.json" || "$ACTIVE_CONFIG_BASENAME" == "config.example.json" ]]; then
CUSTOM_CONFIGS=("$INSTALL_ROOT"/local-agent/config*.json(N))
for custom_config in "${CUSTOM_CONFIGS[@]}"; do
custom_name="$(basename "$custom_config")"
case "$custom_name" in
config.installed.json|config.cloud.json|config.example.json)
continue
;;
esac
if "$NODE_BIN" - "$custom_config" <<'NODE'; then
const fs = require("fs");
const configPath = process.argv[2];
try {
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
process.exit(config.deviceId && config.token ? 0 : 1);
} catch {
process.exit(1);
}
NODE
ACTIVE_CONFIG_BASENAME="$custom_name"
break
fi
done
fi
if [[ ! -f "$INSTALL_ROOT/local-agent/$ACTIVE_CONFIG_BASENAME" ]]; then
ACTIVE_CONFIG_BASENAME="config.cloud.json"
fi
chmod +x "$INSTALL_ROOT/scripts/start-local-agent.sh"
chmod +x "$INSTALL_ROOT/scripts/install-local-launchagent.sh"
chmod +x "$INSTALL_ROOT/scripts/"*.mjs
rm -rf "$APP_TARGET"
cp -R "$SOURCE_DIR/boss-agent.app" "$APP_TARGET"
"$INSTALL_ROOT/scripts/install-local-launchagent.sh" "$INSTALL_ROOT/local-agent/$ACTIVE_CONFIG_BASENAME"
echo "boss-agent installed:"
echo " runtime: $INSTALL_ROOT"
echo " app: $APP_TARGET"
echo "Open $APP_TARGET to view status."
SH
perl -0pi -e "s/__BOSS_AGENT_PACKAGE_VERSION__/$VERSION/g" "$STAGE_DIR/install.command"
chmod +x "$STAGE_DIR/install.command"
cat > "$STAGE_DIR/README_INSTALL.txt" <<'TXT'
Boss Agent macOS runtime package
Install:
1. Unzip this package on the controlled Mac.
2. Double click install.command.
3. Open ~/Applications/boss-agent.app.
Notes:
- Existing ~/boss-agent/current/local-agent/config.cloud.json is preserved.
- The installer updates bossAgentVersion and local runtime paths after preserving binding credentials.
- boss-agent OTA downloads future macOS runtime packages from the Boss server, verifies sha256, then launches this installer flow again.
- Node.js 22 or newer is required because the local agent uses node:sqlite.
- This build includes the Cua Driver desktop control runtime. Install and authorize `cua-driver` on the controlled Mac before enabling full desktop GUI control.
TXT
(
cd "$DIST_DIR"
ditto -c -k --sequesterRsrc --keepParent "$PACKAGE_NAME" "$ARCHIVE_PATH"
)
node - <<'NODE' "$ARCHIVE_PATH" "$ROOT_DIR" "$VERSION"
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const archivePath = process.argv[2];
const rootDir = process.argv[3];
const version = process.argv[4];
const downloadsDir = path.join(rootDir, "public", "downloads");
const latestPath = path.join(downloadsDir, "boss-agent-mac-latest.zip");
const latestMetaPath = path.join(downloadsDir, "boss-agent-mac-latest.json");
fs.mkdirSync(downloadsDir, { recursive: true });
fs.copyFileSync(archivePath, latestPath);
const content = fs.readFileSync(latestPath);
const stat = fs.statSync(latestPath);
const meta = {
packageType: "boss_agent_macos",
version,
fileName: "boss-agent-mac-latest.zip",
archiveFileName: path.basename(archivePath),
sizeBytes: stat.size,
sha256: crypto.createHash("sha256").update(content).digest("hex"),
updatedAt: stat.mtime.toISOString(),
downloadUrl: "/api/v1/boss-agent/ota/package"
};
fs.writeFileSync(latestMetaPath, `${JSON.stringify(meta, null, 2)}\n`);
NODE
echo "$ARCHIVE_PATH"

View File

@@ -3,6 +3,26 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG_PATH="${1:-$ROOT_DIR/local-agent/config.example.json}"
NODE_BIN="${BOSS_NODE_BIN:-}"
if [[ -z "$NODE_BIN" ]]; then
NODE_BIN="$(command -v node 2>/dev/null || true)"
fi
if [[ -z "$NODE_BIN" ]]; then
NODE_CANDIDATES=("$HOME"/.boss-runtime/node-*/bin/node(N) /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node)
for candidate in "${NODE_CANDIDATES[@]}"; do
if [[ -x "$candidate" ]]; then
NODE_BIN="$candidate"
break
fi
done
fi
if [[ -z "$NODE_BIN" || ! -x "$NODE_BIN" ]]; then
echo "Node.js 22 or newer is required. Set BOSS_NODE_BIN or install Node.js." >&2
exit 1
fi
cd "$ROOT_DIR"
exec node ./local-agent/server.mjs "$CONFIG_PATH"
exec "$NODE_BIN" ./local-agent/server.mjs "$CONFIG_PATH"