mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
* feat(browser): unify local Chrome setup across desktop and terminal Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> * feat(browser): unify local Chrome setup across desktop and terminal OpenClaw-Publication: d19e865e-b5a0-4c70-8876-c1662f6e7ef2 Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> * chore(linux): format Chrome setup fixture Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> * feat(browser): keep desktop Chrome setup local and preserve pairing Delegate Windows registration to the shared native management owner, preserve released native bridge compatibility and saved launcher profiles, and integrate serialized desktop setup through isolated local runtimes. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(browser): repair native setup CI contracts Keep Windows installer dependencies acyclic, validate Unicode within the package library target, and remove unused private exports. Require all eight packaged native-host proof cases and update lazy CLI inventory. Apply native Swift formatter diagnostics without changing behavior. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * test(cli): account for plugin-owned browser extension catalog Keep the core-only registration invariant aligned with the Browser plugin owner already exercised by its lazy registration tests. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * test(macos): retain released Chrome bridge request expectation Align the native bridge test with the shipped contract1 request retained by the canonical setup owner, and reject extra legacy payload fields. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * test(tui): give command handler harness a unique export Rename the shared TUI test helper and both consumers to avoid the Gateway placement harness export collision. No alias or guard waiver. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * chore(sdk): allow canonical browser config path resolver Apply the approved single public-export and callable allowance for resolveConfigPath. Preserve canonical pre-config path ownership and all other SDK surface checks. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(browser): preserve desktop setup selection and supported actions Keep native automatic setup selector-free and resolve saved local browser selection through the canonical setup owner before installation. Respect Mac action advertisements and the released legacy install projection in the Apps card, and document the public config-path resolver contract. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(auth): retry model selection after concurrent credential refresh Adopt upstream PR #152426, commit32298b10f6, without changing its five source files. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * test: preserve native setup selection and await dashboard document Match the selector-free native CLI arguments exactly and preserve a saved work-profile result. Wait through the existing document-readiness owner only at the quota test browser-proof boundary, after auth assertions. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(ui): avoid preloading already imported modules Remove exact direct static JavaScript imports from lazy preload tables using the emitted build graph. Preserve HTML, lazy-only JavaScript, CSS, and locale hints. Source-exact CI merge reproduction drops startup gzip from 363283 to 362968 bytes without changing budgets. Add a real emitted-bundle regression. Apply rustfmt layout to the native Chrome selector-free expected arguments. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix: preserve Chrome profiles and attachment follow-up branch binding Let the TUI canonical setup controller retain its saved browser profile and project only a bounded returned name. Align both native first-run fixture expectations with selector-free setup. Join pending chat history before the composer task handoff can expose an admitted attachment to restored-outbox delivery. Preserve idempotency, attachment custody, restored delivery semantics, and all existing assertions and timeouts. Add a deterministic regression reproduced on the exact failed CI merge and its main parent. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * test(state): adopt canonical worker-custody fixture repair Adopt src/plugin-state/plugin-state-worker.test.ts byte-for-byte from upstreambfec65a2a0(#152456). The former fixture held its late competing owner until after awaiting off-thread acquisition. Preserve that overlap, assert continued host authority checks and noncompletion, release custody, then assert the original result and persisted state. No production locking, guard, deadline or outcome assertion is relaxed. Both prior failures reproduced on the exact CI main parent with independently installed frozen dependencies and Node 24.19.0. All 12 repaired file tests, selected state-logging types, scoped typed lint and fresh P0-P2 review passed. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(browser): retain saved Windows setup profiles Recover configured extension profiles through bounded serial read-only C# inspection. Select only independently validated current matching descriptors, confirm the selected generation before effects, and leave the single mutation under the existing C# owner. Preserve POSIX behavior, existing manual relay verification and explicit same-profile repair. Missing descriptors, runtime/origin drift and unknown or changing observations fail closed without automatic mutation. Keep raw management facts private and populate the existing browserProfile field only from validated binding metadata. No ABI, schema, SDK, configuration flag or registry/activation owner change. 29 actual CLI/controller/Windows-adapter boundary cases plus sibling coverage: 94 tests pass. Canonical changed checks, full production build and fresh independent P0-P2 review passed. Actual C# native proof remains separately coordinated. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(browser): preserve saved profiles after POSIX bundle relocation Separate validated native registration ownership from supported origin-migration readiness. Recover the profile only after full private manifest and exact launcher validation; preserve the existing one-slot migration rule and all unsupported-origin, ACL and foreign-host refusals. Fail closed before selector-free installation when the saved selection cannot be proved. Extract the unchanged shared origin helpers into a cohesive sibling to satisfy the existing line-cap guard without waivers. Windows admission, ABI and selector behavior remain unchanged. Actual Linux/Darwin CLI-to-filesystem relocation regressions: 18 failures on original production, all 22 cases repaired. Preserve the private relay key and inode, config, Chrome preferences, work relay19444 and explicit-profile intent. 137 focused tests, eight real POSIX native-host E2E cases, canonical changed checks, full production build and fresh P0-P2 review passed. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(ui): retain input handoff through the shared outbox owner Remove the superseded pending-history no-yield workaround after main introduced foreground submission custody in the shared outbox owner. Restore chat-submit-guard.ts exactly to pinned main cc7 rather than retaining competing timing policies. Keep passive drains fenced while the input task yields. Preserve the retained history regression with explicit MessageChannel admission, no passive send before resume, and the same terminal leaf, idempotency key, attachment bytes and exactly-once assertions after completion. Original composed source fails all five focused cases; the repair passes 67 handoff/attachment cases and 20 real Chromium cases in the canonical secretless network-none runner. Canonical checks, UI build/performance and fresh P0-P2 review pass. No assertion, timeout, origin or proxy-policy weakening. Browser POSIX/Windows repairs remain byte-identical to accepted255f. The failed e40e CI receipts remain preserved; fresh exact-head CI and parent handoff are still required. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(tasks): preserve reads across native event finalization Hand joined event publication to its exact native successor after the native flow and observer publication frame completes. Keep worker settlement and cleanup, reversible claim transfer, current-authority and ABA checks, and post-commit delivery in their existing owners without replaying writes. Cover pre-result and readback finalization, native and reentrant successor chains, rollback, failed publication, delivery, and terminal activity cleanup. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * test(ui): retain rail baseline geometry on readiness failure Keep the exact existing readiness predicate, fixtures, case inventory, assertion and timeout. When the predicate is false, retain synthetic marker identity and numeric geometry so hosted CI can distinguish scroll, visibility and viewport failures. This is diagnostic evidence, not a repair or waiver of the unresolved rail failure. Local rootless browser infrastructure is unavailable; the existing hosted CI lane will verify the reviewed task-publication repair and collect meaningful rail evidence. Canonical changed checks and P0-P2 diagnostic review pass. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * docs(linux): describe saved-profile Chrome setup selection Match the selector-free adapter argument vector and its regression test. Address the fresh P3 review finding without changing runtime behavior. Markdown syntax and diff checks pass; the generic formatter excludes this subtree. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * test(transcripts): join configured startup before cleanup faults Observe and await the real startTranscripts promise through a narrow call-through spy while retaining the configured service entry point and real SQLite/provider work. Bind the await to the existing test lifetime instead of charging startup to the subsequent short active-map poll. Gate provider return after persisted utterance to prove readiness does not settle early; retain both missing/unreadable row injections and all cleanup, private-source, lifecycle-token and summary assertions. Cover real startup rejection explicitly. No production change, timeout increase, retries or broad module/storage mocks. The deterministic ordering boundary fails with the old fire-and-forget readiness and passes with the real promise join. Final 39 tests across 3 files, canonical changed checks and full-owner P0-P2 review pass. This does not recover whether the historical CI startup was late or rejected. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(browser): preserve automatic desktop status inspection Restore read-only Device-page inspection for current and released Mac bridges while keeping installation and verification explicit. Preserve the native filesystem prerequisite proof, split installer repair tests within the existing line cap, and remove the superseded constant export. * fix(browser): preserve registered setup configuration Require canonical setup to match an owned launcher's effective state and config selection before installation or relay access. Preserve equivalent implicit/explicit default selections and the saved launch context. Recheck automatic profile selection before effects and the current manifest before publication through the existing registration owner. Keep manual install and relocation repair contracts unchanged. Cover mismatched configs, legacy selectors, equivalent defaults, selection drift, and actual bootstrap after refused setup. Restore the missing Command import in the existing Unix-only companion CLI test. Focused tests, types, lint, fresh review, clean package build and sealed Mac ARM64 runtime proof pass. The separate historical clock-jump CI failure has bounded replay evidence and remains documented without a speculative fix. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> * fix(browser): keep setup registration types acyclic Move the private registration status contract beside its context policy and point both consumers at that owner. Remove the publication-module back-edge without keeping an unused compatibility export. The full architecture gate, extension production/test types, typed lint and fresh independent review pass. Node's transformed JavaScript is byte-identical for all three affected modules, so the existing runtime proof remains valid. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> * test(linux): handle Chrome setup in desktop sharing fixture Recognize the exact automatic Chrome setup invocation and require its native no-respawn flag. Keep unknown-command rejection, selected-auth validation, process-group ownership and joined teardown assertions unchanged. The original fixture reproduces the CI rejection against the real Linux app. The repaired fixture passes all nine checks against that same binary, with five Chrome setup calls and five node starts and joined stops. Fresh review is clean; production app behavior is unchanged. Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> --------- Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
547 lines
20 KiB
JavaScript
Executable file
547 lines
20 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
import assert from "node:assert/strict";
|
|
import { execFileSync, fork, spawnSync } from "node:child_process";
|
|
// Package proof: relocation, native load dependencies, provenance, and actual
|
|
// JSONL worker readiness. Never admits or opens the operator's live state.
|
|
import { createHash } from "node:crypto";
|
|
import fs from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { createInterface } from "node:readline";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import {
|
|
seedMacNodeWorkerProofState,
|
|
readMacNodeWorkerProofRows,
|
|
} from "./lib/mac-node-worker-proof-state.mjs";
|
|
import { auditMacWorkerPortability } from "./lib/mac-worker-portability.mjs";
|
|
import { runManagedCommand, terminateManagedChild } from "./lib/managed-child-process.mts";
|
|
|
|
const [runtimeArg, expectedInfoPath] = process.argv.slice(2);
|
|
if (!runtimeArg || !expectedInfoPath) {
|
|
throw new Error("Usage: verify-mac-node-worker.mjs <runtime> <expected-build-info.json>");
|
|
}
|
|
const runtime = fs.realpathSync(runtimeArg);
|
|
const node = path.join(runtime, "bin/node");
|
|
const packageRoot = path.join(runtime, "lib/node_modules/openclaw");
|
|
if (fs.existsSync(path.join(packageRoot, "dist/control-ui"))) {
|
|
throw new Error("Private worker must not contain Gateway Control UI assets");
|
|
}
|
|
const expected = JSON.parse(fs.readFileSync(expectedInfoPath, "utf8"));
|
|
const actual = JSON.parse(fs.readFileSync(path.join(packageRoot, "dist/build-info.json"), "utf8"));
|
|
for (const key of ["version", "commit", "builtAt", "buildId"]) {
|
|
if (!expected[key] || expected[key] !== actual[key]) {
|
|
throw new Error(`Private worker build mismatch: ${key}`);
|
|
}
|
|
}
|
|
if (fs.realpathSync(process.execPath) !== node) {
|
|
throw new Error("Worker proof must execute the bundled Node for the requested architecture");
|
|
}
|
|
|
|
const nativeFiles = auditMacWorkerPortability(runtime, node);
|
|
|
|
async function proveServiceChildRuntime(home) {
|
|
const relayPath = path.join(packageRoot, "dist/process/supervisor/service-child-relay.js");
|
|
const anchorPath = path.join(
|
|
packageRoot,
|
|
"dist/process/supervisor/service-child-group-anchor.js",
|
|
);
|
|
assert(fs.existsSync(anchorPath), "Bundled service-child group anchor is missing");
|
|
await new Promise((resolve, reject) => {
|
|
const child = fork(relayPath, [], {
|
|
cwd: home,
|
|
env: { ...process.env, HOME: home, TMPDIR: home },
|
|
execPath: node,
|
|
stdio: ["pipe", "pipe", "pipe", "pipe", "pipe", "ipc"],
|
|
});
|
|
const control = child.stdio[3];
|
|
const lineage = child.stdio[4];
|
|
let controlBuffer = "";
|
|
let output = "";
|
|
let rootSucceeded = false;
|
|
let closed = false;
|
|
let hostSequence = 0;
|
|
let lineageReported = false;
|
|
const timeout = setTimeout(() => {
|
|
child.kill("SIGKILL");
|
|
reject(new Error("Bundled service-child relay proof timed out"));
|
|
}, 20_000);
|
|
const fail = (error) => {
|
|
if (closed) {
|
|
return;
|
|
}
|
|
closed = true;
|
|
clearTimeout(timeout);
|
|
child.kill("SIGKILL");
|
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
};
|
|
child.on("error", fail);
|
|
child.on("message", (message) => {
|
|
if (message?.type === "relay-error") {
|
|
fail(new Error(`Bundled service-child relay failed: ${message.error}`));
|
|
}
|
|
});
|
|
control.on("data", (chunk) => {
|
|
controlBuffer += chunk.toString();
|
|
for (;;) {
|
|
const newline = controlBuffer.indexOf("\n");
|
|
if (newline < 0) {
|
|
break;
|
|
}
|
|
const line = controlBuffer.slice(0, newline);
|
|
controlBuffer = controlBuffer.slice(newline + 1);
|
|
const message = JSON.parse(line);
|
|
if (message.type === "root-result") {
|
|
rootSucceeded = message.code === 0 && message.signal === null;
|
|
} else if (message.type === "closing") {
|
|
control.write(
|
|
`${JSON.stringify({
|
|
type: "closing-ack",
|
|
generation: message.generation,
|
|
sequence: ++hostSequence,
|
|
closingSequence: message.sequence,
|
|
})}\n`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
child.stdout.on("data", (chunk) => {
|
|
output += chunk.toString();
|
|
});
|
|
control.on("error", fail);
|
|
lineage.on("error", fail);
|
|
const reportLineageClosed = () => {
|
|
if (lineageReported) {
|
|
return;
|
|
}
|
|
lineageReported = true;
|
|
control.write(
|
|
`${JSON.stringify({
|
|
type: "lineage-closed",
|
|
generation: "mac-worker-relay-proof",
|
|
sequence: ++hostSequence,
|
|
})}\n`,
|
|
);
|
|
};
|
|
lineage.once("end", reportLineageClosed);
|
|
lineage.once("close", reportLineageClosed);
|
|
child.once("spawn", () => {
|
|
child.send({
|
|
type: "start",
|
|
generation: "mac-worker-relay-proof",
|
|
command: node,
|
|
args: ["-e", 'process.stdout.write("mcp-relay-proof")'],
|
|
cwd: home,
|
|
env: Object.fromEntries(
|
|
Object.entries({ ...process.env, HOME: home, TMPDIR: home }).filter(
|
|
(entry) => entry[1] !== undefined,
|
|
),
|
|
),
|
|
stdinMode: "pipe-closed",
|
|
controlFd: 3,
|
|
lineageFd: 4,
|
|
acknowledgeClosing: true,
|
|
});
|
|
});
|
|
child.once("exit", (code, signal) => {
|
|
if (closed) {
|
|
return;
|
|
}
|
|
closed = true;
|
|
clearTimeout(timeout);
|
|
if (code === 0 && signal === null && rootSucceeded && output === "mcp-relay-proof") {
|
|
resolve();
|
|
} else {
|
|
reject(
|
|
new Error(
|
|
`Bundled service-child relay proof failed (${code}/${signal}): ${JSON.stringify(output)}`,
|
|
),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function proveGitWorkerRuntime(home) {
|
|
const repository = path.join(home, "git-worker-proof");
|
|
fs.mkdirSync(repository);
|
|
for (const args of [
|
|
["init", "-q"],
|
|
["config", "user.email", "proof@openclaw.invalid"],
|
|
["config", "user.name", "OpenClaw Proof"],
|
|
]) {
|
|
execFileSync("git", args, { cwd: repository });
|
|
}
|
|
fs.writeFileSync(path.join(repository, "proof.txt"), "before\n");
|
|
execFileSync("git", ["add", "proof.txt"], { cwd: repository });
|
|
execFileSync("git", ["commit", "-qm", "proof"], { cwd: repository });
|
|
fs.writeFileSync(path.join(repository, "proof.txt"), "after\n");
|
|
const { WorkerTaskPool } = await import(
|
|
pathToFileURL(path.join(packageRoot, "dist/plugin-sdk/process-runtime.js")).href
|
|
);
|
|
const pool = new WorkerTaskPool({
|
|
workerUrl: pathToFileURL(path.join(packageRoot, "dist/infra/git-operation.worker.js")),
|
|
maxWorkers: 1,
|
|
idleTimeoutMs: 1_000,
|
|
});
|
|
try {
|
|
const reply = await pool.run(
|
|
{ type: "checkout.diff", input: { cwd: repository, scope: "uncommitted" } },
|
|
{
|
|
timeoutMs: 30_000,
|
|
onRequest: async (request) => {
|
|
assert.equal(request?.type, "git.batch");
|
|
const replies = request.input.requests.map((operation) => {
|
|
assert(operation.type === "git.text" || operation.type === "git.buffer");
|
|
const result = spawnSync("git", ["-C", operation.input.cwd, ...operation.input.args], {
|
|
env: { ...process.env, ...operation.input.options.env },
|
|
input: operation.input.options.input,
|
|
encoding: null,
|
|
});
|
|
return {
|
|
ok: true,
|
|
value: {
|
|
stdout: new Uint8Array(result.stdout ?? Buffer.alloc(0)),
|
|
stderr: new Uint8Array(result.stderr ?? Buffer.alloc(0)),
|
|
windowsEncoding: null,
|
|
code: result.status,
|
|
signal: result.signal,
|
|
killed: false,
|
|
cleanup: "normal",
|
|
termination: result.signal ? "signal" : "exit",
|
|
timeoutMs: 30_000,
|
|
},
|
|
};
|
|
});
|
|
return { input: replies, timeoutMs: 30_000 };
|
|
},
|
|
},
|
|
);
|
|
assert(reply.ok, `Bundled Git worker failed: ${JSON.stringify(reply.error)}`);
|
|
assert(reply.value.files.some((file) => file.path === "proof.txt"));
|
|
} finally {
|
|
await pool.close();
|
|
}
|
|
}
|
|
|
|
function proveBrowserSetupRuntime(home) {
|
|
const proofHome = path.join(home, "browser-setup");
|
|
const stateDir = path.join(proofHome, "state");
|
|
const configPath = path.join(stateDir, "openclaw.json");
|
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
fs.mkdirSync(path.join(proofHome, "Library/Application Support/Google/Chrome"), {
|
|
recursive: true,
|
|
mode: 0o700,
|
|
});
|
|
const config = JSON.stringify({
|
|
browser: { profiles: { chrome: { driver: "extension", cdpPort: 18999 } } },
|
|
});
|
|
fs.writeFileSync(configPath, config);
|
|
assert(
|
|
!fs.existsSync(path.join(packageRoot, "dist/entry.js")),
|
|
"Private runtime restored the full CLI",
|
|
);
|
|
for (const action of ["inspect", "install", "verify"]) {
|
|
const result = JSON.parse(
|
|
execFileSync(
|
|
node,
|
|
[
|
|
path.join(packageRoot, "dist/extensions/browser/setup-entry.js"),
|
|
"--action",
|
|
action,
|
|
"--wait-ms",
|
|
"1000",
|
|
],
|
|
{
|
|
cwd: proofHome,
|
|
env: {
|
|
HOME: proofHome,
|
|
TMPDIR: proofHome,
|
|
OPENCLAW_STATE_DIR: stateDir,
|
|
OPENCLAW_CONFIG_PATH: configPath,
|
|
OPENCLAW_PROFILE: "mac-browser-proof",
|
|
OPENCLAW_NO_RESPAWN: "1",
|
|
PATH: `${path.dirname(node)}:/usr/bin:/bin:/usr/sbin:/sbin`,
|
|
},
|
|
encoding: "utf8",
|
|
timeout: 60_000,
|
|
},
|
|
),
|
|
);
|
|
assert.equal(result.action, action);
|
|
assert.equal(result.target.platform, "darwin");
|
|
assert.equal(result.target.kind, "local-host");
|
|
assert.equal(result.target.profile, "chrome");
|
|
assert.equal(result.target.relayPort, 18999);
|
|
assert.equal(result.installation.nativeHostRegistered, action !== "inspect");
|
|
assert.notEqual(result.connection.state, "connected");
|
|
assert.equal(fs.readFileSync(configPath, "utf8"), config, "Browser setup rewrote local config");
|
|
}
|
|
}
|
|
|
|
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-worker-proof-")));
|
|
try {
|
|
// Ready manifests do not load lazy native capabilities. Exercise their real
|
|
// package loaders so omitted optional packages and wrong slices fail staging.
|
|
const require = createRequire(path.join(packageRoot, "package.json"));
|
|
// Keep required native mode and bundled module identity in a fresh process;
|
|
// worker readiness below must still use OpenClaw's normal defaults.
|
|
execFileSync(
|
|
node,
|
|
[fileURLToPath(new URL("./verify-mac-node-worker-fs.mjs", import.meta.url)), packageRoot, home],
|
|
{
|
|
cwd: home,
|
|
env: { HOME: home, TMPDIR: home, FS_SAFE_NATIVE_MODE: "require" },
|
|
stdio: "inherit",
|
|
},
|
|
);
|
|
// Browser setup consumes native file operations; prove that prerequisite first.
|
|
// Its plugin-owned entry must survive pruning without reopening the sealed worker CLI.
|
|
proveBrowserSetupRuntime(home);
|
|
const database = new DatabaseSync(":memory:", { allowExtension: true });
|
|
try {
|
|
require("sqlite-vec").load(database);
|
|
assert.equal(
|
|
typeof database.prepare("SELECT vec_version() AS version").get().version,
|
|
"string",
|
|
);
|
|
} finally {
|
|
database.close();
|
|
}
|
|
await new Promise((resolve, reject) => {
|
|
const terminal = require("@lydell/node-pty").spawn(
|
|
"/bin/sh",
|
|
["-c", "printf worker-pty-proof"],
|
|
{
|
|
cwd: home,
|
|
env: { HOME: home, PATH: "/usr/bin:/bin" },
|
|
name: "xterm",
|
|
cols: 80,
|
|
rows: 24,
|
|
},
|
|
);
|
|
let output = "";
|
|
const timeout = setTimeout(() => {
|
|
terminal.kill("SIGKILL");
|
|
reject(new Error("Bundled PTY did not exit"));
|
|
}, 10_000);
|
|
terminal.onData((data) => {
|
|
output += data;
|
|
});
|
|
terminal.onExit(({ exitCode }) => {
|
|
clearTimeout(timeout);
|
|
if (exitCode === 0 && output === "worker-pty-proof") {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Bundled PTY failed (${exitCode}): ${output}`));
|
|
}
|
|
});
|
|
});
|
|
// Browser screenshot normalization reaches this SDK helper, which launches a
|
|
// worker by its declared runtime path rather than through a module import.
|
|
const { resizeToJpeg } = await import(
|
|
pathToFileURL(path.join(packageRoot, "dist/plugin-sdk/media-runtime.js")).href
|
|
);
|
|
const png = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a2ioAAAAASUVORK5CYII=",
|
|
"base64",
|
|
);
|
|
const jpeg = await resizeToJpeg({ buffer: png, maxSide: 1, quality: 80 });
|
|
assert.deepEqual(jpeg.subarray(0, 2), Buffer.from([0xff, 0xd8]));
|
|
// The retained SQLite SDK launches its store host through another declared
|
|
// runtime path. Prove a real create/write/read/close cycle after relocation.
|
|
const sqliteBackendPath = path.join(home, "sqlite-worker-proof-backend.mjs");
|
|
fs.writeFileSync(
|
|
sqliteBackendPath,
|
|
`
|
|
import { DatabaseSync } from "node:sqlite";
|
|
export function createSqliteWorkerBackend(_input, { databasePath }) {
|
|
const database = new DatabaseSync(databasePath);
|
|
return {
|
|
execute(command) {
|
|
if (command.type === "roundTrip") {
|
|
return database.prepare("SELECT ? AS value").get(command.input).value;
|
|
}
|
|
throw new Error("Unexpected SQLite worker proof command");
|
|
},
|
|
close() {
|
|
database.close();
|
|
},
|
|
};
|
|
}
|
|
`,
|
|
);
|
|
const { openSqliteWorkerStore } = await import(
|
|
pathToFileURL(path.join(packageRoot, "dist/plugin-sdk/sqlite-runtime.js")).href
|
|
);
|
|
const sqliteStore = await openSqliteWorkerStore({
|
|
moduleUrl: pathToFileURL(sqliteBackendPath),
|
|
databasePath: path.join(home, "sqlite-worker-proof.sqlite"),
|
|
input: undefined,
|
|
});
|
|
try {
|
|
assert.equal(
|
|
await sqliteStore.execute({ type: "roundTrip", input: "sqlite-worker-proof" }),
|
|
"sqlite-worker-proof",
|
|
);
|
|
} finally {
|
|
await sqliteStore.close();
|
|
}
|
|
// Configured stdio MCP servers and hosted-workspace diffs both reach helpers
|
|
// through runtime descriptors. Execute those relocated process boundaries so
|
|
// a present-but-incomplete closure fails before the app is signed.
|
|
await proveServiceChildRuntime(home);
|
|
await proveGitWorkerRuntime(home);
|
|
for (const { nativeFirst, desktopSharingEnabled } of [false, true].flatMap((nativeFirstEnabled) =>
|
|
[undefined, true, false].map((sharingEnabled) => ({
|
|
nativeFirst: nativeFirstEnabled,
|
|
desktopSharingEnabled: sharingEnabled,
|
|
})),
|
|
)) {
|
|
const appGatedComputer = !nativeFirst;
|
|
const proofHome = path.join(
|
|
home,
|
|
`${nativeFirst ? "native-first" : "absent"}-${desktopSharingEnabled ?? "default"}`,
|
|
);
|
|
const stateDir = path.join(proofHome, "state");
|
|
const databasePath = path.join(stateDir, "state", "openclaw.sqlite");
|
|
fs.mkdirSync(proofHome, { recursive: true });
|
|
// State lifecycle coordination lives outside removable state. Only remove
|
|
// this fresh fixture's exact hashes after the complete worker tree exits.
|
|
const coordinatorHash = createHash("sha256").update(databasePath).digest("hex").slice(0, 8);
|
|
const coordinatorFiles = ["state-lifecycle", "gateway-lifecycle"].flatMap((family) => {
|
|
const file = path.join(
|
|
fs.realpathSync("/tmp"),
|
|
`openclaw-state-locks-${process.getuid()}`,
|
|
`${family}.${coordinatorHash}.lock.sqlite`,
|
|
);
|
|
return [file, `${file}-journal`, `${file}-wal`, `${file}-shm`];
|
|
});
|
|
assert(
|
|
coordinatorFiles.every((file) => !fs.existsSync(file)),
|
|
"Proof coordinator already exists",
|
|
);
|
|
const nativeRows = nativeFirst ? seedMacNodeWorkerProofState(databasePath) : undefined;
|
|
let ready = false;
|
|
let failure;
|
|
let diagnostic = "";
|
|
const exitCode = await runManagedCommand({
|
|
bin: node,
|
|
args: [
|
|
path.join(packageRoot, "dist/mac-node-worker.js"),
|
|
...(nativeFirst ? ["--profile", "mac-worker-proof"] : []),
|
|
"node",
|
|
"worker",
|
|
...(desktopSharingEnabled === undefined
|
|
? []
|
|
: [desktopSharingEnabled ? "--desktop-sharing" : "--no-desktop-sharing"]),
|
|
],
|
|
cwd: proofHome,
|
|
env: {
|
|
HOME: proofHome,
|
|
TMPDIR: proofHome,
|
|
OPENCLAW_STATE_DIR: stateDir,
|
|
OPENCLAW_CONFIG_PATH: path.join(proofHome, "openclaw.json"),
|
|
PATH: `${path.dirname(node)}:/usr/bin:/bin:/usr/sbin:/sbin`,
|
|
OPENCLAW_NODE_EXEC_HOST: "app",
|
|
OPENCLAW_NODE_EXEC_FALLBACK: "0",
|
|
...(appGatedComputer
|
|
? {
|
|
// Match the Mac app's synchronous readiness lease so the proof verifies
|
|
// that the bundled CUA plugin registers its computer-control commands.
|
|
OPENCLAW_CUA_DRIVER_ENDPOINT: JSON.stringify({
|
|
v: 1,
|
|
socketPath: path.join(proofHome, "cua-driver.sock"),
|
|
binaryPath: "/usr/bin/true",
|
|
}),
|
|
}
|
|
: {}),
|
|
// Same launch shape as MacNodeHostWorker: the worker must stay in the owned
|
|
// process group, or requireProcessTreeExit only proves the respawn wrapper died.
|
|
OPENCLAW_NO_RESPAWN: "1",
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
timeoutMs: 300_000,
|
|
requireProcessTreeExit: true,
|
|
onReady(child) {
|
|
const lines = createInterface({ input: child.stdout });
|
|
child.stderr.on("data", (data) => {
|
|
diagnostic = (diagnostic + data.toString()).slice(0, 4000);
|
|
});
|
|
lines.on("line", (line) => {
|
|
let message;
|
|
try {
|
|
message = JSON.parse(line);
|
|
} catch {
|
|
return;
|
|
}
|
|
if (message.type !== "ready") {
|
|
return;
|
|
}
|
|
if (
|
|
message.version !== expected.version ||
|
|
!message.manifest?.commands?.includes("system.run") ||
|
|
!message.manifest?.commands?.includes("system.which") ||
|
|
!message.manifest?.commands?.includes("browser.proxy") ||
|
|
!message.manifest?.commands?.includes("browser.proxy.upload.v1") ||
|
|
!message.manifest?.commands?.includes("mcp.tools.call.v1") ||
|
|
(desktopSharingEnabled !== undefined &&
|
|
message.manifest?.commands?.includes("desktop.stream") !== desktopSharingEnabled) ||
|
|
(appGatedComputer &&
|
|
(!message.manifest?.commands?.includes("screen.snapshot") ||
|
|
!message.manifest?.commands?.includes("computer.act")))
|
|
) {
|
|
failure = new Error(
|
|
`Bundled worker returned an incompatible capability manifest: ${JSON.stringify(message.manifest)}`,
|
|
);
|
|
child.stdin.end('{"type":"stop"}\n');
|
|
return;
|
|
}
|
|
ready = true;
|
|
process.stdout.write(
|
|
`${JSON.stringify({ architecture: process.arch, nativeFirst, desktopSharingEnabled, build: actual, nativeFiles, databasePath, manifest: message.manifest })}\n`,
|
|
);
|
|
if (appGatedComputer) {
|
|
// The readiness lease uses a harmless executable, not a live MCP
|
|
// daemon. Kill this capability probe before provider cleanup tries
|
|
// to contact it; the second lane still proves graceful shutdown.
|
|
terminateManagedChild(child, "SIGKILL");
|
|
} else {
|
|
child.stdin.end('{"type":"stop"}\n');
|
|
}
|
|
});
|
|
child.on("close", () => lines.close());
|
|
child.stdin.on("error", (error) => {
|
|
failure = error;
|
|
terminateManagedChild(child, "SIGKILL");
|
|
});
|
|
},
|
|
});
|
|
for (const file of coordinatorFiles) {
|
|
fs.rmSync(file, { force: true });
|
|
}
|
|
const expectedExitCode = appGatedComputer ? 137 : 0;
|
|
if (
|
|
failure ||
|
|
!ready ||
|
|
exitCode !== expectedExitCode ||
|
|
/failed during register/u.test(diagnostic)
|
|
) {
|
|
throw new Error(
|
|
`Bundled worker proof failed (${exitCode}): ${failure?.message ?? "missing readiness or registration failure"}; ${diagnostic}`,
|
|
{ cause: failure },
|
|
);
|
|
}
|
|
if (nativeRows) {
|
|
const initialized = new DatabaseSync(databasePath, { readOnly: true });
|
|
try {
|
|
assert.deepEqual(readMacNodeWorkerProofRows(initialized), nativeRows);
|
|
} finally {
|
|
initialized.close();
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
fs.rmSync(home, { recursive: true, force: true });
|
|
}
|