feat: ship enterprise control and desktop governance
This commit is contained in:
232
scripts/boss-state-store-maintenance.mjs
Executable file
232
scripts/boss-state-store-maintenance.mjs
Executable file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env node
|
||||
import { Client } from "pg";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
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");
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
"Usage: node scripts/boss-state-store-maintenance.mjs <command> [options]",
|
||||
"",
|
||||
"Commands:",
|
||||
" describe",
|
||||
" backup-file --input <file> [--output <file>] [--dry-run]",
|
||||
" export-file --input <file> --output <file> [--dry-run]",
|
||||
" migrate-file-to-postgres --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",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const [command, ...items] = argv;
|
||||
const options = { command, dryRun: false };
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
const item = items[index];
|
||||
if (item === "--dry-run") {
|
||||
options.dryRun = true;
|
||||
continue;
|
||||
}
|
||||
if (item === "--input") {
|
||||
options.input = items[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (item === "--output") {
|
||||
options.output = items[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`UNKNOWN_OPTION:${item}`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function jsonOut(payload) {
|
||||
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function timestampSegment() {
|
||||
return new Date().toISOString().replace(/[:.]/g, "-");
|
||||
}
|
||||
|
||||
function validateStateText(text, source) {
|
||||
const parsed = JSON.parse(text);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new Error(`STATE_JSON_INVALID:${source}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function readStateText(filePath) {
|
||||
const text = await fs.readFile(filePath, "utf8");
|
||||
validateStateText(text, filePath);
|
||||
return text;
|
||||
}
|
||||
|
||||
async function ensurePostgresSchema(client) {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS boss_state_snapshots (
|
||||
snapshot_key TEXT PRIMARY KEY,
|
||||
state JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
async function withPostgres(handler) {
|
||||
const connectionString = process.env.BOSS_DATABASE_URL?.trim();
|
||||
if (!connectionString) {
|
||||
throw new Error("BOSS_DATABASE_URL_REQUIRED");
|
||||
}
|
||||
const client = new Client({ connectionString });
|
||||
await client.connect();
|
||||
try {
|
||||
return await handler(client);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function backupFile(options) {
|
||||
const source = path.resolve(options.input || defaultStateFile);
|
||||
const text = await readStateText(source);
|
||||
const output = path.resolve(options.output || path.join(defaultBackupDir, `boss-state-${timestampSegment()}.json`));
|
||||
if (!options.dryRun) {
|
||||
await fs.mkdir(path.dirname(output), { recursive: true });
|
||||
await fs.writeFile(output, text, "utf8");
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
action: "backup-file",
|
||||
dryRun: options.dryRun,
|
||||
source,
|
||||
output,
|
||||
bytes: Buffer.byteLength(text),
|
||||
};
|
||||
}
|
||||
|
||||
async function exportFile(options) {
|
||||
const source = path.resolve(options.input || defaultStateFile);
|
||||
if (!options.output) {
|
||||
throw new Error("OUTPUT_REQUIRED");
|
||||
}
|
||||
const text = await readStateText(source);
|
||||
const output = path.resolve(options.output);
|
||||
if (!options.dryRun) {
|
||||
await fs.mkdir(path.dirname(output), { recursive: true });
|
||||
await fs.writeFile(output, text, "utf8");
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
action: "export-file",
|
||||
dryRun: options.dryRun,
|
||||
source,
|
||||
output,
|
||||
bytes: Buffer.byteLength(text),
|
||||
};
|
||||
}
|
||||
|
||||
async function migrateFileToPostgres(options) {
|
||||
const source = path.resolve(options.input || defaultStateFile);
|
||||
const text = await readStateText(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, text],
|
||||
);
|
||||
});
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
action: "migrate-file-to-postgres",
|
||||
dryRun: options.dryRun,
|
||||
source,
|
||||
snapshotKey,
|
||||
bytes: Buffer.byteLength(text),
|
||||
};
|
||||
}
|
||||
|
||||
async function rollbackPostgresToFile(options) {
|
||||
const output = path.resolve(options.output || defaultStateFile);
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
action: "rollback-postgres-to-file",
|
||||
dryRun: true,
|
||||
output,
|
||||
snapshotKey,
|
||||
};
|
||||
}
|
||||
const text = 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);
|
||||
});
|
||||
await fs.mkdir(path.dirname(output), { recursive: true });
|
||||
await fs.writeFile(output, text, "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
action: "rollback-postgres-to-file",
|
||||
dryRun: false,
|
||||
output,
|
||||
snapshotKey,
|
||||
bytes: Buffer.byteLength(text),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
switch (options.command) {
|
||||
case "describe":
|
||||
jsonOut({
|
||||
ok: true,
|
||||
action: "describe",
|
||||
mode: process.env.BOSS_STATE_STORE === "postgres" ? "postgres" : "file",
|
||||
stateFile: path.resolve(defaultStateFile),
|
||||
backupDir: path.resolve(defaultBackupDir),
|
||||
postgresConfigured: Boolean(process.env.BOSS_DATABASE_URL?.trim()),
|
||||
postgresTable: "boss_state_snapshots",
|
||||
snapshotKey,
|
||||
});
|
||||
return;
|
||||
case "backup-file":
|
||||
jsonOut(await backupFile(options));
|
||||
return;
|
||||
case "export-file":
|
||||
jsonOut(await exportFile(options));
|
||||
return;
|
||||
case "migrate-file-to-postgres":
|
||||
jsonOut(await migrateFileToPostgres(options));
|
||||
return;
|
||||
case "rollback-postgres-to-file":
|
||||
jsonOut(await rollbackPostgresToFile(options));
|
||||
return;
|
||||
default:
|
||||
throw new Error(`UNKNOWN_COMMAND\n${usage()}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
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,
|
||||
});
|
||||
96
scripts/codex-desktop-event-consumer.mjs
Normal file
96
scripts/codex-desktop-event-consumer.mjs
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
function parseBoolean(value) {
|
||||
return String(value || "").trim().toLowerCase() === "true";
|
||||
}
|
||||
|
||||
function sanitizeRefreshEvent(data) {
|
||||
return {
|
||||
eventId: data?.eventId,
|
||||
eventType: data?.eventType,
|
||||
receivedAt: data?.receivedAt,
|
||||
targetThreadRef: data?.targetThreadRef,
|
||||
sourceMessageId: data?.sourceMessageId,
|
||||
appName: data?.appName,
|
||||
refreshMode: data?.refreshMode,
|
||||
status: data?.status,
|
||||
deepLink: data?.deepLink,
|
||||
detail: data?.detail,
|
||||
};
|
||||
}
|
||||
|
||||
function compactUndefinedFields(value) {
|
||||
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
||||
}
|
||||
|
||||
async function consumeEvents({ eventsUrl, once = false }) {
|
||||
const response = await fetch(eventsUrl, {
|
||||
headers: {
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
});
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`CODEX_DESKTOP_EVENTS_UNAVAILABLE status=${response.status}`);
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const reader = response.body.getReader();
|
||||
let buffer = "";
|
||||
let currentEvent = {};
|
||||
|
||||
function finishEvent() {
|
||||
if (!currentEvent.event || currentEvent.event !== "codex_desktop_refresh") {
|
||||
currentEvent = {};
|
||||
return false;
|
||||
}
|
||||
const data = currentEvent.data ? JSON.parse(currentEvent.data) : {};
|
||||
process.stdout.write(`${JSON.stringify(compactUndefinedFields(sanitizeRefreshEvent(data)))}\n`);
|
||||
currentEvent = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleLine(line) {
|
||||
if (line === "") {
|
||||
return finishEvent();
|
||||
}
|
||||
if (line.startsWith("event:")) {
|
||||
currentEvent.event = line.slice("event:".length).trim();
|
||||
} else if (line.startsWith("id:")) {
|
||||
currentEvent.id = line.slice("id:".length).trim();
|
||||
} else if (line.startsWith("data:")) {
|
||||
currentEvent.data = `${currentEvent.data || ""}${line.slice("data:".length).trim()}`;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split(/\r?\n/);
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
const consumed = handleLine(line);
|
||||
if (consumed && once) {
|
||||
await reader.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eventsUrl =
|
||||
String(process.env.BOSS_CODEX_DESKTOP_EVENTS_URL || "").trim() ||
|
||||
"http://127.0.0.1:4318/api/v1/codex-desktop/events";
|
||||
|
||||
try {
|
||||
await consumeEvents({
|
||||
eventsUrl,
|
||||
once: parseBoolean(process.env.BOSS_CODEX_DESKTOP_EVENTS_ONCE),
|
||||
});
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
169
scripts/codex-desktop-integration-probe.mjs
Normal file
169
scripts/codex-desktop-integration-probe.mjs
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { access, open } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const DEFAULT_CODEX_APP_PATH = "/Applications/Codex.app";
|
||||
const DEFAULT_BRIDGE_EVENTS_URL = "http://127.0.0.1:4318/api/v1/codex-desktop/events";
|
||||
|
||||
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() || `${command} exit code ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function pathExists(filePath) {
|
||||
try {
|
||||
await access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function readPlistAsJson(plistPath) {
|
||||
const output = await runCommand("plutil", ["-convert", "json", "-o", "-", plistPath]);
|
||||
return JSON.parse(output);
|
||||
}
|
||||
|
||||
function extractUrlSchemes(info) {
|
||||
const urlTypes = Array.isArray(info?.CFBundleURLTypes) ? info.CFBundleURLTypes : [];
|
||||
return [
|
||||
...new Set(
|
||||
urlTypes.flatMap((item) =>
|
||||
Array.isArray(item?.CFBundleURLSchemes)
|
||||
? item.CFBundleURLSchemes.map((scheme) => String(scheme || "").trim()).filter(Boolean)
|
||||
: [],
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
async function fileContains(filePath, needle) {
|
||||
if (!(await pathExists(filePath))) {
|
||||
return false;
|
||||
}
|
||||
const needleBuffer = Buffer.from(needle);
|
||||
const handle = await open(filePath, "r");
|
||||
const chunkSize = 1024 * 1024;
|
||||
const overlapSize = Math.max(needleBuffer.length - 1, 0);
|
||||
let carry = Buffer.alloc(0);
|
||||
try {
|
||||
const buffer = Buffer.alloc(chunkSize);
|
||||
let position = 0;
|
||||
while (true) {
|
||||
const { bytesRead } = await handle.read(buffer, 0, chunkSize, position);
|
||||
if (bytesRead === 0) {
|
||||
return false;
|
||||
}
|
||||
const chunk = Buffer.concat([carry, buffer.subarray(0, bytesRead)]);
|
||||
if (chunk.includes(needleBuffer)) {
|
||||
return true;
|
||||
}
|
||||
carry = overlapSize > 0 ? chunk.subarray(Math.max(0, chunk.length - overlapSize)) : Buffer.alloc(0);
|
||||
position += bytesRead;
|
||||
}
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function detectCodexDesktopIntegration(options = {}) {
|
||||
const appPath =
|
||||
String(options.appPath || process.env.BOSS_CODEX_DESKTOP_APP_PATH || DEFAULT_CODEX_APP_PATH).trim() ||
|
||||
DEFAULT_CODEX_APP_PATH;
|
||||
const bridgeEventsUrl =
|
||||
String(options.bridgeEventsUrl || process.env.BOSS_CODEX_DESKTOP_EVENTS_URL || DEFAULT_BRIDGE_EVENTS_URL).trim() ||
|
||||
DEFAULT_BRIDGE_EVENTS_URL;
|
||||
const infoPlistPath = path.join(appPath, "Contents", "Info.plist");
|
||||
const resourcesDir = path.join(appPath, "Contents", "Resources");
|
||||
const appAsarPath = path.join(resourcesDir, "app.asar");
|
||||
|
||||
const appExists = await pathExists(appPath);
|
||||
if (!appExists || !(await pathExists(infoPlistPath))) {
|
||||
return {
|
||||
ok: false,
|
||||
app: {
|
||||
path: appPath,
|
||||
found: false,
|
||||
},
|
||||
capabilities: {},
|
||||
reason: "CODEX_DESKTOP_APP_NOT_FOUND",
|
||||
};
|
||||
}
|
||||
|
||||
const info = await readPlistAsJson(infoPlistPath);
|
||||
const urlSchemes = extractUrlSchemes(info);
|
||||
const hasCodexScheme = urlSchemes.includes("codex");
|
||||
const hasThreadDeepLinkResource = await fileContains(appAsarPath, "codex://threads/");
|
||||
const hasRouteThreadResource = await fileContains(appAsarPath, "hotkey-window/thread");
|
||||
const threadDeepLinkSupported = hasCodexScheme && (hasThreadDeepLinkResource || hasRouteThreadResource);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
app: {
|
||||
path: appPath,
|
||||
found: true,
|
||||
bundleIdentifier: info.CFBundleIdentifier,
|
||||
shortVersion: info.CFBundleShortVersionString,
|
||||
version: info.CFBundleVersion,
|
||||
urlSchemes,
|
||||
},
|
||||
capabilities: {
|
||||
threadDeepLink: {
|
||||
supported: threadDeepLinkSupported,
|
||||
template: threadDeepLinkSupported ? "codex://threads/{threadId}" : undefined,
|
||||
evidence: threadDeepLinkSupported
|
||||
? "CFBundleURLSchemes contains codex and app resources contain codex://threads/"
|
||||
: "thread deep link route not detected",
|
||||
},
|
||||
desktopBridgeSse: {
|
||||
supported: true,
|
||||
url: bridgeEventsUrl,
|
||||
evidence: "Boss Codex Desktop Bridge exposes local SSE metadata stream",
|
||||
},
|
||||
inAppSubscription: {
|
||||
supported: false,
|
||||
evidence: "No stable public Codex Desktop plugin or IPC subscription API detected by this probe",
|
||||
},
|
||||
packagePatch: {
|
||||
supported: false,
|
||||
evidence: "Boss policy does not patch or modify the signed Codex Desktop application bundle",
|
||||
},
|
||||
},
|
||||
recommendedPath: threadDeepLinkSupported
|
||||
? "Use Boss rollout mirror + codex://threads/{threadId} + local bridge SSE; only move to in-app subscription if Codex Desktop exposes a stable plugin/IPC API."
|
||||
: "Keep Boss rollout mirror and local bridge SSE; avoid Desktop package patching until a stable supported integration exists.",
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const result = await detectCodexDesktopIntegration();
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main();
|
||||
}
|
||||
183
scripts/codex-desktop-refresh-bridge-daemon.mjs
Normal file
183
scripts/codex-desktop-refresh-bridge-daemon.mjs
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import { executeCodexDesktopRefreshHint } from "./codex-desktop-refresh-hint.mjs";
|
||||
import { detectCodexDesktopIntegration } from "./codex-desktop-integration-probe.mjs";
|
||||
|
||||
const subscribers = new Set();
|
||||
const recentEvents = [];
|
||||
let nextEventId = 1;
|
||||
|
||||
function parsePort(value) {
|
||||
const parsed = Number.parseInt(String(value ?? ""), 10);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 4318;
|
||||
}
|
||||
|
||||
function readRequestBody(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let raw = "";
|
||||
request.setEncoding("utf8");
|
||||
request.on("data", (chunk) => {
|
||||
raw += chunk;
|
||||
if (raw.length > 64 * 1024) {
|
||||
reject(new Error("CODEX_DESKTOP_REFRESH_HINT_TOO_LARGE"));
|
||||
request.destroy();
|
||||
}
|
||||
});
|
||||
request.on("end", () => {
|
||||
resolve(raw);
|
||||
});
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function writeJson(response, statusCode, payload) {
|
||||
response.writeHead(statusCode, {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
response.end(`${JSON.stringify(payload)}\n`);
|
||||
}
|
||||
|
||||
function writeSse(response, event) {
|
||||
if (event.id !== undefined) {
|
||||
response.write(`id: ${event.id}\n`);
|
||||
}
|
||||
response.write(`event: ${event.event}\n`);
|
||||
response.write(`data: ${JSON.stringify(event.data)}\n\n`);
|
||||
}
|
||||
|
||||
function buildSafeRefreshEvent(payload, result) {
|
||||
return {
|
||||
eventId: nextEventId++,
|
||||
eventType: "codex_desktop_refresh",
|
||||
receivedAt: new Date().toISOString(),
|
||||
targetThreadRef: String(payload?.targetThreadRef || "").trim() || undefined,
|
||||
sourceMessageId: String(payload?.sourceMessageId || "").trim() || undefined,
|
||||
appName: String(result?.appName || payload?.appName || "Codex").trim() || "Codex",
|
||||
refreshMode: String(payload?.refreshMode || "deeplink-reload").trim() || "deeplink-reload",
|
||||
status: result?.status,
|
||||
deepLink: result?.deepLink,
|
||||
detail: result?.detail || result?.error,
|
||||
};
|
||||
}
|
||||
|
||||
function publishRefreshEvent(eventData) {
|
||||
recentEvents.push(eventData);
|
||||
while (recentEvents.length > 50) {
|
||||
recentEvents.shift();
|
||||
}
|
||||
for (const subscriber of subscribers) {
|
||||
writeSse(subscriber, {
|
||||
id: eventData.eventId,
|
||||
event: "codex_desktop_refresh",
|
||||
data: eventData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvents(request, response) {
|
||||
if (request.method !== "GET") {
|
||||
writeJson(response, 405, { status: "failed", error: "METHOD_NOT_ALLOWED" });
|
||||
return;
|
||||
}
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-store",
|
||||
Connection: "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
});
|
||||
subscribers.add(response);
|
||||
writeSse(response, {
|
||||
event: "ready",
|
||||
data: {
|
||||
ok: true,
|
||||
service: "boss-codex-desktop-refresh-bridge",
|
||||
},
|
||||
});
|
||||
const keepAlive = setInterval(() => {
|
||||
response.write(": keepalive\n\n");
|
||||
}, 15000);
|
||||
request.on("close", () => {
|
||||
clearInterval(keepAlive);
|
||||
subscribers.delete(response);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRefresh(request, response) {
|
||||
if (request.method !== "POST") {
|
||||
writeJson(response, 405, { status: "failed", error: "METHOD_NOT_ALLOWED" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await readRequestBody(request);
|
||||
const payload = JSON.parse(raw || "{}");
|
||||
const result = await executeCodexDesktopRefreshHint(payload);
|
||||
publishRefreshEvent(buildSafeRefreshEvent(payload, result));
|
||||
writeJson(response, result.status === "failed" ? 422 : 200, result);
|
||||
} catch (error) {
|
||||
writeJson(response, 400, {
|
||||
status: "failed",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const host = process.env.BOSS_CODEX_DESKTOP_BRIDGE_HOST || "127.0.0.1";
|
||||
const port = parsePort(process.env.BOSS_CODEX_DESKTOP_BRIDGE_PORT);
|
||||
|
||||
const server = createServer(async (request, response) => {
|
||||
if (request.url === "/health") {
|
||||
writeJson(response, 200, {
|
||||
ok: true,
|
||||
service: "boss-codex-desktop-refresh-bridge",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.url === "/api/v1/codex-desktop/events") {
|
||||
handleEvents(request, response);
|
||||
return;
|
||||
}
|
||||
if (request.url === "/api/v1/codex-desktop/events/recent") {
|
||||
writeJson(response, 200, {
|
||||
ok: true,
|
||||
events: recentEvents,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.url === "/api/v1/codex-desktop/capabilities") {
|
||||
try {
|
||||
writeJson(response, 200, await detectCodexDesktopIntegration());
|
||||
} catch (error) {
|
||||
writeJson(response, 500, {
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (request.url === "/api/v1/codex-desktop/refresh") {
|
||||
await handleRefresh(request, response);
|
||||
return;
|
||||
}
|
||||
writeJson(response, 404, { status: "failed", error: "NOT_FOUND" });
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
const address = server.address();
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
status: "ready",
|
||||
service: "boss-codex-desktop-refresh-bridge",
|
||||
host: address.address,
|
||||
port: address.port,
|
||||
})}\n`,
|
||||
);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
server.close(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
274
scripts/codex-desktop-refresh-hint.mjs
Normal file
274
scripts/codex-desktop-refresh-hint.mjs
Normal file
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
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_CODEX_DESKTOP_REFRESH_PAYLOAD" };
|
||||
}
|
||||
return { ok: true, payload: parsed };
|
||||
} catch {
|
||||
return { ok: false, error: "INVALID_CODEX_DESKTOP_REFRESH_PAYLOAD" };
|
||||
}
|
||||
}
|
||||
|
||||
function escapeAppleScriptString(value) {
|
||||
return String(value || "")
|
||||
.replaceAll("\\", "\\\\")
|
||||
.replaceAll('"', '\\"');
|
||||
}
|
||||
|
||||
function parseBoolean(value) {
|
||||
return String(value || "").trim().toLowerCase() === "true";
|
||||
}
|
||||
|
||||
export function buildCodexThreadDeepLink(targetThreadRef) {
|
||||
return `codex://threads/${encodeURIComponent(targetThreadRef)}`;
|
||||
}
|
||||
|
||||
function normalizeRefreshMode(value) {
|
||||
const mode = String(value || "").trim().toLowerCase();
|
||||
const supportedModes = new Set(["off", "activate", "reload", "deeplink", "deeplink-reload"]);
|
||||
return supportedModes.has(mode) ? mode : null;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function runCommand(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stderr = "";
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(stderr.trim() || `${command} exit code ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function activateMacApp(appName) {
|
||||
const escapedAppName = escapeAppleScriptString(appName);
|
||||
try {
|
||||
await runCommand("osascript", [
|
||||
"-e",
|
||||
`tell application "${escapedAppName}" to activate`,
|
||||
]);
|
||||
return `activated ${appName} by osascript`;
|
||||
} catch {
|
||||
await runCommand("open", ["-a", appName]);
|
||||
return `activated ${appName} by open`;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadMacApp(appName) {
|
||||
const activationDetail = await activateMacApp(appName);
|
||||
const refreshDetail = await sendMacRefreshShortcut();
|
||||
return `${activationDetail}; ${refreshDetail}`;
|
||||
}
|
||||
|
||||
async function sendMacRefreshShortcut() {
|
||||
await runCommand("osascript", [
|
||||
"-e",
|
||||
'tell application "System Events" to key code 15 using command down',
|
||||
]);
|
||||
return "sent Cmd+R";
|
||||
}
|
||||
|
||||
async function openMacThreadDeepLink(deepLink) {
|
||||
await runCommand("open", [deepLink]);
|
||||
return `opened ${deepLink}`;
|
||||
}
|
||||
|
||||
async function activateWindowsApp(appName) {
|
||||
const escaped = String(appName || "Codex").replaceAll("'", "''");
|
||||
await runCommand("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
`$shell = New-Object -ComObject WScript.Shell; if (-not $shell.AppActivate('${escaped}')) { Start-Process '${escaped}' }`,
|
||||
]);
|
||||
return `activated ${appName} by powershell`;
|
||||
}
|
||||
|
||||
async function reloadWindowsApp(appName) {
|
||||
const escaped = String(appName || "Codex").replaceAll("'", "''");
|
||||
await runCommand("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
`$shell = New-Object -ComObject WScript.Shell; if (-not $shell.AppActivate('${escaped}')) { Start-Process '${escaped}'; Start-Sleep -Milliseconds 300 }; $shell.SendKeys('^r')`,
|
||||
]);
|
||||
return `activated ${appName} and sent Ctrl+R`;
|
||||
}
|
||||
|
||||
async function openWindowsThreadDeepLink(deepLink) {
|
||||
const escapedDeepLink = String(deepLink || "").replaceAll("'", "''");
|
||||
await runCommand("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
`Start-Process '${escapedDeepLink}'`,
|
||||
]);
|
||||
return `opened ${deepLink}`;
|
||||
}
|
||||
|
||||
async function sendWindowsRefreshShortcut(appName) {
|
||||
const escaped = String(appName || "Codex").replaceAll("'", "''");
|
||||
await runCommand("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
`$shell = New-Object -ComObject WScript.Shell; if ($shell.AppActivate('${escaped}')) { $shell.SendKeys('^r') }`,
|
||||
]);
|
||||
return "sent Ctrl+R";
|
||||
}
|
||||
|
||||
function buildDryRunDetail(refreshMode, appName, deepLink) {
|
||||
switch (refreshMode) {
|
||||
case "activate":
|
||||
return `dry-run: would activate ${appName}`;
|
||||
case "reload":
|
||||
return `dry-run: would activate ${appName}; would send refresh shortcut`;
|
||||
case "deeplink":
|
||||
return `dry-run: would open ${deepLink}`;
|
||||
case "deeplink-reload":
|
||||
return `dry-run: would open ${deepLink}; would send refresh shortcut`;
|
||||
default:
|
||||
return `dry-run: unsupported mode ${refreshMode}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeCodexDesktopRefreshHint(payload, options = {}) {
|
||||
const env = options.env || process.env;
|
||||
const platform = options.platform || process.platform;
|
||||
const targetThreadRef = String(payload?.targetThreadRef || "").trim();
|
||||
const appName = String(payload?.appName || env.BOSS_CODEX_DESKTOP_APP_NAME || "Codex").trim() || "Codex";
|
||||
const refreshMode = normalizeRefreshMode(
|
||||
payload?.refreshMode || env.BOSS_CODEX_DESKTOP_REFRESH_MODE || "deeplink-reload",
|
||||
);
|
||||
if (payload?.kind !== "codex_desktop_refresh_hint" || !targetThreadRef) {
|
||||
return {
|
||||
status: "failed",
|
||||
targetThreadRef,
|
||||
appName,
|
||||
error: "INVALID_CODEX_DESKTOP_REFRESH_HINT",
|
||||
};
|
||||
}
|
||||
|
||||
if (!refreshMode) {
|
||||
return {
|
||||
status: "failed",
|
||||
targetThreadRef,
|
||||
appName,
|
||||
error: "INVALID_CODEX_DESKTOP_REFRESH_MODE",
|
||||
};
|
||||
}
|
||||
|
||||
if (refreshMode === "off") {
|
||||
return {
|
||||
status: "skipped",
|
||||
targetThreadRef,
|
||||
appName,
|
||||
detail: "refresh hint disabled by mode",
|
||||
};
|
||||
}
|
||||
|
||||
const deepLink = buildCodexThreadDeepLink(targetThreadRef);
|
||||
if (parseBoolean(env.BOSS_CODEX_DESKTOP_REFRESH_DRY_RUN)) {
|
||||
return {
|
||||
status: "completed",
|
||||
targetThreadRef,
|
||||
appName,
|
||||
deepLink,
|
||||
detail: buildDryRunDetail(refreshMode, appName, deepLink),
|
||||
};
|
||||
}
|
||||
|
||||
let detail = "";
|
||||
if (platform === "darwin") {
|
||||
if (refreshMode === "activate") {
|
||||
detail = await activateMacApp(appName);
|
||||
} else if (refreshMode === "reload") {
|
||||
detail = await reloadMacApp(appName);
|
||||
} else if (refreshMode === "deeplink") {
|
||||
detail = await openMacThreadDeepLink(deepLink);
|
||||
} else {
|
||||
const openDetail = await openMacThreadDeepLink(deepLink);
|
||||
await sleep(300);
|
||||
const refreshDetail = await sendMacRefreshShortcut();
|
||||
detail = `${openDetail}; ${refreshDetail}`;
|
||||
}
|
||||
} else if (platform === "win32") {
|
||||
if (refreshMode === "activate") {
|
||||
detail = await activateWindowsApp(appName);
|
||||
} else if (refreshMode === "reload") {
|
||||
detail = await reloadWindowsApp(appName);
|
||||
} else if (refreshMode === "deeplink") {
|
||||
detail = await openWindowsThreadDeepLink(deepLink);
|
||||
} else {
|
||||
const openDetail = await openWindowsThreadDeepLink(deepLink);
|
||||
await sleep(300);
|
||||
const refreshDetail = await sendWindowsRefreshShortcut(appName);
|
||||
detail = `${openDetail}; ${refreshDetail}`;
|
||||
}
|
||||
} else {
|
||||
detail = `platform ${platform} does not support desktop activation`;
|
||||
}
|
||||
return {
|
||||
status: "completed",
|
||||
targetThreadRef,
|
||||
appName,
|
||||
deepLink,
|
||||
detail,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const normalized = normalizePayload(await readStdin());
|
||||
if (!normalized.ok) {
|
||||
writeJson({
|
||||
status: "failed",
|
||||
error: normalized.error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
writeJson(await executeCodexDesktopRefreshHint(normalized.payload));
|
||||
} catch (error) {
|
||||
const payload = normalized.payload;
|
||||
writeJson({
|
||||
status: "failed",
|
||||
targetThreadRef: String(payload?.targetThreadRef || "").trim(),
|
||||
appName: String(payload?.appName || process.env.BOSS_CODEX_DESKTOP_APP_NAME || "Codex").trim() || "Codex",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main();
|
||||
}
|
||||
435
scripts/computer-use-smoke.mjs
Normal file
435
scripts/computer-use-smoke.mjs
Normal file
@@ -0,0 +1,435 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
buildDialogAuditEntry,
|
||||
buildDialogInterventionResult,
|
||||
evaluateDialogSnapshot,
|
||||
readDialogSnapshotFromEnv,
|
||||
} from "../local-agent/desktop-dialog-guard.mjs";
|
||||
|
||||
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_COMPUTER_USE_PAYLOAD: expected object",
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
payload: parsed,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
error: "INVALID_COMPUTER_USE_PAYLOAD: invalid json",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function detectTargetApp(objective) {
|
||||
const text = String(objective || "").toLowerCase();
|
||||
const candidates = [
|
||||
["微信", ["微信", "wechat"]],
|
||||
["飞书", ["飞书", "lark", "feishu"]],
|
||||
["Telegram", ["telegram"]],
|
||||
["QQ", ["qq"]],
|
||||
["Finder", ["finder", "访达"]],
|
||||
["系统设置", ["系统设置", "system settings", "settings"]],
|
||||
["Chrome", ["chrome", "谷歌浏览器"]],
|
||||
["Safari", ["safari"]],
|
||||
];
|
||||
|
||||
for (const [label, aliases] of candidates) {
|
||||
if (aliases.some((alias) => text.includes(alias.toLowerCase()))) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function detectDesktopAction(objective) {
|
||||
const text = String(objective || "").toLowerCase();
|
||||
if (text.includes("系统设置") || text.includes("settings")) {
|
||||
return "open_settings";
|
||||
}
|
||||
if (text.includes("访达") || text.includes("finder")) {
|
||||
return "open_finder";
|
||||
}
|
||||
if (text.includes("微信") || text.includes("wechat")) {
|
||||
return "open_wechat";
|
||||
}
|
||||
if (text.includes("飞书") || text.includes("lark") || text.includes("feishu")) {
|
||||
return "open_feishu";
|
||||
}
|
||||
if (text.includes("telegram")) {
|
||||
return "open_telegram";
|
||||
}
|
||||
if (text.includes("qq")) {
|
||||
return "open_qq";
|
||||
}
|
||||
return "open_app";
|
||||
}
|
||||
|
||||
function extractQuotedText(objective) {
|
||||
const text = String(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;
|
||||
}
|
||||
|
||||
function shouldSubmitAfterTyping(objective) {
|
||||
const text = String(objective || "").toLowerCase();
|
||||
return (
|
||||
text.includes("发送") ||
|
||||
text.includes("提交") ||
|
||||
text.includes("回车") ||
|
||||
text.includes("enter") ||
|
||||
text.includes("submit")
|
||||
);
|
||||
}
|
||||
|
||||
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 resolveOpenAppPrefixArgs(command) {
|
||||
const rawJson = String(process.env.BOSS_COMPUTER_USE_OPEN_APP_ARGS_JSON || "").trim();
|
||||
const rawArgs = String(process.env.BOSS_COMPUTER_USE_OPEN_APP_ARGS || "").trim();
|
||||
if (rawJson || rawArgs) {
|
||||
return parseArgsJson(rawJson) ?? parseArgs(rawArgs);
|
||||
}
|
||||
return path.basename(command || "").toLowerCase() === "open" ? ["-a"] : [];
|
||||
}
|
||||
|
||||
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()
|
||||
: `desktop-${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 getDesktopAutomationMode() {
|
||||
const raw = String(process.env.BOSS_COMPUTER_USE_MODE || "").trim().toLowerCase();
|
||||
if (raw === "off" || raw === "open" || raw === "osascript" || raw === "auto") {
|
||||
return raw;
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
function getDialogGuardEnabled() {
|
||||
return String(process.env.BOSS_DIALOG_GUARD_ENABLED || "").trim().toLowerCase() === "true";
|
||||
}
|
||||
|
||||
function readDialogGuardSnapshot() {
|
||||
return readDialogSnapshotFromEnv(process.env, process.env.BOSS_DIALOG_GUARD_PLATFORM || process.platform);
|
||||
}
|
||||
|
||||
function resolveDialogGuardActionCommand(platform) {
|
||||
const normalizedPlatform = String(platform || "").trim();
|
||||
if (normalizedPlatform === "darwin") {
|
||||
const command = String(process.env.BOSS_MAC_DIALOG_GUARD_ACTION_COMMAND || "").trim();
|
||||
if (command) {
|
||||
return {
|
||||
command,
|
||||
args: parseArgsJson(process.env.BOSS_MAC_DIALOG_GUARD_ACTION_ARGS_JSON) ??
|
||||
parseArgs(process.env.BOSS_MAC_DIALOG_GUARD_ACTION_ARGS),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (normalizedPlatform === "win32") {
|
||||
const command = String(process.env.BOSS_WINDOWS_DIALOG_GUARD_ACTION_COMMAND || "").trim();
|
||||
if (command) {
|
||||
return {
|
||||
command,
|
||||
args: parseArgsJson(process.env.BOSS_WINDOWS_DIALOG_GUARD_ACTION_ARGS_JSON) ??
|
||||
parseArgs(process.env.BOSS_WINDOWS_DIALOG_GUARD_ACTION_ARGS),
|
||||
};
|
||||
}
|
||||
}
|
||||
const command = String(process.env.BOSS_DIALOG_GUARD_ACTION_COMMAND || "").trim();
|
||||
if (!command) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
command,
|
||||
args: parseArgsJson(process.env.BOSS_DIALOG_GUARD_ACTION_ARGS_JSON) ??
|
||||
parseArgs(process.env.BOSS_DIALOG_GUARD_ACTION_ARGS),
|
||||
};
|
||||
}
|
||||
|
||||
function buildDialogGuardActionArgs(snapshot, decision) {
|
||||
return [
|
||||
"--platform",
|
||||
snapshot.platform,
|
||||
"--app",
|
||||
snapshot.appName,
|
||||
"--dialog-id",
|
||||
decision.signature?.id || "",
|
||||
"--action",
|
||||
decision.action || "",
|
||||
"--button",
|
||||
decision.button || "",
|
||||
].filter((item) => item !== "");
|
||||
}
|
||||
|
||||
async function applyDialogGuardAutoAction(snapshot, decision) {
|
||||
if (decision?.disposition !== "auto_action") {
|
||||
return false;
|
||||
}
|
||||
const actionCommand = resolveDialogGuardActionCommand(snapshot.platform);
|
||||
if (!actionCommand?.command) {
|
||||
return false;
|
||||
}
|
||||
await runCommand(actionCommand.command, [
|
||||
...(actionCommand.args || []),
|
||||
...buildDialogGuardActionArgs(snapshot, decision),
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function runDialogGuardPreflight(payload) {
|
||||
if (!getDialogGuardEnabled()) {
|
||||
return {};
|
||||
}
|
||||
const snapshot = readDialogGuardSnapshot();
|
||||
if (!snapshot) {
|
||||
return {};
|
||||
}
|
||||
const decision = evaluateDialogSnapshot(snapshot);
|
||||
if (decision.disposition === "needs_user_action") {
|
||||
return {
|
||||
pausedResult: buildDialogInterventionResult({
|
||||
requestId: typeof payload.requestId === "string" ? payload.requestId : undefined,
|
||||
snapshot,
|
||||
decision,
|
||||
}),
|
||||
decision,
|
||||
snapshot,
|
||||
};
|
||||
}
|
||||
const actionApplied = await applyDialogGuardAutoAction(snapshot, decision);
|
||||
const auditEntry = buildDialogAuditEntry({
|
||||
requestId: typeof payload.requestId === "string" ? payload.requestId : undefined,
|
||||
snapshot,
|
||||
decision,
|
||||
});
|
||||
auditEntry.actionApplied = actionApplied;
|
||||
return {
|
||||
decision,
|
||||
snapshot,
|
||||
actionApplied,
|
||||
auditEntry,
|
||||
};
|
||||
}
|
||||
|
||||
function escapeAppleScriptString(value) {
|
||||
return String(value || "")
|
||||
.replaceAll("\\", "\\\\")
|
||||
.replaceAll('"', '\\"');
|
||||
}
|
||||
|
||||
function buildAppleScript(targetApp, objective) {
|
||||
const app = escapeAppleScriptString(targetApp);
|
||||
const script = [
|
||||
`tell application "${app}"`,
|
||||
"activate",
|
||||
"end tell",
|
||||
];
|
||||
const textToType = extractQuotedText(objective);
|
||||
if (textToType) {
|
||||
script.push("delay 0.2");
|
||||
script.push("tell application \"System Events\"");
|
||||
script.push(`keystroke "${escapeAppleScriptString(textToType)}"`);
|
||||
if (shouldSubmitAfterTyping(objective)) {
|
||||
script.push("key code 36");
|
||||
}
|
||||
script.push("end tell");
|
||||
}
|
||||
return script.join("\n");
|
||||
}
|
||||
|
||||
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() || `computer use open exit code ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runAppleScript(targetApp, objective) {
|
||||
if (!targetApp) {
|
||||
return undefined;
|
||||
}
|
||||
const script = buildAppleScript(targetApp, objective);
|
||||
await runCommand("osascript", ["-e", script]);
|
||||
return `osascript activated ${targetApp}`;
|
||||
}
|
||||
|
||||
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 objective =
|
||||
typeof payload.objective === "string" && payload.objective.trim()
|
||||
? payload.objective.trim()
|
||||
: "桌面控制 smoke 链路正常";
|
||||
const targetApp = detectTargetApp(objective);
|
||||
const desktopAction = detectDesktopAction(objective);
|
||||
const riskLevel =
|
||||
typeof payload.context?.riskLevel === "string" && payload.context.riskLevel.trim()
|
||||
? payload.context.riskLevel.trim()
|
||||
: "unknown";
|
||||
const dryRun = payload.context?.dryRun === true;
|
||||
let dialogGuardState = {};
|
||||
try {
|
||||
dialogGuardState = await runDialogGuardPreflight(payload);
|
||||
} catch (error) {
|
||||
writeJson({
|
||||
status: "failed",
|
||||
requestId: typeof payload.requestId === "string" ? payload.requestId : undefined,
|
||||
error: error?.message || "DIALOG_GUARD_FAILED",
|
||||
});
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (dialogGuardState.pausedResult) {
|
||||
writeJson(dialogGuardState.pausedResult);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let action = targetApp ? desktopAction : "computer_use_smoke";
|
||||
const configuredMode = getDesktopAutomationMode();
|
||||
const automationMode =
|
||||
configuredMode === "auto" ? (process.platform === "darwin" ? "osascript" : "open") : configuredMode;
|
||||
if (targetApp && !dryRun) {
|
||||
if (automationMode === "osascript") {
|
||||
await runAppleScript(targetApp, 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]);
|
||||
action = `${desktopAction}_executed`;
|
||||
}
|
||||
}
|
||||
|
||||
const artifacts = await writeArtifact({
|
||||
requestKind: payload.requestKind,
|
||||
requestId: payload.requestId,
|
||||
action,
|
||||
objective,
|
||||
targetApp,
|
||||
typedText: extractQuotedText(objective),
|
||||
dryRun,
|
||||
riskLevel,
|
||||
mode: automationMode,
|
||||
dialogGuard: dialogGuardState.auditEntry,
|
||||
capturedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
writeJson({
|
||||
status: "completed",
|
||||
requestId: typeof payload.requestId === "string" ? payload.requestId : undefined,
|
||||
replyBody: `桌面控制已完成:${objective}`,
|
||||
executionSummary: `${action} completed (risk=${riskLevel}, mode=${automationMode}${
|
||||
dialogGuardState.decision?.disposition ? `, dialogGuard=${dialogGuardState.decision.disposition}` : ""
|
||||
})`,
|
||||
targetApp,
|
||||
typedText: extractQuotedText(objective),
|
||||
dialogGuard: dialogGuardState.decision
|
||||
? {
|
||||
disposition: dialogGuardState.decision.disposition,
|
||||
kind: dialogGuardState.decision.kind,
|
||||
risk: dialogGuardState.decision.risk,
|
||||
action: dialogGuardState.decision.action,
|
||||
button: dialogGuardState.decision.button,
|
||||
actionApplied: dialogGuardState.actionApplied,
|
||||
}
|
||||
: undefined,
|
||||
artifacts,
|
||||
});
|
||||
@@ -3,6 +3,8 @@ set -euo pipefail
|
||||
|
||||
PLIST_SOURCE="/Users/kris/code/boss/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_TARGET="$HOME/Library/LaunchAgents/com.hyzq.boss.codex-desktop-bridge.plist"
|
||||
CONFIG_PATH="${1:-/Users/kris/code/boss/local-agent/config.cloud.json}"
|
||||
|
||||
if [[ "$CONFIG_PATH" != /* ]]; then
|
||||
@@ -15,6 +17,7 @@ if [[ ! -f "$CONFIG_PATH" ]]; then
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/Library/LaunchAgents"
|
||||
cp "$BRIDGE_PLIST_SOURCE" "$BRIDGE_PLIST_TARGET"
|
||||
cp "$PLIST_SOURCE" "$PLIST_TARGET"
|
||||
python3 - <<'PY' "$PLIST_TARGET" "$CONFIG_PATH"
|
||||
from pathlib import Path
|
||||
@@ -26,6 +29,10 @@ text = plist_path.read_text()
|
||||
plist_path.write_text(text.replace("__BOSS_AGENT_CONFIG__", config_path))
|
||||
PY
|
||||
plutil -lint "$PLIST_TARGET" >/dev/null
|
||||
plutil -lint "$BRIDGE_PLIST_TARGET" >/dev/null
|
||||
launchctl unload "$BRIDGE_PLIST_TARGET" >/dev/null 2>&1 || true
|
||||
launchctl unload "$PLIST_TARGET" >/dev/null 2>&1 || true
|
||||
launchctl load "$BRIDGE_PLIST_TARGET"
|
||||
launchctl load "$PLIST_TARGET"
|
||||
echo "Loaded $BRIDGE_PLIST_TARGET"
|
||||
echo "Loaded $PLIST_TARGET with $CONFIG_PATH"
|
||||
|
||||
9
scripts/postgres-state-schema.sql
Normal file
9
scripts/postgres-state-schema.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS boss_state_snapshots (
|
||||
snapshot_key TEXT PRIMARY KEY,
|
||||
state JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS boss_state_snapshots_updated_at_idx
|
||||
ON boss_state_snapshots (updated_at DESC);
|
||||
@@ -93,8 +93,8 @@ async function loginAsAdmin(baseUrl) {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
account: "17600003315",
|
||||
password: "boss123456",
|
||||
account: "krisolo",
|
||||
password: "Admin_yqs_asd.",
|
||||
method: "password",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -164,7 +164,7 @@ try {
|
||||
storagePath: adminAttachmentPath,
|
||||
previewAvailable: false,
|
||||
uploadedAt: timestamp,
|
||||
uploadedBy: "17600003315",
|
||||
uploadedBy: "krisolo",
|
||||
analysisState: "ready_manual",
|
||||
},
|
||||
],
|
||||
@@ -230,7 +230,7 @@ try {
|
||||
await waitForServer(baseUrl, server, () => serverLogs);
|
||||
|
||||
const memberCookie = await login(baseUrl, memberAccount, memberPassword);
|
||||
const adminCookie = await login(baseUrl, "17600003315", "boss123456");
|
||||
const adminCookie = await login(baseUrl, "krisolo", "Admin_yqs_asd.");
|
||||
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append("file", new File([Buffer.from("blocked upload\n")], "blocked.txt", { type: "text/plain" }));
|
||||
|
||||
@@ -29,7 +29,7 @@ process.env.BOSS_RUNTIME_ROOT = rootDir;
|
||||
|
||||
const { getAttachmentStorageConfig, readState, writeState } = jiti(path.join(scriptDir, "..", "src", "lib", "boss-data.ts"));
|
||||
|
||||
const config = await getAttachmentStorageConfig("17600003315");
|
||||
const config = await getAttachmentStorageConfig("krisolo");
|
||||
if (config.mode !== "server_file") {
|
||||
throw new Error(`Expected default storage mode server_file, got ${config.mode}`);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ state.projects[0].messages.unshift({
|
||||
storagePath: "/tmp/demo.txt",
|
||||
previewAvailable: true,
|
||||
uploadedAt: "2026-03-29T00:00:00+08:00",
|
||||
uploadedBy: "17600003315",
|
||||
uploadedBy: "krisolo",
|
||||
analysisState: "not_applicable",
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user