@myagentroam/node 0.10.34 → 0.10.36

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.
@@ -11,7 +11,7 @@ export class SessionQueryService {
11
11
  'session.cli.context': (data) => this.cliContext(record(data)),
12
12
  'session.goal.get': async (data) => {
13
13
  const input = record(data);
14
- const session = await this.requireSession(input.sessionId, input.workspaceId);
14
+ const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
15
15
  return { goal: this.options.goal?.(session.id) ?? null };
16
16
  },
17
17
  'session.runs': (data) => this.runs(record(data)),
@@ -24,7 +24,7 @@ export class SessionQueryService {
24
24
  }
25
25
  async get(input) {
26
26
  return {
27
- session: this.options.present(await this.requireSession(input.sessionId, input.workspaceId))
27
+ session: this.options.present(await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true))
28
28
  };
29
29
  }
30
30
  async cliContext(input) {
@@ -35,7 +35,7 @@ export class SessionQueryService {
35
35
  throw new Error('WORKSPACE_NOT_FOUND');
36
36
  const session = input.sessionId === undefined
37
37
  ? undefined
38
- : await this.requireSession(input.sessionId, workspace.id);
38
+ : await this.requireSession(input.sessionId, workspace.id, input.registeredOnly === true);
39
39
  return {
40
40
  workspace: { id: workspace.id, path: workspace.path },
41
41
  session: session === undefined ? null : this.options.present(session),
@@ -43,12 +43,12 @@ export class SessionQueryService {
43
43
  };
44
44
  }
45
45
  async runs(input) {
46
- const session = await this.requireSession(input.sessionId, input.workspaceId);
46
+ const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
47
47
  return { runs: this.options.runtime.listRunsForSession(session.id, { includeTerminal: true }) };
48
48
  }
49
49
  async turns(input) {
50
50
  const page = pageInput(input);
51
- const session = await this.requireSession(input.sessionId, input.workspaceId);
51
+ const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
52
52
  if (input.unit === 'SEGMENT')
53
53
  return this.options.segments(session, page);
54
54
  if (input.unit !== undefined && input.unit !== 'TURN')
@@ -66,7 +66,7 @@ export class SessionQueryService {
66
66
  return this.options.runtime.listConversationItems({ sessionId: session.id, ...page });
67
67
  }
68
68
  async readToolResource(input) {
69
- const session = await this.requireSession(input.sessionId, input.workspaceId);
69
+ const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
70
70
  if (typeof input.workspaceId !== 'string' || input.workspaceId !== session.workspaceId)
71
71
  throw new Error('SESSION_NOT_FOUND');
72
72
  const resource = toolResultResourceInputSchema.parse({
@@ -80,7 +80,7 @@ export class SessionQueryService {
80
80
  return toolResultResourcePageSchema.parse(await runner.readToolResultResource(session, resource));
81
81
  }
82
82
  async readContext(input) {
83
- return this.options.context.read(await this.requireSession(input.sessionId, input.workspaceId));
83
+ return this.options.context.read(await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true));
84
84
  }
85
85
  async discover(input) {
86
86
  const runner = this.options.runners.product(input.runner);
@@ -96,10 +96,15 @@ export class SessionQueryService {
96
96
  const sessions = await this.options.discover(runner.name, workspaceId);
97
97
  return { sessions: sessions.map((session) => this.options.present(session)) };
98
98
  }
99
- async requireSession(sessionId, workspaceId) {
99
+ async requireSession(sessionId, workspaceId, registeredOnly = false) {
100
100
  if (typeof sessionId !== 'string')
101
101
  throw new Error('SESSION_INVALID');
102
- const session = this.options.runtime.getAgentSession(sessionId) ?? (await this.options.resolve(sessionId));
102
+ const session = this.options.runtime.getAgentSession(sessionId) ??
103
+ (await this.options.resolve(sessionId, registeredOnly));
104
+ if (session === undefined && registeredOnly && typeof workspaceId === 'string')
105
+ throw Object.assign(new Error('SESSION_NOT_FOUND'), {
106
+ publicDetails: this.options.similar(sessionId, workspaceId)
107
+ });
103
108
  if (session === undefined || (workspaceId !== undefined && session.workspaceId !== workspaceId))
104
109
  throw new Error('SESSION_NOT_FOUND');
105
110
  return session;
@@ -63,10 +63,15 @@ export class WorkspaceQueueWorkbenchService {
63
63
  })
64
64
  };
65
65
  }
66
- get(input) {
67
- const session = this.requireSession(input);
66
+ async get(input) {
67
+ if (typeof input.workspaceId !== 'string' || typeof input.sessionId !== 'string')
68
+ throw new Error('WORKSPACE_NOT_FOUND');
69
+ const session = this.options.runtime.getAgentSession(input.sessionId) ??
70
+ (await this.options.resolve(input.sessionId, input.registeredOnly === true));
71
+ if (session?.workspaceId !== input.workspaceId)
72
+ throw new Error('SESSION_NOT_FOUND');
68
73
  return {
69
- queue: this.present(session.workspaceId, session.id)
74
+ queue: this.snapshot(session.id)
70
75
  };
71
76
  }
72
77
  pauseOrResume(mode, input) {
@@ -1,5 +1,5 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { lstat, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { link, lstat, mkdir, open, readFile, rename, rm, stat } from 'node:fs/promises';
3
3
  import { dirname, join, posix, resolve } from 'node:path';
4
4
  import { parseComposerAttachmentUpload, validComposerAttachmentName } from '../util/node-operation-parsers.js';
5
5
  import { preflightWorkspaceUpload, workspaceUploadTemporaryName } from '../workspace.js';
@@ -38,6 +38,8 @@ export class WorkspaceUploadService {
38
38
  !Number.isSafeInteger(input.size) ||
39
39
  input.size < 0 ||
40
40
  typeof input.overwrite !== 'boolean' ||
41
+ (input.sha256 !== undefined &&
42
+ (typeof input.sha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(input.sha256))) ||
41
43
  (attachment !== undefined && input.directory !== undefined))
42
44
  throw new Error('UPLOAD_INVALID');
43
45
  if (attachment !== undefined &&
@@ -59,20 +61,26 @@ export class WorkspaceUploadService {
59
61
  throw new Error('UPLOAD_TARGET_BUSY');
60
62
  const id = randomUUID();
61
63
  const temporaryPath = resolve(dirname(target.absolutePath), workspaceUploadTemporaryName(id));
62
- const handle = await open(temporaryPath, 'wx');
64
+ const parentIdentity = await stat(dirname(target.absolutePath));
65
+ const handle = await open(temporaryPath, 'wx+');
66
+ const stagingIdentity = await handle.stat();
63
67
  const timer = setTimeout(() => {
64
68
  const upload = this.state.uploads.get(id);
65
69
  if (upload !== undefined)
66
- void this.remove(upload);
70
+ void this.remove(upload).catch(() => undefined);
67
71
  }, TTL_MS);
68
72
  const upload = {
69
73
  id,
70
74
  workspaceId: workspace.id,
75
+ workspacePath: workspace.path,
76
+ parentIdentity,
77
+ stagingIdentity,
71
78
  path: target.path,
72
79
  targetPath: target.absolutePath,
73
80
  temporaryPath,
74
81
  size: input.size,
75
82
  overwrite: input.overwrite,
83
+ ...(typeof input.sha256 === 'string' ? { directSha256: input.sha256 } : {}),
76
84
  ...(attachment === undefined ? {} : { composerAttachment: attachment }),
77
85
  handle,
78
86
  receivedBytes: 0,
@@ -88,6 +96,8 @@ export class WorkspaceUploadService {
88
96
  typeof input.dataBase64 !== 'string')
89
97
  throw new Error('UPLOAD_CHUNK_INVALID');
90
98
  const upload = this.require(input.uploadId);
99
+ if (upload.writing)
100
+ throw new Error('UPLOAD_OFFSET_INVALID');
91
101
  if (input.offset !== upload.receivedBytes)
92
102
  throw new Error('UPLOAD_OFFSET_INVALID');
93
103
  const bytes = Buffer.from(input.dataBase64, 'base64');
@@ -95,18 +105,42 @@ export class WorkspaceUploadService {
95
105
  throw new Error('UPLOAD_CHUNK_INVALID');
96
106
  if (upload.receivedBytes + bytes.length > upload.size)
97
107
  throw new Error('UPLOAD_SIZE_INVALID');
98
- const result = await upload.handle.write(bytes, 0, bytes.length, upload.receivedBytes);
99
- if (result.bytesWritten !== bytes.length)
100
- throw new Error('UPLOAD_WRITE_FAILED');
101
- upload.receivedBytes += bytes.length;
102
- return this.status(upload.id);
108
+ upload.writing = true;
109
+ try {
110
+ const result = await upload.handle.write(bytes, 0, bytes.length, upload.receivedBytes);
111
+ if (result.bytesWritten !== bytes.length)
112
+ throw new Error('UPLOAD_WRITE_FAILED');
113
+ upload.receivedBytes += bytes.length;
114
+ return this.status(upload.id);
115
+ }
116
+ finally {
117
+ upload.writing = false;
118
+ }
103
119
  }
104
120
  async createDirect(workspace, input) {
105
121
  const normalized = input.path.replaceAll('\\', '/');
122
+ const existing = [...this.state.uploads.values()].find((upload) => input.composerAttachment === undefined &&
123
+ upload.composerAttachment === undefined &&
124
+ upload.workspaceId === workspace.id &&
125
+ upload.path === normalized &&
126
+ upload.size === input.size &&
127
+ upload.overwrite === input.overwrite &&
128
+ upload.directSha256 === input.sha256);
129
+ if (existing !== undefined) {
130
+ const target = await preflightWorkspaceUpload(workspace.path, posix.dirname(normalized), posix.basename(normalized));
131
+ if (target.absolutePath !== existing.targetPath ||
132
+ target.path !== existing.path ||
133
+ target.status === 'INVALID_NAME' ||
134
+ target.status === 'EXISTS_DIRECTORY' ||
135
+ (target.status === 'EXISTS_FILE' && !existing.overwrite))
136
+ throw new Error('UPLOAD_TARGET_INVALID');
137
+ return this.status(existing.id);
138
+ }
106
139
  const status = await this.create(workspace, {
107
140
  name: posix.basename(normalized),
108
141
  size: input.size,
109
142
  overwrite: input.overwrite,
143
+ sha256: input.sha256,
110
144
  ...(input.composerAttachment === undefined
111
145
  ? { directory: posix.dirname(normalized) }
112
146
  : {
@@ -124,11 +158,21 @@ export class WorkspaceUploadService {
124
158
  });
125
159
  return status.receivedBytes;
126
160
  }
127
- async completeDirect(uploadId, size) {
161
+ async completeDirect(uploadId, size, signal) {
128
162
  const upload = this.require(uploadId);
129
- if (size !== upload.size)
163
+ if (size !== upload.size || upload.receivedBytes !== upload.size)
130
164
  throw new Error('UPLOAD_SIZE_INVALID');
131
- await this.complete(uploadId);
165
+ const hash = createHash('sha256');
166
+ for await (const bytes of upload.handle.createReadStream({ start: 0, autoClose: false })) {
167
+ this.assertActive(upload, signal);
168
+ hash.update(bytes);
169
+ }
170
+ this.assertActive(upload, signal);
171
+ if (hash.digest('hex') !== upload.directSha256) {
172
+ await this.remove(upload);
173
+ throw new Error('DIRECT_INTEGRITY_FAILED');
174
+ }
175
+ await this.complete(uploadId, signal);
132
176
  }
133
177
  status(uploadId) {
134
178
  const upload = this.require(uploadId);
@@ -139,27 +183,28 @@ export class WorkspaceUploadService {
139
183
  size: upload.size
140
184
  };
141
185
  }
142
- async complete(uploadId) {
186
+ async complete(uploadId, signal) {
143
187
  const upload = this.require(uploadId);
144
188
  if (upload.receivedBytes !== upload.size)
145
189
  throw new Error('UPLOAD_INCOMPLETE');
146
190
  await upload.handle.sync();
191
+ this.assertActive(upload, signal);
147
192
  await upload.handle.close();
148
- const existing = await lstat(upload.targetPath).catch((error) => error.code === 'ENOENT' ? undefined : Promise.reject(error));
149
- if (existing?.isSymbolicLink())
150
- throw new Error('FILE_PATH_ESCAPE');
151
- if (existing !== undefined && (!existing.isFile() || !upload.overwrite))
152
- throw new Error('UPLOAD_TARGET_EXISTS');
153
- try {
193
+ await this.validateCommit(upload);
194
+ this.assertActive(upload, signal);
195
+ if (upload.overwrite) {
154
196
  await rename(upload.temporaryPath, upload.targetPath);
155
197
  }
156
- catch (error) {
157
- if (error.code !== 'EEXIST')
198
+ else {
199
+ try {
200
+ await link(upload.temporaryPath, upload.targetPath);
201
+ }
202
+ catch (error) {
203
+ if (error.code === 'EEXIST')
204
+ throw new Error('UPLOAD_TARGET_EXISTS');
158
205
  throw error;
159
- if (!upload.overwrite)
160
- throw new Error('UPLOAD_TARGET_EXISTS');
161
- await rm(upload.targetPath, { force: false });
162
- await rename(upload.temporaryPath, upload.targetPath);
206
+ }
207
+ await rm(upload.temporaryPath);
163
208
  }
164
209
  clearTimeout(upload.timer);
165
210
  this.state.uploads.delete(upload.id);
@@ -284,17 +329,68 @@ export class WorkspaceUploadService {
284
329
  if (typeof id !== 'string')
285
330
  throw new Error('UPLOAD_NOT_FOUND');
286
331
  const upload = this.state.uploads.get(id);
287
- if (upload === undefined)
332
+ if (upload === undefined || upload.cancelled)
288
333
  throw new Error('UPLOAD_NOT_FOUND');
289
334
  return upload;
290
335
  }
336
+ assertActive(upload, signal) {
337
+ if (this.state.uploads.get(upload.id) !== upload || upload.cancelled || signal?.aborted)
338
+ throw new Error('UPLOAD_CANCELLED');
339
+ }
340
+ async validateCommit(upload) {
341
+ if (upload.composerAttachment === undefined) {
342
+ const target = await preflightWorkspaceUpload(upload.workspacePath, posix.dirname(upload.path), posix.basename(upload.path));
343
+ if (target.absolutePath !== upload.targetPath || target.path !== upload.path)
344
+ throw new Error('UPLOAD_TARGET_INVALID');
345
+ if (target.status === 'EXISTS_DIRECTORY')
346
+ throw new Error('UPLOAD_TARGET_DIRECTORY');
347
+ if (target.status === 'INVALID_NAME')
348
+ throw new Error('UPLOAD_NAME_INVALID');
349
+ if (target.status === 'EXISTS_FILE' && !upload.overwrite)
350
+ throw new Error('UPLOAD_TARGET_EXISTS');
351
+ }
352
+ else {
353
+ const target = await lstat(upload.targetPath).catch((error) => {
354
+ if (error.code === 'ENOENT')
355
+ return undefined;
356
+ throw error;
357
+ });
358
+ if (target?.isSymbolicLink())
359
+ throw new Error('FILE_PATH_ESCAPE');
360
+ if (target !== undefined && (!target.isFile() || !upload.overwrite))
361
+ throw new Error('UPLOAD_TARGET_EXISTS');
362
+ }
363
+ const parent = await lstat(dirname(upload.targetPath));
364
+ if (!parent.isDirectory() ||
365
+ parent.isSymbolicLink() ||
366
+ !sameIdentity(parent, upload.parentIdentity))
367
+ throw new Error('UPLOAD_TARGET_INVALID');
368
+ const staging = await lstat(upload.temporaryPath);
369
+ if (!staging.isFile() ||
370
+ staging.isSymbolicLink() ||
371
+ !sameIdentity(staging, upload.stagingIdentity) ||
372
+ staging.size !== upload.size)
373
+ throw new Error('UPLOAD_STAGING_INVALID');
374
+ }
291
375
  async remove(upload) {
292
- this.state.uploads.delete(upload.id);
376
+ upload.cancelled = true;
293
377
  clearTimeout(upload.timer);
294
378
  await upload.handle.close().catch(() => undefined);
295
- await rm(upload.temporaryPath, { force: true }).catch(() => undefined);
379
+ const parent = await lstat(dirname(upload.temporaryPath));
380
+ const staging = await lstat(upload.temporaryPath).catch((error) => {
381
+ if (error.code === 'ENOENT')
382
+ return undefined;
383
+ throw error;
384
+ });
385
+ if (!sameIdentity(parent, upload.parentIdentity) ||
386
+ parent.isSymbolicLink() ||
387
+ (staging !== undefined && !sameIdentity(staging, upload.stagingIdentity)))
388
+ throw new Error('UPLOAD_CLEANUP_FAILED');
389
+ if (staging !== undefined)
390
+ await rm(upload.temporaryPath, { force: true });
296
391
  if (upload.composerAttachment !== undefined)
297
- await rm(dirname(upload.targetPath), { recursive: true, force: true }).catch(() => undefined);
392
+ await rm(dirname(upload.targetPath), { recursive: true, force: true });
393
+ this.state.uploads.delete(upload.id);
298
394
  }
299
395
  async removeCompleted(attachment) {
300
396
  if (this.state.composerAttachments.get(attachment.id) !== attachment)
@@ -331,6 +427,9 @@ export class WorkspaceUploadService {
331
427
  };
332
428
  }
333
429
  }
430
+ function sameIdentity(actual, expected) {
431
+ return actual.dev === expected.dev && actual.ino === expected.ino;
432
+ }
334
433
  async function safeDirectory(path) {
335
434
  const value = await lstat(path);
336
435
  if (!value.isDirectory() || value.isSymbolicLink())
@@ -14,8 +14,10 @@ export async function superviseNode(configPath, bootstrapEntrypoint) {
14
14
  let entrypoint = await readActiveEntrypoint(activePath, bootstrapEntrypoint);
15
15
  for (;;) {
16
16
  const childResult = await runProcess(process.execPath, [entrypoint, 'run', '--config', configPath], true, { ...process.env, MAR_NODE_SUPERVISED: '1' });
17
- if (childResult.signalled)
17
+ if (childResult.signalled) {
18
+ process.exitCode = childResult.code ?? 1;
18
19
  return;
20
+ }
19
21
  const code = childResult.code;
20
22
  if (code !== UPGRADE_EXIT_CODE) {
21
23
  if (code === 0)
@@ -102,6 +104,8 @@ async function runProcess(command, args, inherit, env = process.env) {
102
104
  let settled = false;
103
105
  let signalled = false;
104
106
  const forward = (signal) => {
107
+ if (signalled)
108
+ return;
105
109
  signalled = true;
106
110
  child.kill(signal);
107
111
  };
@@ -115,8 +119,8 @@ async function runProcess(command, args, inherit, env = process.env) {
115
119
  process.off('SIGTERM', onSigterm);
116
120
  resolveProcess({ code, signalled });
117
121
  };
118
- process.once('SIGINT', onSigint);
119
- process.once('SIGTERM', onSigterm);
122
+ process.on('SIGINT', onSigint);
123
+ process.on('SIGTERM', onSigterm);
120
124
  child.once('error', () => finish(null));
121
125
  child.once('close', finish);
122
126
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.10.34",
3
+ "version": "0.10.36",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -37,10 +37,10 @@
37
37
  "playwright": "1.62.1",
38
38
  "ws": "^8.21.3",
39
39
  "zod": "4.4.3",
40
- "@myagentroam/agent": "0.10.34",
40
+ "@myagentroam/agent": "0.10.36",
41
41
  "@myagentroam/code-navigation": "0.10.1",
42
- "@myagentroam/marctl": "0.10.5",
43
- "@myagentroam/protocol": "0.10.34"
42
+ "@myagentroam/marctl": "0.10.6",
43
+ "@myagentroam/protocol": "0.10.36"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/ws": "^8.18.1"