@myagentroam/node 0.9.94 → 0.9.96

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.
@@ -0,0 +1 @@
1
+ export const computerUseRuntimeVersion = '0.9.95';
@@ -0,0 +1,330 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { constants } from 'node:fs';
4
+ import { access, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
5
+ import { createRequire } from 'node:module';
6
+ import { dirname, relative, resolve, sep } from 'node:path';
7
+ import { promisify } from 'node:util';
8
+ import { z } from 'zod';
9
+ import { computerUseRuntimeExecutable, nodeDataDirectory } from './config.js';
10
+ import { computerUseRuntimeVersion } from './computer-use-runtime-version.js';
11
+ const manifestSchema = z
12
+ .object({
13
+ formatVersion: z.literal(1),
14
+ runtimeVersion: z.string().trim().min(1).max(128),
15
+ platform: z.enum(['win32', 'darwin', 'linux']),
16
+ architecture: z.enum(['x64', 'arm64']),
17
+ executable: z.string().trim().min(1).max(512),
18
+ files: z
19
+ .array(z
20
+ .object({
21
+ path: z.string().trim().min(1).max(512),
22
+ size: z.number().int().nonnegative(),
23
+ sha256: z.string().regex(/^[a-f0-9]{64}$/u)
24
+ })
25
+ .strict())
26
+ .min(1)
27
+ })
28
+ .strict();
29
+ // Only Windows x64 and Linux x64 are published initially because maintainers do not
30
+ // currently have native build environments for the other platform/architecture targets.
31
+ const runtimePackages = {
32
+ 'win32-x64': {
33
+ name: '@myagentroam/computer-runtime-win32-x64',
34
+ version: computerUseRuntimeVersion
35
+ },
36
+ 'linux-x64': {
37
+ name: '@myagentroam/computer-runtime-linux-x64',
38
+ version: computerUseRuntimeVersion
39
+ }
40
+ };
41
+ const execFileAsync = promisify(execFile);
42
+ const installLockTimeoutMs = 620_000;
43
+ const installLockPollMs = 100;
44
+ export class ComputerUseRuntimeError extends Error {
45
+ code;
46
+ constructor(code, options = {}) {
47
+ super(code, options);
48
+ this.code = code;
49
+ this.name = 'ComputerUseRuntimeError';
50
+ }
51
+ }
52
+ export class ManagedComputerUseRuntime {
53
+ config;
54
+ configPath;
55
+ platform;
56
+ architecture;
57
+ installRuntimePackage;
58
+ #ensurePromise;
59
+ constructor(config, configPath, platform = process.platform, architecture = process.arch, installRuntimePackage = defaultRuntimePackageInstaller) {
60
+ this.config = config;
61
+ this.configPath = configPath;
62
+ this.platform = platform;
63
+ this.architecture = architecture;
64
+ this.installRuntimePackage = installRuntimePackage;
65
+ }
66
+ ensure() {
67
+ this.#ensurePromise ??= this.#ensure().finally(() => {
68
+ this.#ensurePromise = undefined;
69
+ });
70
+ return this.#ensurePromise;
71
+ }
72
+ async #ensure() {
73
+ const config = this.config();
74
+ if (config === undefined)
75
+ throw new ComputerUseRuntimeError('NODE_CONFIG_UNAVAILABLE');
76
+ if (config.computerUse?.runtimeExecutable !== undefined) {
77
+ const executable = computerUseRuntimeExecutable(config, this.configPath, this.platform);
78
+ if (executable === undefined)
79
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MISSING');
80
+ await access(executable).catch((cause) => {
81
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MISSING', { cause });
82
+ });
83
+ return executable;
84
+ }
85
+ const runtimePackage = runtimePackages[`${this.platform}-${this.architecture}`];
86
+ if (runtimePackage === undefined)
87
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_PLATFORM_UNSUPPORTED');
88
+ const runtimeRoot = resolve(nodeDataDirectory(config, this.configPath), 'computer-use', 'runtime');
89
+ await mkdir(runtimeRoot, { recursive: true });
90
+ const current = resolve(runtimeRoot, `${runtimePackage.version}-${this.platform}-${this.architecture}`);
91
+ const currentExecutable = await this.#currentExecutable(current, runtimePackage.version);
92
+ if (currentExecutable !== undefined)
93
+ return currentExecutable;
94
+ return this.#withInstallLock(runtimeRoot, async () => {
95
+ const lockedCurrent = await this.#currentExecutable(current, runtimePackage.version);
96
+ if (lockedCurrent !== undefined)
97
+ return lockedCurrent;
98
+ const packageStaging = resolve(runtimeRoot, `.package-${randomUUID()}`);
99
+ const staging = resolve(runtimeRoot, `.staging-${randomUUID()}`);
100
+ try {
101
+ const packageRoot = await this.installRuntimePackage(runtimePackage, packageStaging);
102
+ await this.#verifyPackageMetadata(packageRoot, runtimePackage);
103
+ const source = resolve(packageRoot, 'runtime');
104
+ const manifest = await this.#readManifest(source);
105
+ if (manifest.runtimeVersion !== runtimePackage.version ||
106
+ manifest.platform !== this.platform ||
107
+ manifest.architecture !== this.architecture)
108
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID');
109
+ await this.#verify(source, manifest);
110
+ await cp(source, staging, { recursive: true, errorOnExist: true, force: false });
111
+ await this.#verify(staging, manifest);
112
+ await rm(current, { recursive: true, force: true });
113
+ try {
114
+ await rename(staging, current);
115
+ }
116
+ catch (error) {
117
+ if (!(await this.#currentExecutable(current, runtimePackage.version)))
118
+ throw error;
119
+ }
120
+ return resolve(current, manifest.executable);
121
+ }
122
+ catch (cause) {
123
+ await rm(staging, { recursive: true, force: true });
124
+ if (cause instanceof ComputerUseRuntimeError)
125
+ throw cause;
126
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_INSTALL_FAILED', { cause });
127
+ }
128
+ finally {
129
+ await rm(packageStaging, { recursive: true, force: true });
130
+ }
131
+ });
132
+ }
133
+ async #currentExecutable(directory, runtimeVersion) {
134
+ try {
135
+ const manifest = await this.#readManifest(directory);
136
+ if (manifest.runtimeVersion !== runtimeVersion ||
137
+ manifest.platform !== this.platform ||
138
+ manifest.architecture !== this.architecture)
139
+ return undefined;
140
+ await this.#verify(directory, manifest);
141
+ return resolve(directory, manifest.executable);
142
+ }
143
+ catch {
144
+ return undefined;
145
+ }
146
+ }
147
+ async #readManifest(directory) {
148
+ try {
149
+ const path = resolve(directory, 'manifest.json');
150
+ if (!(await lstat(path)).isFile())
151
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID');
152
+ return manifestSchema.parse(JSON.parse(await readFile(path, 'utf8')));
153
+ }
154
+ catch (cause) {
155
+ if (cause instanceof ComputerUseRuntimeError)
156
+ throw cause;
157
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID', { cause });
158
+ }
159
+ }
160
+ async #verify(directory, manifest) {
161
+ if (!(await lstat(directory)).isDirectory())
162
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID');
163
+ const root = await realpath(directory);
164
+ const paths = new Set();
165
+ for (const file of manifest.files) {
166
+ const path = safeRuntimePath(directory, file.path);
167
+ if (paths.has(path))
168
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID');
169
+ paths.add(path);
170
+ const fileStat = await lstat(path).catch((cause) => {
171
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MISSING', { cause });
172
+ });
173
+ if (!fileStat.isFile() || !isWithin(root, await realpath(path)))
174
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID');
175
+ const bytes = await readFile(path).catch((cause) => {
176
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MISSING', { cause });
177
+ });
178
+ if (bytes.byteLength !== file.size ||
179
+ createHash('sha256').update(bytes).digest('hex') !== file.sha256)
180
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_INTEGRITY_FAILED');
181
+ }
182
+ const actualPaths = new Set((await regularRuntimeFiles(directory))
183
+ .filter((path) => relative(directory, path).split(sep).join('/') !== 'manifest.json')
184
+ .map((path) => resolve(path)));
185
+ if (actualPaths.size !== paths.size || [...actualPaths].some((path) => !paths.has(path)))
186
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID');
187
+ const executable = safeRuntimePath(directory, manifest.executable);
188
+ if (!paths.has(executable) || !(await stat(executable)).isFile())
189
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID');
190
+ if (this.platform !== 'win32')
191
+ await access(executable, constants.X_OK).catch((cause) => {
192
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_INTEGRITY_FAILED', { cause });
193
+ });
194
+ }
195
+ async #verifyPackageMetadata(packageRoot, runtimePackage) {
196
+ try {
197
+ const path = resolve(packageRoot, 'package.json');
198
+ if (!(await lstat(path)).isFile())
199
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_PACKAGE_INVALID');
200
+ const metadata = z
201
+ .object({ name: z.string(), version: z.string() })
202
+ .passthrough()
203
+ .parse(JSON.parse(await readFile(path, 'utf8')));
204
+ if (metadata.name !== runtimePackage.name || metadata.version !== runtimePackage.version)
205
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_PACKAGE_INVALID');
206
+ }
207
+ catch (cause) {
208
+ if (cause instanceof ComputerUseRuntimeError)
209
+ throw cause;
210
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_PACKAGE_INVALID', { cause });
211
+ }
212
+ }
213
+ async #withInstallLock(runtimeRoot, operation) {
214
+ const lock = resolve(runtimeRoot, '.install-lock');
215
+ const owner = resolve(lock, 'owner.json');
216
+ const deadline = Date.now() + installLockTimeoutMs;
217
+ for (;;) {
218
+ try {
219
+ await mkdir(lock);
220
+ await writeFile(owner, JSON.stringify({ pid: process.pid, id: randomUUID() }), {
221
+ flag: 'wx'
222
+ });
223
+ break;
224
+ }
225
+ catch (cause) {
226
+ if (!isAlreadyExists(cause))
227
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_INSTALL_FAILED', { cause });
228
+ if (!(await lockOwnerIsAlive(owner, lock))) {
229
+ await rm(lock, { recursive: true, force: true });
230
+ continue;
231
+ }
232
+ if (Date.now() >= deadline)
233
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_INSTALL_FAILED', {
234
+ cause: new Error('COMPUTER_RUNTIME_INSTALL_LOCK_TIMEOUT')
235
+ });
236
+ await delay(installLockPollMs);
237
+ }
238
+ }
239
+ try {
240
+ return await operation();
241
+ }
242
+ finally {
243
+ await rm(lock, { recursive: true, force: true });
244
+ }
245
+ }
246
+ }
247
+ async function defaultRuntimePackageInstaller(runtimePackage, destination) {
248
+ const npmRoot = dirname(createRequire(import.meta.url).resolve('npm/package.json'));
249
+ const npmCli = resolve(npmRoot, 'bin/npm-cli.js');
250
+ await mkdir(destination, { recursive: true });
251
+ await execFileAsync(process.execPath, [
252
+ npmCli,
253
+ 'install',
254
+ `${runtimePackage.name}@${runtimePackage.version}`,
255
+ '--prefix',
256
+ destination,
257
+ '--ignore-scripts',
258
+ '--no-audit',
259
+ '--no-fund',
260
+ '--package-lock=false',
261
+ '--omit=dev',
262
+ '--loglevel=error'
263
+ ], {
264
+ env: { ...process.env, NPM_CONFIG_UPDATE_NOTIFIER: 'false' },
265
+ timeout: 600_000,
266
+ maxBuffer: 1024 * 1024
267
+ });
268
+ return resolve(destination, 'node_modules', '@myagentroam', runtimePackage.name.split('/')[1]);
269
+ }
270
+ function safeRuntimePath(root, path) {
271
+ const target = resolve(root, path);
272
+ const suffix = relative(root, target);
273
+ if (suffix.length === 0 ||
274
+ suffix === '..' ||
275
+ suffix.startsWith(`..${sep}`) ||
276
+ resolve(root, suffix) !== target)
277
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID');
278
+ return target;
279
+ }
280
+ function isMissing(error) {
281
+ return (error instanceof Error && 'code' in error && error.code === 'ENOENT');
282
+ }
283
+ function isAlreadyExists(error) {
284
+ return (error instanceof Error && 'code' in error && error.code === 'EEXIST');
285
+ }
286
+ function isWithin(root, target) {
287
+ const suffix = relative(root, target);
288
+ return suffix === '' || (suffix !== '..' && !suffix.startsWith(`..${sep}`));
289
+ }
290
+ async function regularRuntimeFiles(directory) {
291
+ const output = [];
292
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
293
+ const path = resolve(directory, entry.name);
294
+ if (entry.isDirectory())
295
+ output.push(...(await regularRuntimeFiles(path)));
296
+ else if (entry.isFile())
297
+ output.push(path);
298
+ else
299
+ throw new ComputerUseRuntimeError('COMPUTER_RUNTIME_MANIFEST_INVALID');
300
+ }
301
+ return output;
302
+ }
303
+ async function lockOwnerIsAlive(ownerPath, lockPath) {
304
+ try {
305
+ const owner = z
306
+ .object({ pid: z.number().int().positive() })
307
+ .passthrough()
308
+ .parse(JSON.parse(await readFile(ownerPath, 'utf8')));
309
+ try {
310
+ process.kill(owner.pid, 0);
311
+ return true;
312
+ }
313
+ catch (cause) {
314
+ return !(cause instanceof Error &&
315
+ 'code' in cause &&
316
+ cause.code === 'ESRCH');
317
+ }
318
+ }
319
+ catch {
320
+ try {
321
+ return Date.now() - (await stat(lockPath)).mtimeMs < 5_000;
322
+ }
323
+ catch {
324
+ return true;
325
+ }
326
+ }
327
+ }
328
+ function delay(milliseconds) {
329
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
330
+ }
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, isAbsolute, relative, resolve, sep, win32 } from 'node:path';
4
+ import { computerUseRuntimeVersion } from './computer-use-runtime-version.js';
4
5
  export function nodeConfigPath() {
5
6
  return resolve(process.env['MAR_NODE_CONFIG'] ?? 'data/mar-node.json');
6
7
  }
