@myagentroam/node 0.11.37 → 0.11.39

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,321 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { resolve } from 'node:path';
6
+ import { createInterface } from 'node:readline';
7
+ import { terminateProcessTree } from '../process-tree.js';
8
+ import { ComputerUseError } from './contracts.js';
9
+ import { MAX_IMAGE_BYTES, MAX_IMAGES, PYTHON_CLEANUP_SOURCE, PYTHON_WORKER_SOURCE } from './worker-source.js';
10
+ const MAX_TEXT_BYTES = 1024 * 1024;
11
+ export class PythonWorker {
12
+ child;
13
+ lines;
14
+ directory;
15
+ #exited;
16
+ #pending = new Map();
17
+ #stderr = '';
18
+ #closed = false;
19
+ #closePromise;
20
+ #requestStarted = false;
21
+ constructor(child, lines, directory) {
22
+ this.child = child;
23
+ this.lines = lines;
24
+ this.directory = directory;
25
+ this.#exited = new Promise((resolveExit) => this.child.once('close', () => resolveExit()));
26
+ this.lines.on('line', (line) => this.#handleLine(line));
27
+ this.child.stderr.on('data', (chunk) => {
28
+ this.#stderr = boundedText(`${this.#stderr}${chunk.toString('utf8')}`);
29
+ });
30
+ this.child.once('exit', (code, signal) => {
31
+ const error = new ComputerUseError('COMPUTER_WORKER_EXITED', `Computer Use worker exited (${code ?? signal ?? 'unknown'}).${this.#stderr ? ` ${this.#stderr}` : ''}`);
32
+ for (const pending of this.#pending.values())
33
+ pending.reject(error);
34
+ this.#pending.clear();
35
+ });
36
+ }
37
+ get startedRequest() {
38
+ return this.#requestStarted;
39
+ }
40
+ static async start(runtimeExecutable, onStarting) {
41
+ const directory = await mkdtemp(resolve(tmpdir(), 'mar-computer-use-'));
42
+ const script = resolve(directory, 'worker.py');
43
+ let child;
44
+ try {
45
+ await writeFile(script, PYTHON_WORKER_SOURCE, { encoding: 'utf8', mode: 0o600 });
46
+ child = spawn(runtimeExecutable, ['-u', script], {
47
+ cwd: directory,
48
+ env: minimalWorkerEnvironment(),
49
+ stdio: ['pipe', 'pipe', 'pipe'],
50
+ windowsHide: true,
51
+ detached: process.platform !== 'win32'
52
+ });
53
+ }
54
+ catch (error) {
55
+ try {
56
+ await rm(directory, { recursive: true, force: true });
57
+ }
58
+ catch (cleanup) {
59
+ throw new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use worker startup cleanup failed.', {
60
+ cause: new AggregateError([error, cleanup])
61
+ });
62
+ }
63
+ throw error;
64
+ }
65
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
66
+ const worker = new PythonWorker(child, lines, directory);
67
+ try {
68
+ onStarting(worker);
69
+ }
70
+ catch (error) {
71
+ try {
72
+ await worker.close();
73
+ }
74
+ catch (cleanup) {
75
+ throw new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use worker handoff cleanup failed.', {
76
+ cause: new AggregateError([error, cleanup])
77
+ });
78
+ }
79
+ throw error;
80
+ }
81
+ await waitForWorkerReady(lines, child);
82
+ return worker;
83
+ }
84
+ execute(code, timeoutMs, signal) {
85
+ this.#requestStarted = false;
86
+ if (this.#closed)
87
+ return Promise.reject(new ComputerUseError('COMPUTER_WORKER_EXITED', 'Computer Use worker is closed.'));
88
+ const id = randomUUID();
89
+ return new Promise((resolve, reject) => {
90
+ let settled = false;
91
+ const finish = (callback) => {
92
+ if (settled)
93
+ return;
94
+ settled = true;
95
+ clearTimeout(timer);
96
+ signal.removeEventListener('abort', abort);
97
+ this.#pending.delete(id);
98
+ callback();
99
+ };
100
+ const abort = () => finish(() => reject(new ComputerUseError('COMPUTER_EXECUTION_CANCELLED', 'Computer Use execution cancelled.')));
101
+ const timer = setTimeout(() => finish(() => reject(new ComputerUseError('COMPUTER_EXECUTION_TIMEOUT', 'Computer Use execution timed out.'))), timeoutMs);
102
+ if (signal.aborted) {
103
+ abort();
104
+ return;
105
+ }
106
+ this.#requestStarted = true;
107
+ this.#pending.set(id, {
108
+ resolve: (value) => finish(() => resolve(value)),
109
+ reject: (error) => finish(() => reject(error))
110
+ });
111
+ signal.addEventListener('abort', abort, { once: true });
112
+ if (signal.aborted) {
113
+ abort();
114
+ return;
115
+ }
116
+ this.child.stdin.write(`${JSON.stringify({ id, code })}\n`, (error) => {
117
+ if (error !== null && error !== undefined)
118
+ finish(() => reject(new ComputerUseError('COMPUTER_WORKER_WRITE_FAILED', 'Worker request failed.', {
119
+ cause: error
120
+ })));
121
+ });
122
+ });
123
+ }
124
+ close() {
125
+ if (this.#closePromise !== undefined)
126
+ return this.#closePromise;
127
+ this.#closed = true;
128
+ this.#closePromise = (async () => {
129
+ this.lines.close();
130
+ this.child.stdin.end();
131
+ if (this.child.exitCode === null && this.child.signalCode === null)
132
+ await terminateProcessTree(this.child).catch(() => this.child.kill('SIGKILL'));
133
+ if (!(await this.#waitForExit(2_000))) {
134
+ try {
135
+ if (process.platform !== 'win32' && this.child.pid !== undefined)
136
+ process.kill(-this.child.pid, 'SIGKILL');
137
+ else
138
+ this.child.kill('SIGKILL');
139
+ }
140
+ catch {
141
+ this.child.kill('SIGKILL');
142
+ }
143
+ if (!(await this.#waitForExit(2_000)))
144
+ throw new ComputerUseError('COMPUTER_WORKER_EXIT_FAILED', 'Computer Use worker did not exit.');
145
+ }
146
+ if (this.child.pid !== undefined)
147
+ await runWorkerCleanup(this.child.spawnfile, this.directory);
148
+ await rm(this.directory, { recursive: true, force: true });
149
+ })();
150
+ return this.#closePromise;
151
+ }
152
+ #waitForExit(timeoutMs) {
153
+ return new Promise((resolveWait) => {
154
+ const timer = setTimeout(() => resolveWait(false), timeoutMs);
155
+ void this.#exited.then(() => {
156
+ clearTimeout(timer);
157
+ resolveWait(true);
158
+ });
159
+ });
160
+ }
161
+ #handleLine(line) {
162
+ let value;
163
+ try {
164
+ value = JSON.parse(line);
165
+ }
166
+ catch {
167
+ return;
168
+ }
169
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
170
+ return;
171
+ const message = value;
172
+ if (message.type === 'ready')
173
+ return;
174
+ const pending = typeof message.id === 'string' ? this.#pending.get(message.id) : undefined;
175
+ if (pending === undefined)
176
+ return;
177
+ if (message.type === 'result') {
178
+ try {
179
+ pending.resolve({
180
+ logs: Array.isArray(message.logs)
181
+ ? message.logs
182
+ .filter((value) => typeof value === 'string')
183
+ .map((value) => boundedText(value))
184
+ : [],
185
+ images: parseWorkerImages(message.images)
186
+ });
187
+ }
188
+ catch (error) {
189
+ pending.reject(error);
190
+ }
191
+ return;
192
+ }
193
+ if (message.type !== 'error')
194
+ return;
195
+ pending.reject(new ComputerUseError('COMPUTER_PYTHON_FAILED', typeof message.error === 'string' ? boundedText(message.error) : 'Python execution failed.'));
196
+ }
197
+ }
198
+ function parseWorkerImages(value) {
199
+ if (!Array.isArray(value) || value.length > MAX_IMAGES)
200
+ throw new ComputerUseError('COMPUTER_IMAGE_INVALID', 'Worker returned invalid images.');
201
+ return value.map((candidate) => {
202
+ if (candidate === null ||
203
+ typeof candidate !== 'object' ||
204
+ candidate['mimeType'] !== 'image/png' ||
205
+ typeof candidate['dataBase64'] !== 'string')
206
+ throw new ComputerUseError('COMPUTER_IMAGE_INVALID', 'Worker returned an invalid image.');
207
+ const dataBase64 = candidate['dataBase64'];
208
+ if (dataBase64.length > Math.ceil((MAX_IMAGE_BYTES * 4) / 3) + 4)
209
+ throw new ComputerUseError('COMPUTER_IMAGE_TOO_LARGE', 'Worker image exceeds the size limit.');
210
+ const bytes = Buffer.from(dataBase64, 'base64');
211
+ if (bytes.length === 0 ||
212
+ bytes.length > MAX_IMAGE_BYTES ||
213
+ !bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
214
+ throw new ComputerUseError('COMPUTER_IMAGE_INVALID', 'Worker returned an invalid PNG image.');
215
+ return { mimeType: 'image/png', dataBase64 };
216
+ });
217
+ }
218
+ async function waitForWorkerReady(lines, child) {
219
+ await new Promise((resolveReady, reject) => {
220
+ const timeout = setTimeout(() => {
221
+ cleanup();
222
+ reject(new ComputerUseError('COMPUTER_WORKER_START_FAILED', 'Worker start timed out.'));
223
+ }, 10_000);
224
+ const line = (value) => {
225
+ try {
226
+ if (JSON.parse(value).type !== 'ready')
227
+ return;
228
+ }
229
+ catch {
230
+ return;
231
+ }
232
+ cleanup();
233
+ resolveReady();
234
+ };
235
+ const exit = () => {
236
+ cleanup();
237
+ reject(new ComputerUseError('COMPUTER_WORKER_START_FAILED', 'Worker exited during startup.'));
238
+ };
239
+ const failed = (error) => {
240
+ cleanup();
241
+ reject(new ComputerUseError('COMPUTER_WORKER_START_FAILED', 'Worker could not start.', {
242
+ cause: error
243
+ }));
244
+ };
245
+ const cleanup = () => {
246
+ clearTimeout(timeout);
247
+ lines.off('line', line);
248
+ child.off('exit', exit);
249
+ child.off('error', failed);
250
+ };
251
+ lines.on('line', line);
252
+ child.once('exit', exit);
253
+ child.once('error', failed);
254
+ });
255
+ }
256
+ async function runWorkerCleanup(executable, directory) {
257
+ const child = spawn(executable, ['-c', PYTHON_CLEANUP_SOURCE], {
258
+ cwd: directory,
259
+ env: minimalWorkerEnvironment(),
260
+ detached: process.platform !== 'win32',
261
+ stdio: 'ignore',
262
+ windowsHide: true
263
+ });
264
+ await new Promise((resolveCleanup, reject) => {
265
+ let timedOut = false;
266
+ let forceTimer;
267
+ const timer = setTimeout(() => {
268
+ timedOut = true;
269
+ void terminateProcessTree(child).catch(() => child.kill('SIGKILL'));
270
+ forceTimer = setTimeout(() => {
271
+ child.kill('SIGKILL');
272
+ reject(new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use cleanup process did not exit.'));
273
+ }, 2_000);
274
+ }, 5_000);
275
+ const finish = (error) => {
276
+ clearTimeout(timer);
277
+ if (forceTimer !== undefined)
278
+ clearTimeout(forceTimer);
279
+ if (error !== undefined)
280
+ reject(error);
281
+ else
282
+ resolveCleanup();
283
+ };
284
+ child.once('close', (code) => {
285
+ if (timedOut || code !== 0)
286
+ finish(new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use cleanup process failed.'));
287
+ else
288
+ finish();
289
+ });
290
+ child.once('error', (error) => finish(new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use cleanup process could not start.', { cause: error })));
291
+ });
292
+ }
293
+ function minimalWorkerEnvironment() {
294
+ const keys = [
295
+ 'PATH',
296
+ 'SystemRoot',
297
+ 'WINDIR',
298
+ 'TEMP',
299
+ 'TMP',
300
+ 'TMPDIR',
301
+ 'HOME',
302
+ 'USERPROFILE',
303
+ 'DISPLAY',
304
+ 'XAUTHORITY',
305
+ 'LANG',
306
+ 'LC_ALL'
307
+ ];
308
+ const environment = {};
309
+ for (const key of keys) {
310
+ const value = process.env[key];
311
+ if (value !== undefined)
312
+ environment[key] = value;
313
+ }
314
+ return environment;
315
+ }
316
+ function boundedText(value) {
317
+ const bytes = Buffer.from(value);
318
+ return bytes.length <= MAX_TEXT_BYTES
319
+ ? value
320
+ : `${bytes.subarray(0, MAX_TEXT_BYTES).toString('utf8')}\n[truncated]`;
321
+ }
package/dist/connector.js CHANGED
@@ -92,6 +92,7 @@ import { probeBubblewrap } from './service/conversation-hosting-capability-servi
92
92
  import { ConversationHostingLifecycleService } from './service/conversation-hosting-lifecycle-service.js';
93
93
  import { ConversationHostingEnablementService } from './service/conversation-hosting-enablement-service.js';
94
94
  import { ComputerUseEnablementService } from './service/computer-use-enablement-service.js';
95
+ import { NodeConfigMutations } from './service/node-config-mutations.js';
95
96
  import { safeErrorCode } from './util/safe-error.js';
96
97
  import { RuntimeCredentialControlService } from './service/runtime-credential-control-service.js';
97
98
  import { MarAgentRuntimeModelFactory } from './runner/mar-agent/runtime-model-factory.js';
@@ -256,6 +257,7 @@ export class NodeConnector {
256
257
  marAgentRunController;
257
258
  computerUseProvider;
258
259
  computerUseEnablement;
260
+ configMutations = new NodeConfigMutations();
259
261
  managedComputerUseRuntime;
260
262
  openCodeRunController;
261
263
  claudeRunController;
@@ -549,6 +551,7 @@ export class NodeConnector {
549
551
  });
550
552
  this.managedComputerUseRuntime = new ManagedComputerUseRuntime(() => this.config, this.configPath);
551
553
  this.computerUseEnablement = new ComputerUseEnablementService({
554
+ mutations: this.configMutations,
552
555
  config: () => this.config,
553
556
  setConfig: (config) => (this.config = config),
554
557
  configPath: this.configPath,
@@ -1187,7 +1190,7 @@ export class NodeConnector {
1187
1190
  this.controlMessages = new NodeControlMessageService({
1188
1191
  config: () => this.config,
1189
1192
  register: async (nodeId, credential, payload) => {
1190
- await this.connectionLifecycle.register(nodeId, credential);
1193
+ await this.configMutations.run(() => this.connectionLifecycle.register(nodeId, credential));
1191
1194
  this.mailNotificationControl.setFeatures(payload);
1192
1195
  this.usageOutbox?.connected(payload);
1193
1196
  for (const change of this.pendingSessionGroups.values())
@@ -1214,9 +1217,9 @@ export class NodeConnector {
1214
1217
  },
1215
1218
  connected: (payload) => {
1216
1219
  this.usageOutbox?.connected(payload);
1220
+ this.connectionLifecycle.connected();
1217
1221
  for (const change of this.pendingSessionGroups.values())
1218
1222
  this.send('session.group.changed', change);
1219
- this.connectionLifecycle.connected();
1220
1223
  this.mailNotificationControl.setFeatures(payload);
1221
1224
  },
1222
1225
  close: (code, reason) => this.controlChannel.close(code, reason),
@@ -1481,6 +1484,7 @@ export class NodeConnector {
1481
1484
  this.operationRouter.registerAll(this.marAgentModelTransportTests.operations());
1482
1485
  this.operationRouter.registerAll(this.chatgptLogins.operations());
1483
1486
  this.operationRouter.registerAll(new ConversationHostingEnablementService({
1487
+ mutations: this.configMutations,
1484
1488
  config: () => this.config,
1485
1489
  setConfig: (config) => (this.config = config),
1486
1490
  configPath: this.configPath,
@@ -42,6 +42,9 @@ export class ControlOutboundScheduler {
42
42
  this.scheduleExpiry();
43
43
  this.schedule();
44
44
  }
45
+ resume() {
46
+ this.schedule();
47
+ }
45
48
  reset() {
46
49
  this.high.length = 0;
47
50
  this.normal.length = 0;
@@ -936,7 +936,7 @@ function modelSummary(model, index) {
936
936
  defaultReasoningEffort: resolveMarAgentDefaultReasoningEffort(model.defaultReasoningEffort, model.reasoningEfforts),
937
937
  titleGeneration: model.titleGeneration,
938
938
  codeMode: model.codeMode,
939
- ...(model.fastMode === true ? { fastMode: true } : {}),
939
+ ...(model.serviceTier === undefined ? {} : { serviceTier: model.serviceTier }),
940
940
  hostedWebSearch: model.hostedWebSearch,
941
941
  ...(model.responsesTransport === undefined
942
942
  ? {}
@@ -14,7 +14,7 @@ export const NODE_MCP = {
14
14
  name: 'mar_notification',
15
15
  tools: { send: 'send_notification', email: 'send_email_notification' }
16
16
  },
17
- computer: { name: 'mar_computer', tools: { exec: 'exec' } },
17
+ computer: { name: 'mar_computer', tools: { exec: 'exec', wait: 'wait' } },
18
18
  channel: { name: 'mar_channel', tools: { reply: 'reply' } }
19
19
  };
20
20
  const { version } = createRequire(import.meta.url)('../../package.json');
@@ -25,6 +25,11 @@ export const NODE_COMPUTER_TOOL_NAME = [
25
25
  NODE_MCP.computer.name,
26
26
  NODE_MCP.computer.tools.exec
27
27
  ].join('__');
28
+ export const NODE_COMPUTER_WAIT_TOOL_NAME = [
29
+ 'mcp',
30
+ NODE_MCP.computer.name,
31
+ NODE_MCP.computer.tools.wait
32
+ ].join('__');
28
33
  const reservedNames = new Set(Object.values(NODE_MCP).map(({ name }) => name));
29
34
  export function mcpNameAlias(value) {
30
35
  return value.replace(/[^a-zA-Z0-9_-]/gu, '_').slice(0, 64);
@@ -1,10 +1,10 @@
1
- import { NODE_COMPUTER_TOOL_NAME } from '../../mcp/naming.js';
1
+ import { NODE_COMPUTER_TOOL_NAME, NODE_COMPUTER_WAIT_TOOL_NAME } from '../../mcp/naming.js';
2
2
  export const computerUseHostSkill = {
3
3
  name: 'computer-use',
4
4
  description: 'Use when the user asks to operate or verify a real desktop GUI through ' +
5
5
  NODE_COMPUTER_TOOL_NAME +
6
6
  '. Load before the first desktop action.',
7
- requiredTools: [NODE_COMPUTER_TOOL_NAME],
7
+ requiredTools: [NODE_COMPUTER_TOOL_NAME, NODE_COMPUTER_WAIT_TOOL_NAME],
8
8
  load: async () => computerUseSkill()
9
9
  };
10
10
  export function computerUseSkill() {
@@ -25,11 +25,20 @@ Use \`${NODE_COMPUTER_TOOL_NAME}\` only for user-authorized interaction with app
25
25
  - The runtime provides \`pyautogui\`, \`time\`, \`display(image)\`, \`log(value)\`, and \`paste_text(text)\`. Use these directly; do not import or install another desktop automation stack.
26
26
  - Keep each script focused and bounded, but prefer one coherent call over several one-action calls when the sequence is deterministic. Use short waits only for expected transitions.
27
27
  - Once repeated controls and their geometry are established, batch safe repetitive interactions such as filling several known fields or placing several known items. Do not spend a separate observe-act cycle on every identical row, item, or dialog.
28
+ - For drawing, confirm the canvas, focus, and tool, then make a small test stroke. Combine known geometry into complete strokes or bounded batches; stop and observe at visual decision points. Confirm the format and path before saving. If the user requested real GUI drawing, do not substitute a generated file for that interaction.
29
+ - Keep PyAutoGUI's FAILSAFE enabled. Adjust input timing only when the target application needs it; if a script temporarily changes library settings, restore those settings in a finally block so the warm namespace is not polluted.
30
+ - The desktop lease coordinates MAR callers, not human input isolation. Tell the user: do not operate the same desktop while automation is active.
28
31
  - Do not issue sleep-only calls. Wait for an expected transition inside the action that caused it, then use the Runtime's automatic final screenshot to inspect the result.
29
32
  - Use PyAutoGUI for clicks, hotkeys, control keys, and compatible ASCII typing. Use \`paste_text\` for Unicode or longer literal text.
30
33
  - Do not assume a click, keypress, or paste can be rolled back. Avoid broad destructive shortcuts unless the user explicitly requested their effect.
31
34
  - The Runtime already returns a final primary-display screenshot. Call \`display()\` only for an additional image that is materially useful; do not duplicate the automatic final screenshot.
32
35
 
36
+ ### Overlap independent work safely
37
+ - For a long bounded script, optionally set \`yieldMs\` to return early with a \`taskId\`. Omit it to wait synchronously. Early return changes responsiveness, not mouse speed or the execution timeout.
38
+ - While \`RUNNING\` or \`STOPPING\`, prepare coordinates or do other work that cannot affect the desktop, focus, target application, or files it is using. Do not start another desktop script, observation, or input through another tool; the desktop is not safe for concurrent human input.
39
+ - Call \`${NODE_COMPUTER_WAIT_TOOL_NAME}\` with that taskId and \`action: "poll"\` to obtain the final result and screenshot before claiming completion. A wait timeout or interrupted wait does not stop the task. Use \`action: "cancel"\` to stop and drain it; \`STOPPING\` is not a completed cancellation.
40
+ - Tasks belong to this Execution and are stopped when it ends. Repeated wait/cancel reads the current terminal result without replay; a newly accepted exec replaces it. After \`OUTCOME_UNKNOWN\`, observe again and never automatically replay the script.
41
+
33
42
  ### Verify and recover
34
43
  - Verify at decision boundaries and once after the requested result is complete. Intermediate screenshots are unnecessary when a deterministic sequence completed without an error.
35
44
  - Pass the latest \`observationId\` when acting on observed coordinates. If the observation is stale, observe again rather than guessing.