mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 00:52:10 -06:00
feat(cua-computer): verify the managed driver artifact on Windows and Linux node hosts (#123986)
This commit is contained in:
committed by
GitHub
parent
05c64cb39c
commit
a32a7ae9af
@@ -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. |
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { CUA_DRIVER_ARTIFACT_CHECK_ID, registerCuaDriverDoctorChecks } from "./src/doctor.js";
|
||||
@@ -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({
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"',
|
||||
|
||||
Reference in New Issue
Block a user