@@ -27,6 +28,7 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
27
28
  : defaultAllowedRoots();
28
29
  const conversationHosting = parseConversationHosting(input['conversationHosting']);
29
30
  const directTransfer = parseDirectTransfer(input['directTransfer']);
31
+ const computerUse = parseComputerUse(input['computerUse']);
30
32
  return {
31
33
  serverUrl: parsed.serverUrl,
32
34
  allowedRoots,
@@ -35,7 +37,8 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
35
37
  ...(registrationToken === undefined ? {} : { registrationToken }),
36
38
  ...(dataDirectory === undefined ? {} : { dataDirectory }),
37
39
  ...(conversationHosting === undefined ? {} : { conversationHosting }),
38
- ...(directTransfer === undefined ? {} : { directTransfer })
40
+ ...(directTransfer === undefined ? {} : { directTransfer }),
41
+ ...(computerUse === undefined ? {} : { computerUse })
39
42
  };
40
43
  }
41
44
  export function directTransferUdpPortRange(config) {
@@ -57,6 +60,16 @@ export function nodeDataDirectory(config, configPath = nodeConfigPath()) {
57
60
  return resolve(dirname(configPath), config.dataDirectory);
58
61
  return resolve(dirname(configPath));
59
62
  }
63
+ export function computerUseRuntimeExecutable(config, configPath = nodeConfigPath(), platform = process.platform, architecture = process.arch) {
64
+ if (config.computerUse?.enabled !== true)
65
+ return undefined;
66
+ if (config.computerUse.runtimeExecutable !== undefined)
67
+ return resolve(dirname(configPath), config.computerUse.runtimeExecutable);
68
+ const runtimeRoot = resolve(nodeDataDirectory(config, configPath), 'computer-use', 'runtime', `${computerUseRuntimeVersion}-${platform}-${architecture}`);
69
+ return platform === 'win32'
70
+ ? resolve(runtimeRoot, 'python.exe')
71
+ : resolve(runtimeRoot, 'bin', 'python3');
72
+ }
60
73
  export function hostedConversationStorePath(config, configPath = nodeConfigPath()) {
61
74
  const hosting = config.conversationHosting;
62
75
  if (hosting?.enabled !== true)
@@ -129,6 +142,27 @@ function parseDirectTransfer(value) {
129
142
  udpPortRangeEnd: end
130
143
  };
131
144
  }
145
+ function parseComputerUse(value) {
146
+ if (value === undefined)
147
+ return undefined;
148
+ if (typeof value !== 'object' ||
149
+ value === null ||
150
+ Array.isArray(value) ||
151
+ typeof value['enabled'] !== 'boolean' ||
152
+ Object.keys(value).some((key) => key !== 'enabled' && key !== 'runtimeExecutable'))
153
+ throw new Error('NODE_CONFIG_INVALID');
154
+ const input = value;
155
+ const runtimeExecutable = input['runtimeExecutable'];
156
+ if (runtimeExecutable !== undefined &&
157
+ (typeof runtimeExecutable !== 'string' ||
158
+ runtimeExecutable.length === 0 ||
159
+ runtimeExecutable.trim() !== runtimeExecutable))
160
+ throw new Error('NODE_CONFIG_INVALID');
161
+ return {
162
+ enabled: input['enabled'],
163
+ ...(runtimeExecutable === undefined ? {} : { runtimeExecutable })
164
+ };
165
+ }
132
166
  function validStoreDirectory(value) {
133
167
  if (typeof value !== 'string' || value.trim() !== value || value.length === 0)
134
168
  return false;
package/dist/connector.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { dirname, resolve } from 'node:path';
2
2
  import { createEnvelope, directNodeCommandSchema } from '@myagentroam/protocol';
3
- import { hostedConversationStorePath, loadNodeConfig, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
3
+ import { hostedConversationStorePath, loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
4
4
  import { NodeMetrics, nodeLog } from './operational.js';
5
5
  import { assertLocalSqlitePath } from './storage.js';
6
6
  import { detectCapabilitiesAsync, unavailableCapabilities } from './capabilities.js';
@@ -20,6 +20,8 @@ import { RunnerRegistry } from './runner/runner-registry.js';
20
20
  import { OpenCodeRunner } from './runner/opencode-runner.js';
21
21
  import { MarAgentRunner } from './runner/mar-agent-runner.js';
22
22
  import { MarAgentManagedRunController } from './runner/mar-agent/managed-run-controller.js';
23
+ import { PyAutoGuiComputerUseProvider } from './runner/mar-agent/computer-use.js';
24
+ import { ManagedComputerUseRuntime } from './computer-use-runtime.js';
23
25
  import { OpenCodeManagedRunController } from './runner/opencode/managed-run-controller.js';
24
26
  import { NodeOperationRouter } from './connector/node-operation-router.js';
25
27
  import { SkillNodeOperationService } from './service/skill-node-operation-service.js';
@@ -85,6 +87,7 @@ import { MarAgentHostedConversationRuntime } from './runner/mar-agent/hosted-con
85
87
  import { conversationHostingCapability, probeBubblewrap } from './service/conversation-hosting-capability-service.js';
86
88
  import { ConversationHostingMaintenanceService } from './service/conversation-hosting-maintenance-service.js';
87
89
  import { ConversationHostingEnablementService } from './service/conversation-hosting-enablement-service.js';
90
+ import { ComputerUseEnablementService } from './service/computer-use-enablement-service.js';
88
91
  import { safeErrorCode } from './util/safe-error.js';
89
92
  import { RuntimeCredentialControlService } from './service/runtime-credential-control-service.js';
90
93
  import { MarAgentRuntimeModelFactory } from './runner/mar-agent/runtime-model-factory.js';
@@ -210,6 +213,9 @@ export class NodeConnector {
210
213
  openCodeClient;
211
214
  marAgentClient;
212
215
  marAgentRunController;
216
+ computerUseProvider;
217
+ computerUseEnablement;
218
+ managedComputerUseRuntime;
213
219
  openCodeRunController;
214
220
  claudeRunController;
215
221
  runners;
@@ -262,7 +268,7 @@ export class NodeConnector {
262
268
  terminalManager;
263
269
  upgradeCoordinator;
264
270
  workspaceState = new WorkspaceDomainState();
265
- workspaceWatchService = new WorkspaceWatchService(this.workspaceState, (workspaceId) => this.workspaceSessionService.ensureWatch(workspaceId), (workspaceId) => this.workspaceSessionService.stopWatch(workspaceId), (workspaceId) => this.workspaceWorkbenchService?.clearWorkspace(workspaceId));
271
+ workspaceWatchService = new WorkspaceWatchService(this.workspaceState, (workspaceId) => this.workspaceSessionService.ensureWatch(workspaceId), (workspaceId) => this.workspaceSessionService.stopWatch(workspaceId), (workspaceId) => this.workspaceWorkbenchService?.releaseWorkspace(workspaceId));
266
272
  workspaceSessionService = new WorkspaceSessionService(this.workspaceState, {
267
273
  discover: (workspaceId) => this.discoverWorkspaceSessions(workspaceId),
268
274
  listPage: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 20 }),
@@ -274,9 +280,13 @@ export class NodeConnector {
274
280
  waitMs
275
281
  })
276
282
  });
277
- workspaceFileService = new WorkspaceFileService(this.workspaceState, () => this.config?.allowedRoots ?? [], (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId));
283
+ workspaceFileService = new WorkspaceFileService(this.workspaceState, () => this.config?.allowedRoots ?? [], (workspaceId) => {
284
+ this.workspaceWorkbenchService?.invalidateWorkspaceFiles(workspaceId);
285
+ this.workspaceCoordinator.invalidate(workspaceId);
286
+ });
278
287
  workspaceUploadService = new WorkspaceUploadService(this.workspaceState, (workspaceId) => {
279
288
  this.workspaceFileService.dispose(workspaceId);
289
+ this.workspaceWorkbenchService?.invalidateWorkspaceFiles(workspaceId);
280
290
  this.workspaceCoordinator.invalidate(workspaceId);
281
291
  });
282
292
  directTransferService;
@@ -443,10 +453,10 @@ export class NodeConnector {
443
453
  const detect = options.capabilityDetector ?? detectCapabilitiesAsync;
444
454
  this.capabilityDetector = async () => {
445
455
  const capabilities = await detect();
446
- return this.projectConversationHostingCapability({
456
+ return this.projectComputerUseCapability(await this.projectConversationHostingCapability({
447
457
  ...capabilities,
448
458
  metadata: upgradeMetadata(capabilities.metadata, this.upgradeCoordinator.snapshot())
449
- });
459
+ }));
450
460
  };
451
461
  this.reconnect = options.reconnect ?? true;
452
462
  this.fakeRunner =
@@ -493,6 +503,25 @@ export class NodeConnector {
493
503
  this.openCodeClient = new OpenCodeRunner(options.openCodeClient);
494
504
  this.marAgentClient = new MarAgentRunner();
495
505
  this.marAgentClient.setGeneratedImageRegistrar((session, images) => this.runAttachmentService.registerNative(session, images));
506
+ this.computerUseProvider = new PyAutoGuiComputerUseProvider(() => {
507
+ if (this.config?.computerUse?.enabled !== true)
508
+ return undefined;
509
+ const runtimeExecutable = computerUseRuntimeExecutable(this.config, this.configPath);
510
+ return runtimeExecutable === undefined ? undefined : { enabled: true, runtimeExecutable };
511
+ });
512
+ this.managedComputerUseRuntime = new ManagedComputerUseRuntime(() => this.config, this.configPath);
513
+ this.computerUseEnablement = new ComputerUseEnablementService({
514
+ config: () => this.config,
515
+ setConfig: (config) => (this.config = config),
516
+ configPath: this.configPath,
517
+ provider: this.computerUseProvider,
518
+ prepareRuntime: () => this.managedComputerUseRuntime.ensure(),
519
+ publish: (capability) => {
520
+ this.capabilities = { ...this.capabilities, computerUse: capability };
521
+ this.send('capabilities', this.capabilities);
522
+ }
523
+ });
524
+ this.capabilities = this.projectComputerUseCapability(this.capabilities);
496
525
  this.marAgentRunController = new MarAgentManagedRunController(this.marAgentClient, {
497
526
  active: this.runState.active,
498
527
  adopt: (input) => this.runtime.adoptRun(input),
@@ -519,7 +548,8 @@ export class NodeConnector {
519
548
  : this.marAgentClient.registerGeneratedImages(session, nativeSessionId, images);
520
549
  }
521
550
  }, {
522
- modelFactory: this.marAgentRuntimeModels
551
+ modelFactory: this.marAgentRuntimeModels,
552
+ computerUse: this.computerUseProvider
523
553
  });
524
554
  this.marAgentClient.setSessionResourceReleaser((sessionId) => this.marAgentRunController.releaseSession(sessionId));
525
555
  this.openCodeRunController = new OpenCodeManagedRunController(this.openCodeClient, {
@@ -1173,7 +1203,10 @@ export class NodeConnector {
1173
1203
  metrics: this.metrics,
1174
1204
  heartbeatMs: HEARTBEAT_MS,
1175
1205
  refreshMs: CAPABILITY_REFRESH_MS,
1176
- onRegistered: () => this.initializeConversationHosting(),
1206
+ onRegistered: async () => {
1207
+ await this.initializeConversationHosting();
1208
+ this.computerUseEnablement.initialize();
1209
+ },
1177
1210
  onConnected: () => {
1178
1211
  this.runtimeCredentials.connected();
1179
1212
  this.recentRunProjectionService.publishSnapshot();
@@ -1213,6 +1246,7 @@ export class NodeConnector {
1213
1246
  listSessions: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 20 }),
1214
1247
  invalidate: (workspaceId) => {
1215
1248
  this.workspaceFileService.dispose(workspaceId);
1249
+ this.workspaceWorkbenchService.invalidateWorkspaceFiles(workspaceId);
1216
1250
  this.workspaceCoordinator.invalidate(workspaceId);
1217
1251
  },
1218
1252
  nodeId: () => this.config?.nodeId ?? 'runtime-node',
@@ -1263,6 +1297,7 @@ export class NodeConnector {
1263
1297
  prepareForDisable: () => this.hostedConversationService?.prepareForDisable() ?? (() => { }),
1264
1298
  publishCapabilities: () => this.send('capabilities', this.capabilities)
1265
1299
  }).operations());
1300
+ this.operationRouter.registerAll(this.computerUseEnablement.operations());
1266
1301
  this.operationRouter.registerAll(new SkillNodeOperationService(requireDatabase, () => {
1267
1302
  if (this.config === undefined)
1268
1303
  throw new Error('NODE_CONFIG_UNAVAILABLE');
@@ -1393,6 +1428,12 @@ export class NodeConnector {
1393
1428
  }
1394
1429
  this.capabilities = await this.projectConversationHostingCapability(this.capabilities);
1395
1430
  }
1431
+ projectComputerUseCapability(capabilities) {
1432
+ return {
1433
+ ...capabilities,
1434
+ computerUse: this.computerUseEnablement.capability()
1435
+ };
1436
+ }
1396
1437
  async projectConversationHostingCapability(capabilities) {
1397
1438
  let storageReady = this.conversationHostingStorageReady;
1398
1439
  let storageFailureCode = this.conversationHostingFailureCode;
@@ -1422,6 +1463,7 @@ export class NodeConnector {
1422
1463
  this.database ??= new NodeDatabase(nodeDatabasePath(this.config, this.configPath), () => this.config?.nodeId ?? 'unregistered');
1423
1464
  this.database.closeRunningTerminalSessions();
1424
1465
  await this.initializeConversationHosting();
1466
+ this.computerUseEnablement.initialize();
1425
1467
  this.directTransferService ??= this.createDirectTransferService();
1426
1468
  this.connectionLifecycle.start();
1427
1469
  this.controlChannel.connect();
@@ -0,0 +1,18 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ export const BUNDLED_CTAGS_VERSION = '6.2.1';
4
+ const TARGETS = {
5
+ 'linux-x64': ['linux-x64', 'ctags'],
6
+ 'win32-x64': ['win32-x64', 'ctags.exe']
7
+ };
8
+ export function bundledCtagsDirectory(packageRoot) {
9
+ return packageRoot === undefined
10
+ ? resolve(dirname(fileURLToPath(import.meta.url)), '..', 'vendor/ctags')
11
+ : resolve(packageRoot, 'mar-node/vendor/ctags');
12
+ }
13
+ export function bundledCtagsPath(platform = process.platform, architecture = process.arch, packageRoot) {
14
+ const target = TARGETS[`${platform}-${architecture}`];
15
+ if (target === undefined)
16
+ throw new Error('CTAGS_PLATFORM_UNSUPPORTED');
17
+ return resolve(bundledCtagsDirectory(packageRoot), ...target);
18
+ }