feat(cua-computer): verify the managed driver artifact on Windows and Linux node hosts (#123986)

This commit is contained in:
Peter Steinberger
2026-08-14 21:08:13 -07:00
committed by GitHub
parent 05c64cb39c
commit a32a7ae9af
15 changed files with 701 additions and 16 deletions
+17 -3
View File
@@ -19,7 +19,7 @@ Provider selection never falls back per action. Switching providers closes the a
- **macOS fulfiller:** app setting **Allow Computer Control** enabled. It defaults on; an explicit off choice stays off.
- **macOS fulfiller:** choose **Peekaboo** (default) or **CUA**. CUA is selectable only when the pinned driver is present in the signed app bundle; development builds without that artifact show **driver not bundled**.
- **macOS fulfiller:** **Accessibility** and **Screen Recording** granted to OpenClaw. The native Peekaboo path also requires Event Posting access for its CoreGraphics input primitives.
- **Windows/Linux fulfiller:** bundled `cua-computer` plugin enabled. Its package includes the pinned CUA Driver SDK 0.19.3 runtime; no `cua-driver` executable, daemon, or MCP server is configured.
- **Windows/Linux fulfiller:** bundled `cua-computer` plugin enabled on Windows x64/ARM64 or glibc-based Linux x64/ARM64. Its package includes the pinned CUA Driver SDK 0.19.3 runtime; no `cua-driver` executable, daemon, or MCP server is configured.
- The pairing update that includes `computer.act` approved on the gateway.
- A vision-capable agent model.
- Tool policy that exposes `computer`. The default `coding` profile does not. Add `computer` to `tools.alsoAllow`; sandboxed agents also need it in `tools.sandbox.tools.alsoAllow`.
@@ -72,9 +72,17 @@ The bundled `cua-computer` plugin provides an experimental fulfiller for Windows
openclaw plugins enable cua-computer
```
2. Start `openclaw node run` from the interactive desktop session. The plugin creates its configured SDK runtime lazily, then creates one OpenClaw-owned trusted session for the node-host command execution. It closes that session and shuts down the runtime when the command host stops or restarts.
2. Verify the node-local SDK package before starting the node:
3. Add `computer.act` to the Gateway allowlist. This plugin registers `computer.act` as a dangerous plugin node command, so enabling the plugin alone is not enough; the operator must opt in explicitly:
```bash
openclaw doctor --lint --only cua-computer/driver-artifacts
```
OpenClaw checks the SDK package version, the selected OS/CPU package version, regular-file identity, and the pinned SHA-256 digest of the native library and Node runtime. A clean check prints `no findings`. If it reports a `COMPUTER_DRIVER_*` error, reinstall or update OpenClaw on this node host and run the check again. Do not download a standalone `cua-driver` executable or add one to `PATH`; Windows and Linux use the npm-installed in-process SDK.
3. Start `openclaw node run` from the interactive desktop session. The plugin repeats the artifact verification at startup before it imports native code, creates its configured SDK runtime lazily, then creates one OpenClaw-owned trusted session for the node-host command execution. It closes that session and shuts down the runtime when the command host stops or restarts.
4. Add `computer.act` to the Gateway allowlist. This plugin registers `computer.act` as a dangerous plugin node command, so enabling the plugin alone is not enough; the operator must opt in explicitly:
```json5
{
@@ -92,6 +100,8 @@ The plugin calls `CuaDriver.createConfigured`, never bare `create()`. Its author
On Windows and Linux this is a hard replacement of the former 0.10 daemon/MCP integration: OpenClaw does not spawn a CUA process or proxy an MCP client. macOS deliberately uses the app-owned embedded daemon described above so the driver remains in `OpenClaw.app`'s TCC responsibility chain. Neither path falls back to another provider for an individual action.
The accepted driver record lives with the `cua-computer` package and supplies both the npm native-file digests and the macOS archive digest. Updating OpenClaw updates that record and the SDK packages together. There is no independent Windows/Linux driver updater or rollback directory because there is no separate driver installation on those hosts; roll back by installing the previous known-good OpenClaw package, then rerun the focused doctor check before restarting the node.
### Troubleshooting
The `cua-computer` fulfiller surfaces typed error codes in the tool result and node logs. Common ones:
@@ -99,6 +109,10 @@ The `cua-computer` fulfiller surfaces typed error codes in the tool result and n
| Code | Cause | Fix |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `COMPUTER_DRIVER_UNAVAILABLE` | The CUA runtime cannot initialize, the macOS app-owned endpoint is absent, or the desktop permissions/session are unavailable. | On macOS, verify CUA is selected and the bundled driver is ready; on Windows/Linux, run `openclaw node run` inside the interactive desktop session. Reinstall OpenClaw if the pinned runtime is missing. |
| `COMPUTER_DRIVER_PACKAGE_MISSING` | The pinned SDK package, OS/CPU native package, native library, or Node runtime is absent or unreadable. | Reinstall OpenClaw on the node host, rerun `openclaw doctor --lint --only cua-computer/driver-artifacts`, then restart the node. |
| `COMPUTER_DRIVER_VERSION_MISMATCH` | The SDK package or selected native package does not match the accepted 0.19.3 version. | Update or reinstall OpenClaw so both packages come from the same release; rerun the focused doctor check. |
| `COMPUTER_DRIVER_DIGEST_MISMATCH` | A native SDK library or Node runtime is not a regular package file or does not match its pinned SHA-256 digest. | Do not run or replace the file manually. Reinstall OpenClaw, rerun the focused doctor check, then restart the node. |
| `COMPUTER_DRIVER_PLATFORM_UNSUPPORTED` | The node host has no published 0.19.3 native SDK package, such as musl Linux or an unsupported CPU architecture. | Use Windows x64/ARM64 or glibc-based Linux x64/ARM64 for this provider. |
| `COMPUTER_REFUSED_<code>` | The driver refused the action with a structured code such as `background_unavailable`, `background_occluded`, or `foreground_unavailable` (KDE/KWin Wayland). | Bring the target window forward, switch to X11, or use a supported compositor. See the compatibility notes above. |
| `COMPUTER_STALE_FRAME` | The coordinates referenced a screenshot that is no longer current (context compaction, a display geometry change, or a reference-width change). | Take a fresh `screenshot` before the coordinate action. |
| `COMPUTER_STALE_OBSERVATION` | A window or browser reference belongs to an older observation, navigation, execution, or driver generation. | Run `get_window_state` or `get_browser_state` again and retry with the new opaque references. |
+1
View File
@@ -0,0 +1 @@
export { CUA_DRIVER_ARTIFACT_CHECK_ID, registerCuaDriverDoctorChecks } from "./src/doctor.js";
+36 -1
View File
@@ -9,7 +9,16 @@ import type {
OpenClawPluginNodeInvokePolicy,
OpenClawPluginNodeInvokePolicyContext,
} from "openclaw/plugin-sdk/plugin-entry";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const artifactMocks = vi.hoisted(() => ({
verify: vi.fn(),
}));
vi.mock("./src/driver-artifacts.js", () => ({
verifyInstalledCuaDriverArtifacts: artifactMocks.verify,
}));
import plugin from "./index.js";
function validateManifestConfig(value: unknown) {
@@ -24,6 +33,10 @@ function validateManifestConfig(value: unknown) {
}
describe("cua-computer plugin registration", () => {
beforeEach(() => {
artifactMocks.verify.mockReset().mockReturnValue({ ok: true, applicable: false });
});
it("defaults on only for the app-gated macOS provider path", () => {
const manifest = JSON.parse(
fs.readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"),
@@ -74,6 +87,28 @@ describe("cua-computer plugin registration", () => {
]);
});
it("logs the typed artifact diagnostic during plugin startup", () => {
const error = vi.fn();
artifactMocks.verify.mockReturnValue({
ok: false,
code: "COMPUTER_DRIVER_PACKAGE_MISSING",
diagnostic:
"COMPUTER_DRIVER_PACKAGE_MISSING: native package absent. Fix: reinstall OpenClaw.",
fixHint: "Reinstall OpenClaw.",
});
plugin.register({
pluginConfig: {},
logger: { error },
registerNodeHostCommand: () => {},
registerNodeInvokePolicy: () => {},
} as unknown as OpenClawPluginApi);
expect(error).toHaveBeenCalledWith(
"COMPUTER_DRIVER_PACKAGE_MISSING: native package absent. Fix: reinstall OpenClaw.",
);
});
it("forwards an explicitly armed computer action and preserves node refusals", async () => {
const policies: OpenClawPluginNodeInvokePolicy[] = [];
plugin.register({
+7
View File
@@ -1,7 +1,9 @@
import { registerComputerUseProvider } from "openclaw/plugin-sdk/computer-use";
import { buildPluginConfigSchema, definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { z } from "zod";
import { registerCuaDriverDoctorChecks } from "./api.js";
import { createCuaComputerProvider } from "./src/commands.js";
import { verifyInstalledCuaDriverArtifacts } from "./src/driver-artifacts.js";
const CuaComputerConfigSchema = z.strictObject({
// Keep the shipped daemon setting as a named no-op: strict validation accepts
@@ -17,12 +19,17 @@ export default definePluginEntry({
description: "Experimental CUA Driver computer control for macOS, Windows, and Linux node hosts.",
configSchema,
register(api) {
registerCuaDriverDoctorChecks();
const parsed = CuaComputerConfigSchema.safeParse(api.pluginConfig ?? {});
if (!parsed.success) {
throw new Error(
`Invalid cua-computer plugin config: ${parsed.error.issues[0]?.message ?? "invalid config"}`,
);
}
const artifactVerification = verifyInstalledCuaDriverArtifacts();
if (!artifactVerification.ok) {
api.logger?.error(artifactVerification.diagnostic);
}
registerComputerUseProvider(api, createCuaComputerProvider());
// Dangerous plugin command: excluded from default allowlists, and the
// Gateway fails closed when this policy registration is missing.
+29
View File
@@ -11,6 +11,35 @@
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"cuaDriverArtifacts": {
"darwin-universal-binary": {
"archiveSha256": "733e28a3782ac8d325f8fce8b5d97486c1054af755b40dfd086151b34c79377e"
},
"linux-arm64-gnu": {
"files": {
"cua_driver_node_runtime.node": "8347edcfd0a6e842d2e76bd90f465b228f36bf402c0e59da90bfac3fe6004d58",
"libcua_driver_sdk.so": "7263f8980f91585d5a02e0001042b619322bfc5cc56ca9fb626ae56fcccceb2a"
}
},
"linux-x64-gnu": {
"files": {
"cua_driver_node_runtime.node": "52b70432d2eb167e69632246a38d895c9e7aa61618a31638d393ad6838117293",
"libcua_driver_sdk.so": "31c142f5c67443a1fa933160bfa20d93b9914220fb9a47f2884d38df20ab0671"
}
},
"win32-arm64-msvc": {
"files": {
"cua_driver_node_runtime.node": "fe025669d1614b1ac9a82d1b6a331acd15b44caef81e5bda6a0b02e1d9a4b71f",
"cua_driver_sdk.dll": "f1f25699dbdcc05169230b8286800b69a10407abb20effd5b767629fe725f21b"
}
},
"win32-x64-msvc": {
"files": {
"cua_driver_node_runtime.node": "fa9231bfa0c3c9d6deb8ed32d29dd0ef96921b51ea6864c963fd36c99b716b1c",
"cua_driver_sdk.dll": "d7e67baef87fac1a315d86113eeb485fbf8e03e5906797f0bf79d24a990fa38b"
}
}
},
"openclaw": {
"extensions": [
"./index.ts"
@@ -0,0 +1,43 @@
import type { HealthCheck } from "openclaw/plugin-sdk/health";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
verify: vi.fn(),
}));
vi.mock("./driver-artifacts.js", () => ({
verifyInstalledCuaDriverArtifacts: mocks.verify,
}));
import { CUA_DRIVER_ARTIFACT_CHECK_ID, registerCuaDriverDoctorChecks } from "./doctor.js";
describe("CUA Driver doctor check", () => {
beforeEach(() => {
mocks.verify.mockReset();
});
it("returns the typed artifact failure with the operator repair", async () => {
mocks.verify.mockReturnValue({
ok: false,
code: "COMPUTER_DRIVER_VERSION_MISMATCH",
diagnostic: "COMPUTER_DRIVER_VERSION_MISMATCH: expected 0.19.3. Fix: reinstall OpenClaw.",
fixHint: "Reinstall OpenClaw.",
});
let check: HealthCheck | undefined;
registerCuaDriverDoctorChecks({
registerHealthCheck(value) {
check = value;
},
});
expect(check?.id).toBe(CUA_DRIVER_ARTIFACT_CHECK_ID);
await expect(check?.detect({} as never)).resolves.toEqual([
expect.objectContaining({
checkId: CUA_DRIVER_ARTIFACT_CHECK_ID,
severity: "error",
message: expect.stringContaining("COMPUTER_DRIVER_VERSION_MISMATCH"),
fixHint: "Reinstall OpenClaw.",
}),
]);
});
});
+53
View File
@@ -0,0 +1,53 @@
import {
getHealthCheck,
registerHealthCheck as registerPluginHealthCheck,
type HealthCheck,
} from "openclaw/plugin-sdk/health";
import { verifyInstalledCuaDriverArtifacts } from "./driver-artifacts.js";
export const CUA_DRIVER_ARTIFACT_CHECK_ID = "cua-computer/driver-artifacts";
const cuaDriverArtifactCheck: HealthCheck = {
id: CUA_DRIVER_ARTIFACT_CHECK_ID,
kind: "plugin",
description: "Verify the installed Windows/Linux CUA Driver SDK artifact.",
source: "cua-computer",
async detect() {
const verification = verifyInstalledCuaDriverArtifacts();
if (verification.ok) {
return [];
}
return [
{
checkId: CUA_DRIVER_ARTIFACT_CHECK_ID,
severity: "error",
source: "cua-computer",
message: verification.diagnostic,
target: "@trycua/cua-driver",
requirement: "the accepted CUA Driver SDK version and native package digests",
fixHint: verification.fixHint,
},
];
},
};
type CuaDriverDoctorRegistrationHost = {
readonly registerHealthCheck: (check: HealthCheck) => void;
};
const registeredHosts = new WeakSet<(check: HealthCheck) => void>();
export function registerCuaDriverDoctorChecks(host?: CuaDriverDoctorRegistrationHost): void {
const registerHealthCheck = host?.registerHealthCheck ?? registerPluginHealthCheck;
if (registeredHosts.has(registerHealthCheck)) {
return;
}
if (
host === undefined &&
getHealthCheck(CUA_DRIVER_ARTIFACT_CHECK_ID) === cuaDriverArtifactCheck
) {
return;
}
registerHealthCheck(cuaDriverArtifactCheck);
registeredHosts.add(registerHealthCheck);
}
@@ -0,0 +1,244 @@
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
const DRIVER_PACKAGE = "@trycua/cua-driver";
type SupportedArtifactPlatform =
| "linux-arm64-gnu"
| "linux-x64-gnu"
| "win32-arm64-msvc"
| "win32-x64-msvc";
type DriverArtifactRecord = {
files: Record<string, string>;
};
type CuaDriverManifest = {
dependencies?: Record<string, string>;
cuaDriverArtifacts?: Partial<Record<SupportedArtifactPlatform, DriverArtifactRecord>>;
};
type CuaDriverArtifactDiagnosticCode =
| "COMPUTER_DRIVER_DIGEST_MISMATCH"
| "COMPUTER_DRIVER_MANIFEST_INVALID"
| "COMPUTER_DRIVER_PACKAGE_MISSING"
| "COMPUTER_DRIVER_PLATFORM_UNSUPPORTED"
| "COMPUTER_DRIVER_VERSION_MISMATCH";
export type CuaDriverArtifactVerification =
| { ok: true; applicable: false }
| { ok: true; applicable: true; version: string; platformPackage: string }
| {
ok: false;
code: CuaDriverArtifactDiagnosticCode;
diagnostic: string;
fixHint: string;
};
type CuaDriverArtifactInspectionOptions = {
platform: NodeJS.Platform;
arch: string;
linuxLibc?: "gnu" | "musl";
pluginManifestPath: string;
resolvePackageJson: (packageName: string) => string | undefined;
};
function failure(
code: CuaDriverArtifactDiagnosticCode,
message: string,
fixHint: string,
): CuaDriverArtifactVerification {
return { ok: false, code, diagnostic: `${code}: ${message} Fix: ${fixHint}`, fixHint };
}
function resolveArtifactPlatform(
platform: NodeJS.Platform,
arch: string,
linuxLibc: "gnu" | "musl" | undefined,
):
| { kind: "applicable"; key: SupportedArtifactPlatform }
| { kind: "not-applicable" }
| { kind: "unsupported"; host: string } {
if (platform === "linux") {
if (linuxLibc !== "gnu" || (arch !== "arm64" && arch !== "x64")) {
return { kind: "unsupported", host: `${platform}/${arch}/${linuxLibc ?? "unknown-libc"}` };
}
return { kind: "applicable", key: `linux-${arch}-gnu` };
}
if (platform === "win32") {
if (arch !== "arm64" && arch !== "x64") {
return { kind: "unsupported", host: `${platform}/${arch}` };
}
return { kind: "applicable", key: `win32-${arch}-msvc` };
}
return { kind: "not-applicable" };
}
function readJson(pathname: string): unknown {
return JSON.parse(fs.readFileSync(pathname, "utf8"));
}
export function readPackageIdentity(pathname: string): { name?: string; version?: string } {
const value = readJson(pathname);
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {};
}
const record = value as Record<string, unknown>;
return {
name: typeof record.name === "string" ? record.name : undefined,
version: typeof record.version === "string" ? record.version : undefined,
};
}
function isSha256(value: unknown): value is string {
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
}
function loadArtifactRecord(
manifestPath: string,
key: SupportedArtifactPlatform,
): { version: string; artifact: DriverArtifactRecord } | undefined {
const value = readJson(manifestPath) as CuaDriverManifest;
const version = value.dependencies?.[DRIVER_PACKAGE];
const artifact = value.cuaDriverArtifacts?.[key];
if (
typeof version !== "string" ||
!/^\d+\.\d+\.\d+$/u.test(version) ||
!artifact ||
!artifact.files ||
Object.keys(artifact.files).length === 0 ||
Object.entries(artifact.files).some(
([filename, digest]) => path.basename(filename) !== filename || !isSha256(digest),
)
) {
return undefined;
}
return { version, artifact };
}
function hashFile(pathname: string): string {
return createHash("sha256").update(fs.readFileSync(pathname)).digest("hex");
}
export function inspectCuaDriverArtifacts(
options: CuaDriverArtifactInspectionOptions,
): CuaDriverArtifactVerification {
const selected = resolveArtifactPlatform(options.platform, options.arch, options.linuxLibc);
if (selected.kind === "not-applicable") {
return { ok: true, applicable: false };
}
if (selected.kind === "unsupported") {
const fixHint = "Run this node host on Windows x64/ARM64 or glibc-based Linux x64/ARM64.";
return failure(
"COMPUTER_DRIVER_PLATFORM_UNSUPPORTED",
`the pinned CUA Driver SDK has no native package for ${selected.host}.`,
fixHint,
);
}
let accepted: ReturnType<typeof loadArtifactRecord>;
try {
accepted = loadArtifactRecord(options.pluginManifestPath, selected.key);
} catch {
accepted = undefined;
}
if (!accepted) {
const fixHint = "Reinstall OpenClaw from a complete official package.";
return failure(
"COMPUTER_DRIVER_MANIFEST_INVALID",
`the cua-computer artifact record for ${selected.key} is missing or invalid.`,
fixHint,
);
}
const platformPackage = `${DRIVER_PACKAGE}-${selected.key}`;
const sdkManifestPath = options.resolvePackageJson(DRIVER_PACKAGE);
const platformManifestPath = options.resolvePackageJson(platformPackage);
if (!sdkManifestPath || !platformManifestPath) {
const missing = sdkManifestPath ? platformPackage : DRIVER_PACKAGE;
const fixHint = `Reinstall OpenClaw on this node host so ${DRIVER_PACKAGE} ${accepted.version} and its native platform package are installed together.`;
return failure(
"COMPUTER_DRIVER_PACKAGE_MISSING",
`${missing} ${accepted.version} is not installed.`,
fixHint,
);
}
let sdkIdentity: ReturnType<typeof readPackageIdentity>;
let platformIdentity: ReturnType<typeof readPackageIdentity>;
try {
sdkIdentity = readPackageIdentity(sdkManifestPath);
platformIdentity = readPackageIdentity(platformManifestPath);
} catch {
const fixHint =
"Reinstall OpenClaw on this node host; do not repair native package files by hand.";
return failure(
"COMPUTER_DRIVER_PACKAGE_MISSING",
"the resolved CUA Driver package metadata cannot be read.",
fixHint,
);
}
if (
sdkIdentity.name !== DRIVER_PACKAGE ||
platformIdentity.name !== platformPackage ||
sdkIdentity.version !== accepted.version ||
platformIdentity.version !== accepted.version
) {
const observed = `${sdkIdentity.name ?? "unknown"}@${sdkIdentity.version ?? "unknown"} + ${platformIdentity.name ?? "unknown"}@${platformIdentity.version ?? "unknown"}`;
const fixHint = `Reinstall or update OpenClaw on this node host so both CUA Driver packages resolve to ${accepted.version}.`;
return failure(
"COMPUTER_DRIVER_VERSION_MISMATCH",
`expected ${DRIVER_PACKAGE} and ${platformPackage} ${accepted.version}, resolved ${observed}.`,
fixHint,
);
}
const packageDir = path.dirname(platformManifestPath);
for (const [filename, expectedDigest] of Object.entries(accepted.artifact.files).toSorted(
([left], [right]) => left.localeCompare(right),
)) {
const pathname = path.join(packageDir, filename);
let stat: fs.Stats;
try {
stat = fs.lstatSync(pathname);
} catch {
const fixHint = `Reinstall OpenClaw on this node host to restore ${platformPackage} ${accepted.version}.`;
return failure(
"COMPUTER_DRIVER_PACKAGE_MISSING",
`${platformPackage} is missing ${filename}.`,
fixHint,
);
}
if (!stat.isFile() || stat.isSymbolicLink()) {
const fixHint = "Reinstall OpenClaw; the native driver files must be regular package files.";
return failure(
"COMPUTER_DRIVER_DIGEST_MISMATCH",
`${platformPackage}/${filename} is not a regular file.`,
fixHint,
);
}
let actualDigest: string;
try {
actualDigest = hashFile(pathname);
} catch {
const fixHint = `Reinstall OpenClaw on this node host to restore ${platformPackage} ${accepted.version}.`;
return failure(
"COMPUTER_DRIVER_PACKAGE_MISSING",
`${platformPackage}/${filename} cannot be read.`,
fixHint,
);
}
if (actualDigest !== expectedDigest) {
const fixHint =
"Reinstall OpenClaw; do not run or replace the mismatched native package files.";
return failure(
"COMPUTER_DRIVER_DIGEST_MISMATCH",
`${platformPackage}/${filename} does not match the accepted ${accepted.version} digest.`,
fixHint,
);
}
}
return { ok: true, applicable: true, version: accepted.version, platformPackage };
}
@@ -0,0 +1,144 @@
import { createHash } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { inspectCuaDriverArtifacts } from "./driver-artifact-verification.js";
const temporaryDirectories: string[] = [];
function writeJson(pathname: string, value: unknown): void {
fs.writeFileSync(pathname, `${JSON.stringify(value)}\n`, "utf8");
}
function createArtifactFixture(
options: {
platformKey?: "linux-x64-gnu" | "win32-x64-msvc";
sdkVersion?: string;
platformVersion?: string;
omitPlatformPackage?: boolean;
expectedDigest?: string;
} = {},
) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cua-artifacts-"));
temporaryDirectories.push(root);
const platformKey = options.platformKey ?? "linux-x64-gnu";
const acceptedVersion = "0.19.3";
const nativeFile = platformKey.startsWith("linux")
? "libcua_driver_sdk.so"
: "cua_driver_sdk.dll";
const nativeContents = "accepted native artifact";
const expectedDigest =
options.expectedDigest ?? createHash("sha256").update(nativeContents).digest("hex");
const pluginManifestPath = path.join(root, "plugin-package.json");
const sdkManifestPath = path.join(root, "sdk-package.json");
const platformPackageName = `@trycua/cua-driver-${platformKey}`;
const platformDir = path.join(root, "platform");
const platformManifestPath = path.join(platformDir, "package.json");
fs.mkdirSync(platformDir);
writeJson(pluginManifestPath, {
dependencies: { "@trycua/cua-driver": acceptedVersion },
cuaDriverArtifacts: { [platformKey]: { files: { [nativeFile]: expectedDigest } } },
});
writeJson(sdkManifestPath, {
name: "@trycua/cua-driver",
version: options.sdkVersion ?? acceptedVersion,
});
writeJson(platformManifestPath, {
name: platformPackageName,
version: options.platformVersion ?? acceptedVersion,
});
fs.writeFileSync(path.join(platformDir, nativeFile), nativeContents);
const packages = new Map<string, string>([["@trycua/cua-driver", sdkManifestPath]]);
if (!options.omitPlatformPackage) {
packages.set(platformPackageName, platformManifestPath);
}
return {
pluginManifestPath,
resolvePackageJson: (packageName: string) => packages.get(packageName),
};
}
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});
describe("CUA Driver artifact verification", () => {
it("accepts the pinned SDK and native file digest", () => {
const fixture = createArtifactFixture();
expect(
inspectCuaDriverArtifacts({
platform: "linux",
arch: "x64",
linuxLibc: "gnu",
...fixture,
}),
).toEqual({
ok: true,
applicable: true,
version: "0.19.3",
platformPackage: "@trycua/cua-driver-linux-x64-gnu",
});
});
it("reports an actionable typed diagnostic when the native package is absent", () => {
const fixture = createArtifactFixture({ omitPlatformPackage: true });
const result = inspectCuaDriverArtifacts({
platform: "linux",
arch: "x64",
linuxLibc: "gnu",
...fixture,
});
expect(result).toMatchObject({ ok: false, code: "COMPUTER_DRIVER_PACKAGE_MISSING" });
expect(result.ok ? "" : result.diagnostic).toContain("Reinstall OpenClaw on this node host");
});
it("refuses SDK and platform package version skew", () => {
const fixture = createArtifactFixture({ platformVersion: "0.19.2" });
const result = inspectCuaDriverArtifacts({
platform: "linux",
arch: "x64",
linuxLibc: "gnu",
...fixture,
});
expect(result).toMatchObject({ ok: false, code: "COMPUTER_DRIVER_VERSION_MISMATCH" });
expect(result.ok ? "" : result.diagnostic).toContain("resolved @trycua/cua-driver@0.19.3");
});
it("refuses a native file that does not match the accepted digest", () => {
const fixture = createArtifactFixture({ expectedDigest: "0".repeat(64) });
const result = inspectCuaDriverArtifacts({
platform: "linux",
arch: "x64",
linuxLibc: "gnu",
...fixture,
});
expect(result).toMatchObject({ ok: false, code: "COMPUTER_DRIVER_DIGEST_MISMATCH" });
expect(result.ok ? "" : result.diagnostic).toContain("do not run or replace");
});
it("rejects Linux hosts without a published glibc package", () => {
const fixture = createArtifactFixture();
const result = inspectCuaDriverArtifacts({
platform: "linux",
arch: "x64",
linuxLibc: "musl",
...fixture,
});
expect(result).toMatchObject({ ok: false, code: "COMPUTER_DRIVER_PLATFORM_UNSUPPORTED" });
expect(result.ok ? "" : result.diagnostic).toContain("glibc-based Linux");
});
});
@@ -0,0 +1,57 @@
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
inspectCuaDriverArtifacts,
readPackageIdentity,
type CuaDriverArtifactVerification,
} from "./driver-artifact-verification.js";
const PLUGIN_MANIFEST_PATH = fileURLToPath(new URL("../package.json", import.meta.url));
const requireFromPlugin = createRequire(import.meta.url);
function resolvePackageJson(packageName: string): string | undefined {
try {
return requireFromPlugin.resolve(`${packageName}/package.json`);
} catch {}
let entry: string;
try {
entry = requireFromPlugin.resolve(packageName);
} catch {
return undefined;
}
let current = path.dirname(entry);
while (true) {
const candidate = path.join(current, "package.json");
try {
if (readPackageIdentity(candidate).name === packageName) {
return candidate;
}
} catch {}
const parent = path.dirname(current);
if (parent === current) {
return undefined;
}
current = parent;
}
}
function detectLinuxLibc(): "gnu" | "musl" {
const report = process.report?.getReport() as
| { header?: { glibcVersionRuntime?: unknown } }
| undefined;
return typeof report?.header?.glibcVersionRuntime === "string" ? "gnu" : "musl";
}
let installedVerification: CuaDriverArtifactVerification | undefined;
export function verifyInstalledCuaDriverArtifacts(): CuaDriverArtifactVerification {
installedVerification ??= inspectCuaDriverArtifacts({
platform: process.platform,
arch: process.arch,
...(process.platform === "linux" ? { linuxLibc: detectLinuxLibc() } : {}),
pluginManifestPath: PLUGIN_MANIFEST_PATH,
resolvePackageJson,
});
return installedVerification;
}
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto";
import { verifyInstalledCuaDriverArtifacts } from "./driver-artifacts.js";
type DriverClickButton = import("@trycua/cua-driver").ClickButton;
type DriverCaptureScope = import("@trycua/cua-driver").CaptureScope;
@@ -310,10 +311,17 @@ class DirectCuaDriverSession implements CuaDriverSession {
}
async function loadCuaDriverSdk(): Promise<CuaDriverSdk> {
const artifactVerification = verifyInstalledCuaDriverArtifacts();
if (!artifactVerification.ok) {
throw new Error(artifactVerification.diagnostic);
}
return (await import("@trycua/cua-driver")) as CuaDriverSdk;
}
function unavailableError(failure: unknown): Error {
if (failure instanceof Error && /^COMPUTER_DRIVER_[A-Z_]+:/u.test(failure.message)) {
return failure;
}
const detail = failure instanceof Error ? failure.message : String(failure);
return new Error(`COMPUTER_DRIVER_UNAVAILABLE: failed to load CUA Driver SDK: ${detail}`, {
cause: failure,
+3 -2
View File
@@ -2,10 +2,11 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
VERSION="0.19.3"
ARTIFACT_MANIFEST="$ROOT_DIR/extensions/cua-computer/package.json"
VERSION="$(node -e 'const manifest = require(process.argv[1]); process.stdout.write(manifest.dependencies["@trycua/cua-driver"]);' "$ARTIFACT_MANIFEST")"
TAG="cua-driver-rs-v${VERSION}"
ASSET="cua-driver-rs-${VERSION}-darwin-universal-binary.tar.gz"
EXPECTED_SHA256="733e28a3782ac8d325f8fce8b5d97486c1054af755b40dfd086151b34c79377e"
EXPECTED_SHA256="$(node -e 'const manifest = require(process.argv[1]); process.stdout.write(manifest.cuaDriverArtifacts["darwin-universal-binary"].archiveSha256);' "$ARTIFACT_MANIFEST")"
DOWNLOAD_URL="https://github.com/trycua/cua/releases/download/${TAG}/${ASSET}"
CACHE_DIR="$ROOT_DIR/apps/macos/.build/cua-driver/${TAG}"
ARCHIVE="$CACHE_DIR/$ASSET"
+21 -3
View File
@@ -6,10 +6,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { registerBundledHealthChecks } from "./bundled-health-checks.js";
const mocks = vi.hoisted(() => ({
registerCuaDriverDoctorChecks: vi.fn(),
registerPolicyDoctorChecks: vi.fn(),
loadBundledPluginPublicArtifactModuleSync: vi.fn(() => ({
registerPolicyDoctorChecks: mocks.registerPolicyDoctorChecks,
})),
loadBundledPluginPublicArtifactModuleSync: vi.fn(({ dirName }: { dirName: string }) =>
dirName === "cua-computer"
? { registerCuaDriverDoctorChecks: mocks.registerCuaDriverDoctorChecks }
: { registerPolicyDoctorChecks: mocks.registerPolicyDoctorChecks },
),
}));
vi.mock("../plugins/public-surface-loader.js", () => ({
@@ -50,6 +53,21 @@ describe("registerBundledHealthChecks", () => {
});
});
it("loads CUA Driver artifact health when the plugin is enabled", () => {
registerBundledHealthChecks({
cfg: { plugins: { entries: { "cua-computer": { enabled: true } } } },
cwd: workspaceDir,
});
expect(mocks.loadBundledPluginPublicArtifactModuleSync).toHaveBeenCalledWith({
dirName: "cua-computer",
artifactBasename: "api.js",
});
expect(mocks.registerCuaDriverDoctorChecks).toHaveBeenCalledWith({
registerHealthCheck: expect.any(Function),
});
});
it("does not use policy.jsonc existence as extension activation", () => {
writeFileSync(join(workspaceDir, "policy.jsonc"), "{}\n", "utf-8");
+25 -6
View File
@@ -8,18 +8,37 @@ import { registerHealthCheck } from "./health-check-registry.js";
// Bridges bundled plugin doctor checks into the core health registry.
type BundledHealthApi = {
registerCuaDriverDoctorChecks?: (host: {
registerHealthCheck: typeof registerHealthCheck;
}) => void;
registerPolicyDoctorChecks?: (host: { registerHealthCheck: typeof registerHealthCheck }) => void;
};
/** Registers bundled health checks that are explicitly enabled by config and owner policy. */
export function registerBundledHealthChecks(params: { cfg: OpenClawConfig; cwd?: string }): void {
if (!shouldRegisterPolicyHealth(params)) {
return;
if (shouldRegisterPolicyHealth(params)) {
loadBundledPluginPublicArtifactModuleSync<BundledHealthApi>({
dirName: "policy",
artifactBasename: "api.js",
}).registerPolicyDoctorChecks?.({ registerHealthCheck });
}
loadBundledPluginPublicArtifactModuleSync<BundledHealthApi>({
dirName: "policy",
artifactBasename: "api.js",
}).registerPolicyDoctorChecks?.({ registerHealthCheck });
if (shouldRegisterPluginHealth(params.cfg, "cua-computer")) {
loadBundledPluginPublicArtifactModuleSync<BundledHealthApi>({
dirName: "cua-computer",
artifactBasename: "api.js",
}).registerCuaDriverDoctorChecks?.({ registerHealthCheck });
}
}
function shouldRegisterPluginHealth(cfg: OpenClawConfig, pluginId: string): boolean {
const entry = cfg.plugins?.entries?.[pluginId];
if (entry?.enabled !== true) {
return false;
}
return passesManifestOwnerBasePolicy({
plugin: { id: pluginId },
normalizedConfig: normalizePluginsConfig(cfg.plugins),
});
}
function shouldRegisterPolicyHealth(params: { cfg: OpenClawConfig; cwd?: string }): boolean {
+13 -1
View File
@@ -1408,10 +1408,22 @@ describe("package-mac-app plist stamping", () => {
const packageScript = readFileSync(scriptPath, "utf8");
const stageScript = readFileSync("scripts/stage-cua-driver-macos.sh", "utf8");
const codesignScript = readFileSync("scripts/codesign-mac-app.sh", "utf8");
const cuaManifest = JSON.parse(
readFileSync("extensions/cua-computer/package.json", "utf8"),
) as {
dependencies: Record<string, string>;
cuaDriverArtifacts: Record<string, { archiveSha256?: string }>;
};
expect(stageScript).toContain('TAG="cua-driver-rs-v${VERSION}"');
expect(stageScript).toContain(
'EXPECTED_SHA256="733e28a3782ac8d325f8fce8b5d97486c1054af755b40dfd086151b34c79377e"',
'ARTIFACT_MANIFEST="$ROOT_DIR/extensions/cua-computer/package.json"',
);
expect(stageScript).toContain('manifest.dependencies["@trycua/cua-driver"]');
expect(stageScript).toContain('manifest.cuaDriverArtifacts["darwin-universal-binary"]');
expect(cuaManifest.dependencies["@trycua/cua-driver"]).toBe("0.19.3");
expect(cuaManifest.cuaDriverArtifacts["darwin-universal-binary"]?.archiveSha256).toBe(
"733e28a3782ac8d325f8fce8b5d97486c1054af755b40dfd086151b34c79377e",
);
expect(packageScript).toContain(
'"$ROOT_DIR/scripts/stage-cua-driver-macos.sh" "$APP_ROOT/Contents/Resources/cua-driver"',