@frontendxlab/opencode-computer-use 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +162 -35
- package/SECURITY.md +6 -2
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/bin/opencode-computer-use-permissions.js +134 -21
- package/bin/opencode-computer-use-repl.js +25 -3
- package/bin/opencode-computer-use-setup.js +113 -95
- package/docs/NATIVE_VALIDATION.md +102 -0
- package/docs/index.html +577 -42
- package/docs/llms-full.txt +146 -35
- package/docs/llms.txt +8 -2
- package/docs/sitemap.xml +1 -1
- package/docs/webmcp.js +2 -2
- package/lib/backend.js +50 -14
- package/lib/json-line-peer.mjs +3 -0
- package/lib/sandbox.mjs +51 -4
- package/package.json +7 -5
|
@@ -5,8 +5,8 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { applyEdits, modify, parse
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
10
10
|
|
|
11
11
|
const PACKAGE_NAME = "@frontendxlab/opencode-computer-use";
|
|
12
12
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -36,14 +36,9 @@ For OpenCode2 V2, add this to opencode.json or opencode.jsonc:
|
|
|
36
36
|
]
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
For global
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
After changing configuration, restart OpenCode:
|
|
44
|
-
|
|
45
|
-
opencode service restart
|
|
46
|
-
opencode2 service restart
|
|
39
|
+
For global setup, rerun with --global and the appropriate --v1 or --v2 flag.
|
|
40
|
+
For a local checkout, also pass --source /absolute/path/to/opencode-computer-use.
|
|
41
|
+
After changing configuration, close and reopen OpenCode.
|
|
47
42
|
`.trim();
|
|
48
43
|
|
|
49
44
|
function print(text, stream = "stdout") {
|
|
@@ -76,6 +71,12 @@ function parseArguments(argv) {
|
|
|
76
71
|
result.flavor = value === "1" ? "v1" : "v2";
|
|
77
72
|
} else if (argument === "--dry-run") {
|
|
78
73
|
result.dryRun = true;
|
|
74
|
+
} else if (argument === "--source") {
|
|
75
|
+
const value = argv[++index];
|
|
76
|
+
if (!value || value.startsWith("--")) {
|
|
77
|
+
throw new Error("--source requires a local directory, file path, or file URL");
|
|
78
|
+
}
|
|
79
|
+
result.source = value;
|
|
79
80
|
} else if (argument === "--help" || argument === "-h") {
|
|
80
81
|
result.help = true;
|
|
81
82
|
} else if (argument === "--auto") {
|
|
@@ -98,18 +99,19 @@ Options:
|
|
|
98
99
|
--v1 Configure the OpenCode 1 plugin format
|
|
99
100
|
--v2 Configure the OpenCode2 V2 plugin format
|
|
100
101
|
--opencode-version 1|2 Select the OpenCode generation explicitly
|
|
102
|
+
--source PATH_OR_URL Load a local checkout directory or entrypoint file
|
|
101
103
|
--dry-run Show the action without changing files
|
|
102
104
|
--help, -h Show this help
|
|
103
105
|
`;
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
function executableCandidates(command, env = process.env) {
|
|
107
|
-
const
|
|
109
|
+
const pathKey = Object.keys(env).find((key) => key.toUpperCase() === "PATH");
|
|
110
|
+
const pathValue = env.PATH || (pathKey && env[pathKey]) || "";
|
|
108
111
|
const directories = pathValue.split(delimiter).filter(Boolean);
|
|
109
112
|
if (process.platform === "win32") {
|
|
110
113
|
const extensions = (env.PATHEXT || ".EXE;.CMD;.BAT").split(";").filter(Boolean);
|
|
111
114
|
return directories.flatMap((directory) => [
|
|
112
|
-
join(directory, command),
|
|
113
115
|
...extensions.map((extension) => join(directory, `${command}${extension.toLowerCase()}`)),
|
|
114
116
|
...extensions.map((extension) => join(directory, `${command}${extension}`)),
|
|
115
117
|
]);
|
|
@@ -131,10 +133,13 @@ function resolveCommand(command, env = process.env) {
|
|
|
131
133
|
|
|
132
134
|
function probeCommand(command, env = process.env) {
|
|
133
135
|
const executable = resolveCommand(command, env);
|
|
134
|
-
const
|
|
136
|
+
const batchFile = process.platform === "win32" && /\.(?:cmd|bat)$/iu.test(executable);
|
|
137
|
+
// npm's Windows shims need cmd.exe; a quoted fixed command keeps paths with
|
|
138
|
+
// spaces intact. The only argument is the fixed --version probe.
|
|
139
|
+
const result = spawnSync(batchFile ? `"${executable}" --version` : executable, batchFile ? [] : ["--version"], {
|
|
135
140
|
encoding: "utf8",
|
|
136
141
|
env,
|
|
137
|
-
shell:
|
|
142
|
+
shell: batchFile,
|
|
138
143
|
windowsHide: true,
|
|
139
144
|
timeout: 10_000,
|
|
140
145
|
});
|
|
@@ -170,19 +175,32 @@ function packageKey(entry) {
|
|
|
170
175
|
return undefined;
|
|
171
176
|
}
|
|
172
177
|
|
|
173
|
-
function
|
|
174
|
-
return
|
|
178
|
+
function isPackageReference(value) {
|
|
179
|
+
return value === PACKAGE_NAME || (typeof value === "string" && value.startsWith(`${PACKAGE_NAME}@`) && value.length > PACKAGE_NAME.length + 1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function hasPackage(entries, specifier = PACKAGE_NAME) {
|
|
183
|
+
return Array.isArray(entries) && entries.some((entry) => {
|
|
184
|
+
const key = packageKey(entry);
|
|
185
|
+
return key === specifier || (specifier === PACKAGE_NAME && isPackageReference(key));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseConfiguration(text) {
|
|
190
|
+
const errors = [];
|
|
191
|
+
const data = parse(text, errors, { allowTrailingComma: true, allowEmptyContent: true });
|
|
192
|
+
if (errors.length > 0) throw new Error("configuration is not valid JSON or JSONC");
|
|
193
|
+
if (!text.trim()) return {};
|
|
194
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
195
|
+
throw new Error("configuration is not a JSON or JSONC object");
|
|
196
|
+
}
|
|
197
|
+
return data;
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
function configFlavor(text) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
.filter((child) => child.type === "property")
|
|
182
|
-
.map((property) => property.children?.[0]?.value)
|
|
183
|
-
.filter((name) => name === "plugin" || name === "plugins");
|
|
184
|
-
const hasV1 = names.includes("plugin");
|
|
185
|
-
const hasV2 = names.includes("plugins");
|
|
201
|
+
const data = parseConfiguration(text);
|
|
202
|
+
const hasV1 = Object.hasOwn(data, "plugin");
|
|
203
|
+
const hasV2 = Object.hasOwn(data, "plugins");
|
|
186
204
|
if (hasV1 && hasV2) return "ambiguous";
|
|
187
205
|
if (hasV1) return "v1";
|
|
188
206
|
if (hasV2) return "v2";
|
|
@@ -244,40 +262,52 @@ function defaultConfigFile(root, scope, env) {
|
|
|
244
262
|
return join(configDir, "opencode.json");
|
|
245
263
|
}
|
|
246
264
|
|
|
247
|
-
function addEntry(entries, flavor) {
|
|
248
|
-
if (hasPackage(entries)) return { entries, changed: false };
|
|
265
|
+
function addEntry(entries, flavor, specifier) {
|
|
266
|
+
if (hasPackage(entries, specifier)) return { entries, changed: false };
|
|
267
|
+
if (specifier !== PACKAGE_NAME) {
|
|
268
|
+
const index = entries.findIndex((entry) => isPackageReference(packageKey(entry)));
|
|
269
|
+
if (index !== -1) {
|
|
270
|
+
const current = entries[index];
|
|
271
|
+
const replacement = Array.isArray(current)
|
|
272
|
+
? [specifier, ...current.slice(1)]
|
|
273
|
+
: typeof current === "object" ? { ...current, package: specifier } : specifier;
|
|
274
|
+
const updated = [...entries];
|
|
275
|
+
updated[index] = replacement;
|
|
276
|
+
return { entries: updated, changed: true };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
249
279
|
const entry = flavor === "v1"
|
|
250
|
-
? [
|
|
251
|
-
:
|
|
280
|
+
? [specifier, { ...DEFAULT_V1_OPTIONS }]
|
|
281
|
+
: specifier;
|
|
252
282
|
return { entries: [...entries, entry], changed: true };
|
|
253
283
|
}
|
|
254
284
|
|
|
255
|
-
function newConfig(flavor) {
|
|
285
|
+
function newConfig(flavor, specifier) {
|
|
256
286
|
const key = flavor === "v1" ? "plugin" : "plugins";
|
|
257
287
|
const entry = flavor === "v1"
|
|
258
|
-
? [[
|
|
259
|
-
: [
|
|
288
|
+
? [[specifier, { ...DEFAULT_V1_OPTIONS }]]
|
|
289
|
+
: [specifier];
|
|
260
290
|
return {
|
|
261
291
|
$schema: "https://opencode.ai/config.json",
|
|
262
292
|
[key]: entry,
|
|
263
293
|
};
|
|
264
294
|
}
|
|
265
295
|
|
|
266
|
-
export function updateConfigText(text, flavor) {
|
|
267
|
-
const
|
|
268
|
-
if (text.trim() && (parsed === undefined || parsed === null || typeof parsed !== "object" || Array.isArray(parsed))) {
|
|
269
|
-
throw new Error("configuration is not a JSON or JSONC object");
|
|
270
|
-
}
|
|
271
|
-
const data = parsed && typeof parsed === "object" ? parsed : {};
|
|
296
|
+
export function updateConfigText(text, flavor, specifier = PACKAGE_NAME) {
|
|
297
|
+
const data = parseConfiguration(text);
|
|
272
298
|
if (data.plugin !== undefined && data.plugins !== undefined) {
|
|
273
299
|
throw new Error("configuration contains both plugin and plugins fields");
|
|
274
300
|
}
|
|
275
301
|
const key = flavor === "v1" ? "plugin" : "plugins";
|
|
302
|
+
const otherKey = flavor === "v1" ? "plugins" : "plugin";
|
|
303
|
+
if (Object.hasOwn(data, otherKey)) {
|
|
304
|
+
throw new Error(`selected ${flavor} conflicts with the existing ${otherKey} configuration`);
|
|
305
|
+
}
|
|
276
306
|
const current = data[key];
|
|
277
307
|
if (current !== undefined && !Array.isArray(current)) {
|
|
278
308
|
throw new Error(`configuration field "${key}" is not an array`);
|
|
279
309
|
}
|
|
280
|
-
const result = addEntry(current || [], flavor);
|
|
310
|
+
const result = addEntry(current || [], flavor, specifier);
|
|
281
311
|
if (!result.changed) return { text, changed: false };
|
|
282
312
|
const edits = modify(text, [key], result.entries, {
|
|
283
313
|
formattingOptions: {
|
|
@@ -307,20 +337,21 @@ function writeConfig(file, content, dryRun) {
|
|
|
307
337
|
|
|
308
338
|
function validateConfigFiles(root, scope, env) {
|
|
309
339
|
for (const file of existingConfigFiles(root, scope, env)) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
340
|
+
if (!existsSync(file)) continue;
|
|
341
|
+
try {
|
|
342
|
+
parseConfiguration(readFileSync(file, "utf8"));
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw new Error(`${error.message}: ${file}`);
|
|
314
345
|
}
|
|
315
346
|
}
|
|
316
347
|
}
|
|
317
348
|
|
|
318
|
-
function packageAlreadyConfigured(root, scope, env) {
|
|
349
|
+
function packageAlreadyConfigured(root, scope, env, specifier) {
|
|
319
350
|
for (const file of existingConfigFiles(root, scope, env)) {
|
|
320
351
|
try {
|
|
321
|
-
const data =
|
|
352
|
+
const data = parseConfiguration(readFileSync(file, "utf8"));
|
|
322
353
|
const key = data?.plugins !== undefined ? "plugins" : data?.plugin !== undefined ? "plugin" : undefined;
|
|
323
|
-
if (key && hasPackage(data[key])) return file;
|
|
354
|
+
if (key && hasPackage(data[key], specifier)) return file;
|
|
324
355
|
} catch {
|
|
325
356
|
continue;
|
|
326
357
|
}
|
|
@@ -328,31 +359,7 @@ function packageAlreadyConfigured(root, scope, env) {
|
|
|
328
359
|
return undefined;
|
|
329
360
|
}
|
|
330
361
|
|
|
331
|
-
function
|
|
332
|
-
return String(value)
|
|
333
|
-
.replace(/\b(token|secret|password|api[_-]?key|authorization)\s*[:=]\s*(?:bearer|basic)?[^\n\r,;]*/giu, "$1=[redacted]")
|
|
334
|
-
.slice(0, 500);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function runPluginCli(flavor, scope, root, env) {
|
|
338
|
-
const command = flavor === "v1" ? "opencode" : "opencode2";
|
|
339
|
-
const args = ["plugin", "add", PACKAGE_NAME];
|
|
340
|
-
if (flavor === "v1" && scope === "global") args.push("--global");
|
|
341
|
-
const result = spawnSync(resolveCommand(command, env), args, {
|
|
342
|
-
cwd: root,
|
|
343
|
-
encoding: "utf8",
|
|
344
|
-
env,
|
|
345
|
-
shell: process.platform === "win32",
|
|
346
|
-
windowsHide: true,
|
|
347
|
-
timeout: 120_000,
|
|
348
|
-
});
|
|
349
|
-
if (result.error || result.status !== 0) {
|
|
350
|
-
const detail = redactMessage(`${result.stdout || ""}${result.stderr || ""}`.trim());
|
|
351
|
-
throw new Error(`${command} plugin add failed${detail ? `: ${detail}` : ""}`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function configForSetup(root, scope, flavor, env, dryRun) {
|
|
362
|
+
function configForSetup(root, scope, flavor, env, dryRun, specifier) {
|
|
356
363
|
const files = existingConfigFiles(root, scope, env);
|
|
357
364
|
if (files.length > 0) {
|
|
358
365
|
const flavors = files.map((file) => {
|
|
@@ -369,14 +376,19 @@ function configForSetup(root, scope, flavor, env, dryRun) {
|
|
|
369
376
|
throw new Error("multiple conflicting OpenCode configuration files were found");
|
|
370
377
|
}
|
|
371
378
|
}
|
|
372
|
-
const
|
|
379
|
+
const configured = packageAlreadyConfigured(root, scope, env, specifier);
|
|
380
|
+
if (configured) return { file: configured, changed: false };
|
|
381
|
+
// A checkout source replaces an npm registration in its original config,
|
|
382
|
+
// even when another JSON/JSONC config is also present.
|
|
383
|
+
const file = (specifier !== PACKAGE_NAME && packageAlreadyConfigured(root, scope, env, PACKAGE_NAME))
|
|
384
|
+
|| files[0] || defaultConfigFile(root, scope, env);
|
|
373
385
|
if (!existsSync(file)) {
|
|
374
|
-
const content = JSON.stringify(newConfig(flavor), null, 2) + "\n";
|
|
386
|
+
const content = JSON.stringify(newConfig(flavor, specifier), null, 2) + "\n";
|
|
375
387
|
writeConfig(file, content, dryRun);
|
|
376
388
|
return { file, changed: true };
|
|
377
389
|
}
|
|
378
|
-
const result = updateConfigText(readFileSync(file, "utf8"), flavor);
|
|
379
|
-
writeConfig(file, result.text, dryRun);
|
|
390
|
+
const result = updateConfigText(readFileSync(file, "utf8"), flavor, specifier);
|
|
391
|
+
if (result.changed) writeConfig(file, result.text, dryRun);
|
|
380
392
|
return { file, changed: result.changed };
|
|
381
393
|
}
|
|
382
394
|
|
|
@@ -394,8 +406,13 @@ function chooseFlavor(requested, detection, config) {
|
|
|
394
406
|
if (config === "v1" || config === "v2") return config;
|
|
395
407
|
throw new Error("both OpenCode 1 and OpenCode2 are installed, and the configuration is ambiguous");
|
|
396
408
|
}
|
|
397
|
-
if (detection.v1.available)
|
|
398
|
-
|
|
409
|
+
if (detection.v1.available || detection.v2.available) {
|
|
410
|
+
const detected = detection.v1.available ? "v1" : "v2";
|
|
411
|
+
if (config && config !== detected) {
|
|
412
|
+
throw new Error(`detected ${detected} conflicts with the existing ${config} configuration`);
|
|
413
|
+
}
|
|
414
|
+
return detected;
|
|
415
|
+
}
|
|
399
416
|
throw new Error("neither opencode nor opencode2 was found");
|
|
400
417
|
}
|
|
401
418
|
|
|
@@ -405,6 +422,17 @@ function chooseRoot(scope, env) {
|
|
|
405
422
|
return resolve(initial);
|
|
406
423
|
}
|
|
407
424
|
|
|
425
|
+
function sourceSpecifier(source, root) {
|
|
426
|
+
if (source === undefined) return PACKAGE_NAME;
|
|
427
|
+
if (typeof source !== "string" || !source.trim()) throw new Error("--source requires a local path or file URL");
|
|
428
|
+
let path = source.startsWith("file:") ? fileURLToPath(source) : resolve(root, source);
|
|
429
|
+
if (existsSync(path) && statSync(path).isDirectory()) path = join(path, "index.js");
|
|
430
|
+
if (!existsSync(path) || !statSync(path).isFile()) {
|
|
431
|
+
throw new Error(`local plugin entrypoint does not exist: ${path}`);
|
|
432
|
+
}
|
|
433
|
+
return pathToFileURL(realpathSync(path)).href;
|
|
434
|
+
}
|
|
435
|
+
|
|
408
436
|
export async function setup(options = {}, env = process.env, io = print) {
|
|
409
437
|
const requestedScope = options.scope || "auto";
|
|
410
438
|
const requestedFlavor = options.flavor || "auto";
|
|
@@ -413,31 +441,20 @@ export async function setup(options = {}, env = process.env, io = print) {
|
|
|
413
441
|
if (env.OPENCODE_CONFIG_CONTENT) {
|
|
414
442
|
throw new Error("OPENCODE_CONFIG_CONTENT is immutable; add the plugin to that configuration manually");
|
|
415
443
|
}
|
|
444
|
+
validateConfigFiles(root, scope, env);
|
|
445
|
+
const specifier = sourceSpecifier(options.source, root);
|
|
416
446
|
const detection = detectOpenCode(env);
|
|
417
447
|
const config = detectConfigFlavor(root, scope, env);
|
|
418
448
|
const flavor = chooseFlavor(requestedFlavor, detection, config);
|
|
419
449
|
const dryRun = Boolean(options.dryRun);
|
|
420
|
-
|
|
421
|
-
if (scope === "global" && !dryRun) {
|
|
422
|
-
validateConfigFiles(root, scope, env);
|
|
423
|
-
const existing = packageAlreadyConfigured(root, scope, env);
|
|
424
|
-
if (existing) {
|
|
425
|
-
result = { file: existing, changed: false };
|
|
426
|
-
} else {
|
|
427
|
-
runPluginCli(flavor, scope, root, env);
|
|
428
|
-
const configured = packageAlreadyConfigured(root, scope, env);
|
|
429
|
-
if (!configured) throw new Error(`${flavor.toUpperCase()} plugin command did not register the package`);
|
|
430
|
-
result = { file: configured, changed: true };
|
|
431
|
-
}
|
|
432
|
-
} else {
|
|
433
|
-
result = configForSetup(root, scope, flavor, env, dryRun);
|
|
434
|
-
}
|
|
450
|
+
const result = configForSetup(root, scope, flavor, env, dryRun, specifier);
|
|
435
451
|
const action = options.dryRun
|
|
436
452
|
? (result.changed ? "would configure" : "already configured")
|
|
437
453
|
: (result.changed ? "configured" : "already configured");
|
|
438
454
|
io(`OpenCode Computer Use setup ${action} for ${flavor.toUpperCase()} (${scope}).`);
|
|
439
|
-
|
|
440
|
-
|
|
455
|
+
io(`Configuration: ${result.file}`);
|
|
456
|
+
io(`Plugin: ${specifier}`);
|
|
457
|
+
return { ...result, scope, flavor, detection, specifier };
|
|
441
458
|
}
|
|
442
459
|
|
|
443
460
|
export function printHelp() {
|
|
@@ -477,9 +494,10 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
477
494
|
return setup(options, env, io)
|
|
478
495
|
.then(() => 0)
|
|
479
496
|
.catch((error) => {
|
|
480
|
-
|
|
497
|
+
const automatic = env.npm_lifecycle_event === "postinstall";
|
|
498
|
+
io(`[opencode-computer-use] ${automatic ? "Automatic setup was not changed" : "Setup failed"}: ${error instanceof Error ? error.message : String(error)}`, "stderr");
|
|
481
499
|
io(FALLBACK_INSTRUCTIONS, "stderr");
|
|
482
|
-
return 0;
|
|
500
|
+
return automatic ? 0 : 1;
|
|
483
501
|
});
|
|
484
502
|
}
|
|
485
503
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Native Desktop Validation
|
|
2
|
+
|
|
3
|
+
Use this checklist before marking a native operating-system target as live-certified. The headless adapter test suite does not replace these checks.
|
|
4
|
+
|
|
5
|
+
## Target matrix
|
|
6
|
+
|
|
7
|
+
- macOS arm64
|
|
8
|
+
- macOS x64
|
|
9
|
+
- Linux arm64
|
|
10
|
+
- Linux x64
|
|
11
|
+
- Windows arm64
|
|
12
|
+
- Windows x64
|
|
13
|
+
|
|
14
|
+
Run only on a signed-in interactive desktop session. Do not use banking, password-manager, administrator, or other high-value applications for validation.
|
|
15
|
+
|
|
16
|
+
## 1. Runtime and permissions
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm exec --package=@frontendxlab/opencode-computer-use -- opencode-computer-use-permissions --strict
|
|
20
|
+
npm exec --package=open-computer-use@0.3.5 -- open-computer-use list-apps
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Confirm that the native runtime starts successfully and that a disposable test application appears in the app list. Resolve Accessibility, Screen Recording, AT-SPI, UI Automation, capture, or input prerequisites before continuing.
|
|
24
|
+
|
|
25
|
+
## 2. Read-only session
|
|
26
|
+
|
|
27
|
+
Start OpenCode with `safetyMode: "read-only"` and verify:
|
|
28
|
+
|
|
29
|
+
- `cua_repl` connects.
|
|
30
|
+
- `cua.listApps()` returns the disposable test application.
|
|
31
|
+
- `cua.getApp(...)` binds it.
|
|
32
|
+
- `getAXState()` returns bounded state.
|
|
33
|
+
- `getScreenshot()` returns an image when the host supports capture.
|
|
34
|
+
- click, type, key, scroll, drag, value, and secondary actions are rejected by the adapter before native execution.
|
|
35
|
+
|
|
36
|
+
## 3. Controlled mutation
|
|
37
|
+
|
|
38
|
+
Use a disposable editor or test application and restore `safetyMode: "full"`.
|
|
39
|
+
|
|
40
|
+
Verify semantic click, text input, keyboard input, scrolling/drag, and a fresh state read. Coordinate/global-pointer fallback must stay disabled unless explicitly enabled. Do not automatically replay a mutating action after a timeout or transport failure; observe first.
|
|
41
|
+
|
|
42
|
+
## 4. Lifecycle and recovery
|
|
43
|
+
|
|
44
|
+
Verify `js_reset`, CPU-loop timeout recovery, normal shutdown, native-request interruption recovery, and that no orphan native process remains.
|
|
45
|
+
|
|
46
|
+
## 5. Platform-specific checks
|
|
47
|
+
|
|
48
|
+
### macOS
|
|
49
|
+
|
|
50
|
+
- Accessibility permission is granted only to the expected executable.
|
|
51
|
+
- Screen Recording permission is sufficient for screenshots.
|
|
52
|
+
- Intel and Apple Silicon use the expected packaged executable.
|
|
53
|
+
|
|
54
|
+
### Linux
|
|
55
|
+
|
|
56
|
+
- Record desktop environment and session type (X11 or Wayland).
|
|
57
|
+
- Verify AT-SPI exposes actionable elements for the chosen test application.
|
|
58
|
+
- Record the screenshot/input backend used by the native runtime.
|
|
59
|
+
- Treat compositor-blocked synthetic input as unsupported rather than success.
|
|
60
|
+
|
|
61
|
+
### Windows
|
|
62
|
+
|
|
63
|
+
- Run in the interactive user desktop, not a service/session-0 context.
|
|
64
|
+
- Verify UI Automation state and keyboard input.
|
|
65
|
+
- Do not cross a UAC secure desktop.
|
|
66
|
+
- Confirm process cleanup terminates the native process tree.
|
|
67
|
+
|
|
68
|
+
## 6. Certification record
|
|
69
|
+
|
|
70
|
+
For each target record the OS/version, architecture, Node/OpenCode/native-runtime versions, desktop/session type, permission result, app listing, AX state, screenshot, semantic input, lifecycle/recovery result, and known limitations.
|
|
71
|
+
|
|
72
|
+
Only then change public documentation from packaged/supported target to live-certified for that target.
|
|
73
|
+
|
|
74
|
+
## Recorded validation — 2026-10-30, 0.1.5 release candidate
|
|
75
|
+
|
|
76
|
+
Host: Linux x64, Ubuntu/GNOME Wayland, GTK 3.24. Runtime versions: Node
|
|
77
|
+
26.13.0, OpenCode 1.19.3, `open-computer-use` 0.3.5. The OpenCode integration
|
|
78
|
+
also ran from an independently installed npm tarball with isolated settings.
|
|
79
|
+
|
|
80
|
+
| Check | Observed result |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| Adapter regression suite | 70 passed, zero failed on this Linux host. |
|
|
83
|
+
| Syntax/lint and whitespace checks | Passed. |
|
|
84
|
+
| Real OpenCode registration | `cua_repl` connected with options preserved; both fake-backend and native startup modes passed. |
|
|
85
|
+
| Packed installation | Clean tarball install, CLI help, and actual OpenCode fake-backend integration passed. |
|
|
86
|
+
| Native app inventory | Eight running apps returned before opening the disposable fixture. |
|
|
87
|
+
| Persistent JavaScript and reset | A variable survived a second call; `js_reset` succeeded. |
|
|
88
|
+
| Disposable GTK accessibility tree | The test window exposed its counter, text entry, and named button. |
|
|
89
|
+
| Element-index click | Counter changed from 0 to 1, confirmed by fresh state and the fixture's event log. |
|
|
90
|
+
| `setValue` | Text field changed to the test string, confirmed by fresh state and the fixture's event log. |
|
|
91
|
+
| Screenshot | Unavailable for the GTK window in this Wayland session; the adapter returned an explicit error. |
|
|
92
|
+
| Coordinate input, keyboard input, drag, focus recovery | Not validated in this run. |
|
|
93
|
+
| macOS/Windows live desktops | Not available for this run; native behavior remains unverified. |
|
|
94
|
+
| Real OpenCode2 installation | Not exercised; adapter contract coverage only. |
|
|
95
|
+
|
|
96
|
+
The fixture was a disposable local GTK window with no external action or data.
|
|
97
|
+
Mutating checks allowed access only to that fixture, and global-pointer
|
|
98
|
+
fallback remained off. ATK diagnostic warnings occurred during native tree
|
|
99
|
+
inspection; the checked semantic actions still completed. This record supports
|
|
100
|
+
the specific checks above, not universal Linux desktop certification or
|
|
101
|
+
screenshot-based automation. Remote CI jobs must be checked on the release
|
|
102
|
+
commit; their configuration alone is not evidence of a passed run.
|