Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

1079 lines
33 KiB
JavaScript

#!/usr/bin/env bun
/**
* Captures real macOS desktop-control evidence — probes platform capabilities,
* screenshot capture, clipboard, and browser availability against a live headful
* session, then writes a structured evidence manifest. Accessibility/TCC blockers
* are classified as requires-device-evidence rather than failures. Produces the
* macOS validation artifacts consumed by validate-platform-evidence.
*/
import { execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ComputerUseApprovalManager } from "../src/approval-manager.ts";
import { isBrowserAvailable } from "../src/platform/browser.ts";
import { detectPlatformCapabilities } from "../src/platform/capabilities.ts";
import { readClipboard, writeClipboard } from "../src/platform/clipboard.ts";
import { commandExists } from "../src/platform/helpers.ts";
import {
analyzePngScreenshot,
screenshotQualityIssues,
} from "../src/platform/screenshot-quality.ts";
import { ComputerUseService } from "../src/services/computer-use-service.ts";
const CHECK_ORDER = [
"capabilityProbe",
"screenRecordingPermission",
"screenshotCapture",
"accessibilityPermission",
"mouseKeyboardInput",
"windowListFocus",
"browserAutomation",
"clipboardRoundTrip",
"approvalMode",
];
const CHECK_METHODS = {
capabilityProbe: "detectPlatformCapabilities/getCapabilities",
screenRecordingPermission: "screencapture permission gate",
screenshotCapture: "captureDisplay/capturePrimaryDisplay",
accessibilityPermission: "Accessibility/TCC gate",
mouseKeyboardInput: "ComputerUseService desktop actions",
windowListFocus: "listWindows/focusWindow",
browserAutomation: "browser open/navigate/get/screenshot/close",
clipboardRoundTrip: "readClipboard/writeClipboard",
approvalMode: "ComputerUseApprovalManager",
};
const ISSUE = 9581;
const SLUG = "9581-macos-desktop-cua";
const SCRIPT_NAME = "capture-macos-desktop-evidence.mjs";
const here = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(here, "..");
const repoRoot = path.resolve(packageRoot, "../..");
const defaultOutDir = path.join(repoRoot, "test-results/evidence", SLUG);
const approvalConfigPath = path.join(
os.homedir(),
".eliza",
"computer-use-approval.json",
);
function usage() {
return [
`Usage: bun scripts/${SCRIPT_NAME} [--out <dir>] [--skip-browser] [--skip-input]`,
"",
"Captures repeatable macOS desktop computer-use evidence for GitHub issue #9581.",
"The harness writes a report, manifest candidate, screenshots, and README into",
`test-results/evidence/${SLUG}/ by default.`,
].join("\n");
}
function parseArgs(argv) {
const options = {
outDir: defaultOutDir,
skipBrowser: false,
skipInput: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--help" || arg === "-h") {
console.log(usage());
process.exit(0);
}
if (arg === "--out") {
const value = argv[index + 1];
if (!value) throw new Error("--out requires a directory");
options.outDir = path.resolve(value);
index += 1;
continue;
}
if (arg === "--skip-browser") {
options.skipBrowser = true;
continue;
}
if (arg === "--skip-input") {
options.skipInput = true;
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
return options;
}
function runText(command, args, options = {}) {
return execFileSync(command, args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: options.timeout ?? 10_000,
}).trim();
}
function commandText(command, args, fallback = "unknown") {
try {
const value = runText(command, args);
return value.length > 0 ? value : fallback;
} catch {
return fallback;
}
}
function createRuntime(settings = {}) {
return {
character: {},
getSetting(key) {
return settings[key];
},
getService() {
return null;
},
};
}
function relativeToRepo(filePath) {
return path.relative(repoRoot, filePath).replaceAll(path.sep, "/");
}
function pngBufferFromBase64(base64) {
if (typeof base64 !== "string" || base64.length === 0) {
throw new Error("missing PNG base64 payload");
}
return Buffer.from(base64, "base64");
}
function describePng(buffer, label) {
const quality = analyzePngScreenshot(buffer);
const issues = screenshotQualityIssues(label, quality);
if (buffer.length <= 100) {
issues.unshift(`${label}: decoded PNG byte length ${buffer.length} <= 100`);
}
if (issues.length > 0) {
throw new Error(
`${label}: screenshot quality failed: ${issues.join("; ")}; metrics=${JSON.stringify(
{
byteLength: buffer.length,
...quality,
},
)}`,
);
}
return {
byteLength: buffer.length,
...quality,
};
}
function displayForPoint(displays, x, y) {
return (
displays.find((display) => {
const [dx, dy, width, height] = display.bounds;
return x >= dx && x < dx + width && y >= dy && y < dy + height;
}) ??
displays.find((display) => display.primary) ??
displays[0]
);
}
function usableWindow(windowInfo) {
const app = String(windowInfo?.app ?? "").trim();
const title = String(windowInfo?.title ?? "").trim();
const id = String(windowInfo?.id ?? "").trim();
const meaningfulApp = app.length > 0 && app.toLowerCase() !== "unknown";
const meaningfulTitle = title.length > 0 && title.toLowerCase() !== "unknown";
return id.length > 0 && (meaningfulApp || meaningfulTitle);
}
function focusableWindow(windowInfo) {
const app = String(windowInfo?.app ?? "").trim();
return (
usableWindow(windowInfo) &&
app.length > 0 &&
app.toLowerCase() !== "unknown"
);
}
async function waitForPendingApproval(service) {
const startedAt = Date.now();
while (Date.now() - startedAt < 5000) {
const snapshot = service.getApprovalSnapshot();
const approval = snapshot.pendingApprovals[0];
if (approval) return approval.id;
await new Promise((resolve) => setTimeout(resolve, 50));
}
throw new Error("Timed out waiting for a pending approval.");
}
async function preserveApprovalConfig(run) {
const hadConfig = existsSync(approvalConfigPath);
const original = hadConfig ? await readFile(approvalConfigPath, "utf8") : "";
try {
return await run();
} finally {
if (hadConfig) {
await mkdir(path.dirname(approvalConfigPath), { recursive: true });
await writeFile(approvalConfigPath, original, "utf8");
} else {
await rm(approvalConfigPath, { force: true });
}
}
}
function newCheck(id) {
return {
id,
method: CHECK_METHODS[id],
status: "requires_device_evidence",
requiredEvidence: [`${id} was not run`],
};
}
export function isMacosAccessibilityEvidenceBlocker(message) {
const normalized = String(message ?? "").toLowerCase();
return [
"accessibility permission",
"assistive access",
"system events",
"placeholder window metadata",
"could not read textedit bounds",
"listwindows could not resolve the textedit window",
"spawnsync osascript etimedout",
].some((needle) => normalized.includes(needle));
}
function macosAccessibilityEvidence(id, message) {
if (!isMacosAccessibilityEvidenceBlocker(message)) return null;
return {
status: "requires_device_evidence",
requiredEvidence: [
`${id} requires rerun with macOS Accessibility permission granted: ${message}`,
],
};
}
function setCheck(checks, details, id, status, requiredEvidence, extra = {}) {
checks.set(id, {
id,
method: CHECK_METHODS[id],
status,
requiredEvidence,
});
details[id] = {
status,
requiredEvidence,
...extra,
};
}
async function runCheck(checks, details, id, fn, options = {}) {
try {
const result = await fn();
setCheck(
checks,
details,
id,
result.status ?? "passed",
result.requiredEvidence,
result.details ? { details: result.details } : {},
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const classified = options.classifyError?.(message, error);
setCheck(
checks,
details,
id,
classified?.status ?? "failed",
classified?.requiredEvidence ?? [`${id} failed: ${message}`],
{
error:
error instanceof Error
? (error.stack ?? error.message)
: String(error),
},
);
}
}
async function runTextEditInputCheck(service, displays) {
let previousWindow = null;
let createdDocument = false;
const token = `macos-cua-${Date.now()}`;
try {
const activeBefore = await service.executeWindowAction({
action: "get_current_window_id",
});
if (activeBefore?.success && activeBefore.window) {
previousWindow = activeBefore.window;
}
runText(
"osascript",
[
"-e",
'tell application "TextEdit"',
"-e",
"activate",
"-e",
'make new document with properties {text:""}',
"-e",
"end tell",
],
{ timeout: 30_000 },
);
createdDocument = true;
await new Promise((resolve) => setTimeout(resolve, 700));
const active = await service.executeWindowAction({
action: "get_current_window_id",
});
if (!active.success || !active.window) {
throw new Error(
`could not identify active TextEdit window: ${active.error ?? "unknown"}`,
);
}
const boundsResult = await service.executeWindowAction({
action: "get_window_position",
windowId: active.window.id,
});
if (!boundsResult.success || !boundsResult.bounds) {
const rawReason = boundsResult.error ?? "unknown";
const reason = String(rawReason).includes("Window not found")
? `${rawReason}; listWindows could not resolve the TextEdit window. Grant Accessibility permission in System Settings > Privacy & Security > Accessibility, then retry.`
: rawReason;
throw new Error(`could not read TextEdit bounds: ${reason}`);
}
const bounds = boundsResult.bounds;
const globalX = Math.round(bounds.x + bounds.width / 2);
const globalY = Math.round(bounds.y + Math.max(90, bounds.height / 2));
const display = displayForPoint(displays, globalX, globalY);
if (!display)
throw new Error("no display available for TextEdit input target");
const [displayX, displayY] = display.bounds;
const coordinate = [globalX - displayX, globalY - displayY];
const move = await service.executeCommand("mouse_move", {
coordinate,
displayId: display.id,
});
if (!move.success) {
throw new Error(`mouse_move failed: ${move.error ?? "unknown"}`);
}
const click = await service.executeCommand("click", {
coordinate,
displayId: display.id,
});
if (!click.success) {
throw new Error(`click failed: ${click.error ?? "unknown"}`);
}
const selectAll = await service.executeCommand("key_combo", {
key: "cmd+a",
});
if (!selectAll.success) {
throw new Error(`key_combo failed: ${selectAll.error ?? "unknown"}`);
}
const typed = await service.executeCommand("type", {
text: token,
});
if (!typed.success) {
throw new Error(`type failed: ${typed.error ?? "unknown"}`);
}
await new Promise((resolve) => setTimeout(resolve, 300));
// Read the typed text back from the accessibility tree (`value` of the text
// area) rather than `tell application "TextEdit" to get text of front
// document`: directly after synthetic keyboard input TextEdit's Apple Event
// dispatch is briefly blocked and that document read hangs to its timeout,
// whereas the AX value — exactly what a computer-use agent observes on
// screen — is available immediately. The document read is kept as a fallback
// for macOS builds whose TextEdit AX hierarchy differs.
let text = "";
try {
text = runText(
"osascript",
[
"-e",
'tell application "System Events" to tell process "TextEdit" to return value of text area 1 of scroll area 1 of front window',
],
{ timeout: 15_000 },
);
} catch {
text = "";
}
if (!text.includes(token)) {
text = runText(
"osascript",
["-e", 'tell application "TextEdit" to get text of front document'],
{ timeout: 30_000 },
);
}
if (!text.includes(token)) {
throw new Error(`TextEdit document did not contain typed token ${token}`);
}
return {
status: "passed",
requiredEvidence: [
`mouse_move succeeded at ${coordinate.join(",")} on display ${display.id}`,
"click succeeded on a controlled TextEdit document",
"key_combo cmd+a succeeded in the controlled text field",
`type wrote and verified marker ${token}`,
"post-action screenshots were requested by service configuration",
],
details: {
activeWindow: active.window,
bounds,
coordinate,
displayId: display.id,
marker: token,
},
};
} finally {
if (createdDocument) {
// The close Apple Event is blocked by the same transient post-input state
// as the read above; retry with a short delay so the document still closes
// and runs don't accumulate stale windows (which slow TextEdit further).
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
runText(
"osascript",
[
"-e",
'tell application "TextEdit" to close front document saving no',
],
{ timeout: 20_000 },
);
break;
} catch {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
if (previousWindow?.id) {
try {
await service.executeCommand("switch_to_window", {
windowId: previousWindow.id,
});
} catch {
// Best effort focus restore.
}
}
}
}
async function runBrowserCheck(service, outDir, artifacts) {
const pageUrl =
"data:text/html,<html><head><title>macOS CUA Evidence</title></head><body><main><h1>macOS CUA Evidence</h1><button id='go'>Ready</button><input id='field' value=''></main></body></html>";
const open = await service.executeCommand("browser_open", { url: pageUrl });
if (!open.success) {
throw new Error(`browser_open failed: ${open.error ?? "unknown"}`);
}
const dom = await service.executeCommand("browser_get_dom");
if (
!dom.success ||
!String(dom.content ?? "").includes("macOS CUA Evidence")
) {
throw new Error(
`browser_get_dom did not return the evidence page: ${dom.error ?? "unknown"}`,
);
}
const clickables = await service.executeCommand("browser_get_clickables");
if (!clickables.success) {
throw new Error(
`browser_get_clickables failed: ${clickables.error ?? "unknown"}`,
);
}
const screenshot = await service.executeCommand("browser_screenshot");
if (!screenshot.success) {
throw new Error(
`browser_screenshot failed: ${screenshot.error ?? "unknown"}`,
);
}
const browserPng = pngBufferFromBase64(screenshot.screenshot);
const browserQuality = describePng(browserPng, "browser screenshot");
const browserArtifact = path.join(outDir, "browser-evidence.png");
await writeFile(browserArtifact, browserPng);
artifacts.push(relativeToRepo(browserArtifact));
const close = await service.executeCommand("browser_close");
if (!close.success) {
throw new Error(`browser_close failed: ${close.error ?? "unknown"}`);
}
return {
status: "passed",
requiredEvidence: [
`browser target opened ${open.url ?? "data URL"}`,
"browser_get_dom returned the local evidence page",
`browser_get_clickables returned ${clickables.count ?? clickables.elements?.length ?? "some"} element(s)`,
`browser screenshot artifact ${relativeToRepo(browserArtifact)} (${browserQuality.width}x${browserQuality.height})`,
"browser cleanup closed the test browser",
],
details: {
open: {
url: open.url,
title: open.title,
},
browserQuality,
},
};
}
async function runApprovalCheck(outDir, artifacts) {
const approvalPath = path.join(outDir, "approval-full-control.txt");
const smartService = await ComputerUseService.start(
createRuntime({
COMPUTER_USE_APPROVAL_MODE: "smart_approve",
COMPUTER_USE_SCREENSHOT_AFTER_ACTION: "false",
}),
);
try {
const manager = new ComputerUseApprovalManager();
manager.setMode("smart_approve");
const smartReadOnly = manager.shouldAutoApprove("screenshot");
const smartWrite = manager.shouldAutoApprove("file_write");
manager.setMode("full_control");
const fullControl = manager.shouldAutoApprove("file_write");
manager.setMode("approve_all");
const approveAll = manager.shouldAutoApprove("screenshot");
manager.setMode("off");
const offDenied = manager.isDenyAll();
if (
!smartReadOnly ||
smartWrite ||
!fullControl ||
approveAll ||
!offDenied
) {
throw new Error(
"approval manager mode predicates did not match expected policy",
);
}
const pending = smartService.executeCommand("file_write", {
path: approvalPath,
content: "should not be written without approval",
});
const approvalId = await waitForPendingApproval(smartService);
smartService.resolveApproval(
approvalId,
false,
"macOS evidence denial check",
);
const denied = await pending;
if (denied.success) {
throw new Error(
"smart_approve destructive action unexpectedly succeeded without approval",
);
}
smartService.setApprovalMode("full_control");
const fullWrite = await smartService.executeCommand("file_write", {
path: approvalPath,
content: "full_control approval evidence",
});
if (!fullWrite.success) {
throw new Error(
`full_control file_write failed: ${fullWrite.error ?? "unknown"}`,
);
}
artifacts.push(relativeToRepo(approvalPath));
smartService.setApprovalMode("off");
const offResult = await smartService.executeCommand("screenshot");
if (offResult.success) {
throw new Error("off approval mode unexpectedly allowed screenshot");
}
return {
status: "passed",
requiredEvidence: [
"smart_approve auto-approves read-only actions",
"smart_approve queues destructive file_write until approval",
"full_control auto-approves destructive actions",
"approve_all does not auto-approve read-only actions",
"off mode denies actions",
],
details: {
approvalPath: relativeToRepo(approvalPath),
},
};
} finally {
await smartService.stop();
}
}
async function main() {
const options = parseArgs(process.argv.slice(2));
if (process.platform !== "darwin") {
throw new Error(
`macOS desktop evidence must be captured on darwin, got ${process.platform}`,
);
}
await rm(options.outDir, { recursive: true, force: true });
await mkdir(options.outDir, { recursive: true });
const generatedAt = new Date().toISOString();
const machineModel = commandText("sysctl", ["-n", "hw.model"]);
const macosVersion = commandText("sw_vers", ["-productVersion"]);
const buildId = commandText("sw_vers", ["-buildVersion"]);
const gitHead = commandText("git", ["rev-parse", "--short", "HEAD"]);
const artifacts = [];
const details = {};
const checks = new Map(CHECK_ORDER.map((id) => [id, newCheck(id)]));
const service = await ComputerUseService.start(
createRuntime({
COMPUTER_USE_APPROVAL_MODE: "full_control",
COMPUTER_USE_SCREENSHOT_AFTER_ACTION: "true",
COMPUTER_USE_BROWSER_HEADLESS: "false",
}),
);
try {
const capabilities = detectPlatformCapabilities({
osName: "darwin",
commandExists,
isBrowserAvailable,
shell: process.env.SHELL ?? "/bin/bash",
});
const serviceCapabilities = service.getCapabilities();
const displays = service.getDisplays();
await runCheck(checks, details, "capabilityProbe", async () => {
const expectedKeys = [
"screenshot",
"computerUse",
"windowList",
"browser",
"terminal",
"fileSystem",
"clipboard",
];
for (const key of expectedKeys) {
if (
!capabilities[key] ||
typeof capabilities[key].available !== "boolean"
) {
throw new Error(`missing capability ${key}`);
}
}
return {
status: "passed",
requiredEvidence: [
"platform is darwin",
`reported capabilities: ${expectedKeys
.map(
(key) =>
`${key}=${capabilities[key].available ? "available" : "unavailable"}:${capabilities[key].tool}`,
)
.join("; ")}`,
`service capabilities match darwin probe for screenshot=${serviceCapabilities.screenshot.available}, windowList=${serviceCapabilities.windowList.available}`,
],
details: {
capabilities,
serviceCapabilities,
displays,
},
};
});
let screenshotResult = null;
let screenshotQuality = null;
let screenshotArtifact = null;
await runCheck(checks, details, "screenshotCapture", async () => {
screenshotResult = await service.executeCommand("screenshot");
if (!screenshotResult.success) {
throw new Error(screenshotResult.error ?? "screenshot failed");
}
const screenshotPng = pngBufferFromBase64(screenshotResult.screenshot);
screenshotQuality = describePng(
screenshotPng,
"primary display screenshot",
);
screenshotArtifact = path.join(options.outDir, "screenshot-primary.png");
await writeFile(screenshotArtifact, screenshotPng);
artifacts.push(relativeToRepo(screenshotArtifact));
const display =
displays.find(
(candidate) => candidate.id === screenshotResult.displayId,
) ??
displays.find((candidate) => candidate.primary) ??
displays[0];
if (!display) throw new Error("no display metadata returned");
const expectedWidth = Math.round(display.bounds[2] * display.scaleFactor);
const expectedHeight = Math.round(
display.bounds[3] * display.scaleFactor,
);
if (
screenshotQuality.width !== expectedWidth ||
screenshotQuality.height !== expectedHeight
) {
throw new Error(
`screenshot dimensions ${screenshotQuality.width}x${screenshotQuality.height} did not match display ${expectedWidth}x${expectedHeight}`,
);
}
return {
status: "passed",
requiredEvidence: [
`primary display capture returned ${screenshotQuality.byteLength} PNG bytes`,
`captured dimensions ${screenshotQuality.width}x${screenshotQuality.height} match display ${display.id}`,
`screenshot artifact ${relativeToRepo(screenshotArtifact)}`,
],
details: {
display,
screenshotQuality,
},
};
});
await runCheck(checks, details, "screenRecordingPermission", async () => {
if (!screenshotResult?.success) {
throw new Error(
screenshotResult?.permissionDenied
? screenshotResult.error
: "screenshot capture did not succeed",
);
}
return {
status: "passed",
requiredEvidence: [
"granted Screen Recording permission allowed screenshot capture",
"permission-denied screenshot errors are classified by the service when TCC blocks capture",
`captured nonblank artifact ${relativeToRepo(screenshotArtifact)}`,
],
details: {
screenshotQuality,
},
};
});
await runCheck(
checks,
details,
"windowListFocus",
async () => {
const list = await service.executeCommand("list_windows");
if (!list.success || !Array.isArray(list.windows)) {
throw new Error(`list_windows failed: ${list.error ?? "unknown"}`);
}
const usableWindows = list.windows.filter(usableWindow);
if (list.windows.length === 0) {
throw new Error(
"list_windows returned no visible windows; grant Accessibility permission in System Settings > Privacy & Security > Accessibility, then retry",
);
}
if (usableWindows.length === 0) {
throw new Error(
"list_windows returned only placeholder window metadata; grant Accessibility permission in System Settings > Privacy & Security > Accessibility, then retry",
);
}
const focusableWindows = usableWindows.filter(focusableWindow);
if (focusableWindows.length === 0) {
throw new Error(
"list_windows returned windows without focusable application names; grant Accessibility permission in System Settings > Privacy & Security > Accessibility, then retry",
);
}
const active = await service.executeWindowAction({
action: "get_current_window_id",
});
const target =
active?.window && focusableWindow(active.window)
? (focusableWindows.find(
(windowInfo) => windowInfo.id === active.window.id,
) ?? focusableWindows[0])
: focusableWindows[0];
if (!target) throw new Error("no visible window available to focus");
const focus = await service.executeCommand("switch_to_window", {
windowId: target.id,
});
if (!focus.success) {
throw new Error(
`switch_to_window failed: ${focus.error ?? "unknown"}`,
);
}
return {
status: "passed",
requiredEvidence: [
`listWindows returned ${list.windows.length} visible window(s)`,
`focusWindow/switchWindow succeeded for ${target.app ?? "unknown"}:${target.id}`,
"window operation errors retain permission guidance through service failure results",
],
details: {
focusedWindow: target,
sampleWindows: list.windows.slice(0, 5),
},
};
},
{
classifyError: (message) =>
macosAccessibilityEvidence("windowListFocus", message),
},
);
if (options.skipInput) {
setCheck(
checks,
details,
"mouseKeyboardInput",
"requires_device_evidence",
["input proof skipped by --skip-input"],
);
setCheck(
checks,
details,
"accessibilityPermission",
"requires_device_evidence",
["Accessibility proof skipped by --skip-input"],
);
} else {
try {
const inputResult = await runTextEditInputCheck(service, displays);
setCheck(
checks,
details,
"mouseKeyboardInput",
inputResult.status,
inputResult.requiredEvidence,
{ details: inputResult.details },
);
setCheck(
checks,
details,
"accessibilityPermission",
"passed",
[
"granted Accessibility permission allowed TextEdit focus, window bounds, click, key_combo, and type operations",
"missing Accessibility permission is classified by desktop service errors when TCC blocks input or System Events access",
],
{
details: {
activeWindow: inputResult.details.activeWindow,
bounds: inputResult.details.bounds,
},
},
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const inputBlocker = macosAccessibilityEvidence(
"controlled TextEdit input proof",
message,
);
const accessibilityBlocker = macosAccessibilityEvidence(
"Accessibility-gated input/window proof",
message,
);
setCheck(
checks,
details,
"mouseKeyboardInput",
inputBlocker?.status ?? "failed",
inputBlocker?.requiredEvidence ?? [
`controlled TextEdit input proof failed: ${message}`,
],
);
setCheck(
checks,
details,
"accessibilityPermission",
accessibilityBlocker?.status ?? "failed",
accessibilityBlocker?.requiredEvidence ?? [
`Accessibility-gated input/window proof failed: ${message}`,
],
);
}
}
if (options.skipBrowser) {
setCheck(
checks,
details,
"browserAutomation",
"requires_device_evidence",
["browser proof skipped by --skip-browser"],
);
} else if (!capabilities.browser.available) {
setCheck(
checks,
details,
"browserAutomation",
"requires_device_evidence",
[`browser unavailable on this host: ${capabilities.browser.tool}`],
);
} else {
await runCheck(checks, details, "browserAutomation", () =>
runBrowserCheck(service, options.outDir, artifacts),
);
}
await runCheck(checks, details, "clipboardRoundTrip", async () => {
const originalText = await readClipboard();
const token = `macos-clipboard-${Date.now()}`;
try {
await writeClipboard(token);
const read = await readClipboard();
if (read !== token) {
throw new Error("clipboard_read did not return test token");
}
} finally {
await writeClipboard(originalText);
}
return {
status: "passed",
requiredEvidence: [
`pbcopy wrote test marker ${token}`,
"pbpaste read the same test marker",
"large payload cap and command failures remain covered by unit tests",
],
details: {
marker: token,
restoredOriginalClipboard: true,
},
};
});
await runCheck(checks, details, "approvalMode", () =>
runApprovalCheck(options.outDir, artifacts),
);
} finally {
await service.stop();
}
const manifestChecks = CHECK_ORDER.map(
(id) => checks.get(id) ?? newCheck(id),
);
const complete = manifestChecks.every((check) =>
["passed", "blocked_by_platform"].includes(check.status),
);
const failed = manifestChecks.some((check) => check.status === "failed");
const manifestPath = path.join(
options.outDir,
"macos-desktop-validation.json",
);
const reportPath = path.join(options.outDir, "report.json");
const readmePath = path.join(options.outDir, "README.md");
const stableManifestPath = path.join(options.outDir, "manifest.json");
const finalArtifacts = Array.from(
new Set([
...artifacts,
relativeToRepo(manifestPath),
relativeToRepo(reportPath),
relativeToRepo(readmePath),
relativeToRepo(stableManifestPath),
]),
);
const manifest = {
schemaVersion: 1,
platform: "macos-desktop",
status: complete
? "passed"
: failed
? "failed"
: "requires_device_evidence",
target: {
minimumMacos: "macOS 13 or newer",
requiredPermissions: ["Screen Recording", "Accessibility"],
driver: "nutjs preferred; legacy cliclick/AppleScript fallback allowed",
},
evidence: {
machineModel,
macosVersion,
buildId,
validatedAt: generatedAt,
validator: `bun scripts/${SCRIPT_NAME} (${gitHead})`,
artifacts: finalArtifacts,
},
checks: manifestChecks,
};
const report = {
issue: ISSUE,
generatedAt,
gitHead,
host: {
hostname: os.hostname(),
machineModel,
macosVersion,
buildId,
node: process.version,
bun: typeof Bun === "undefined" ? null : Bun.version,
arch: process.arch,
},
options,
manifest,
details,
};
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
await writeFile(
readmePath,
[
`# Issue #${ISSUE} macOS desktop CUA evidence`,
"",
`Generated by \`bun run --cwd plugins/plugin-computeruse capture:macos-desktop-evidence\` at ${generatedAt}.`,
"",
`- Status: \`${manifest.status}\``,
`- Machine: \`${machineModel}\``,
`- macOS: \`${macosVersion} (${buildId})\``,
`- Git: \`${gitHead}\``,
"",
"Artifacts:",
...finalArtifacts.map((artifact) => `- \`${artifact}\``),
"",
"Validate the generated manifest with:",
"",
"```bash",
`bun run --cwd plugins/plugin-computeruse validate:platform-evidence -- ../../${relativeToRepo(manifestPath)} --require-complete`,
"```",
"",
].join("\n"),
"utf8",
);
await copyFile(manifestPath, stableManifestPath);
console.log(
JSON.stringify(
{
status: manifest.status,
outDir: relativeToRepo(options.outDir),
manifest: relativeToRepo(manifestPath),
report: relativeToRepo(reportPath),
checks: Object.fromEntries(
manifestChecks.map((check) => [check.id, check.status]),
),
},
null,
2,
),
);
if (!complete) {
process.exitCode = 1;
}
}
if (import.meta.main) {
await preserveApprovalConfig(main);
}