@myagentroam/node 0.9.154 → 0.9.156

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/dist/connector.js CHANGED
@@ -601,6 +601,7 @@ export class NodeConnector {
601
601
  }
602
602
  }, {
603
603
  modelFactory: this.marAgentRuntimeModels,
604
+ retainEnvironment: (environment) => this.gitExecution().retain(environment),
604
605
  computerUse: this.computerUseProvider
605
606
  });
606
607
  this.marAgentClient.setSessionResourceReleaser((sessionId) => this.marAgentRunController.releaseSession(sessionId));
@@ -717,8 +718,9 @@ export class NodeConnector {
717
718
  });
718
719
  this.runWorkbenchService = new RunWorkbenchService({
719
720
  runtime: this.runtime,
720
- readImage: async (runId, imageIndex) => this.runAttachmentService.read(runId, imageIndex) ??
721
- (await this.runAttachmentService.readNative(runId, imageIndex)),
721
+ readImage: async (runId, selector) => typeof selector === 'number'
722
+ ? this.runAttachmentService.read(runId, selector)
723
+ : this.runAttachmentService.readNative(runId, selector),
722
724
  respondUserInput: (runId, requestId, answers) => this.runnerInteractionService.respondUserInput(runId, requestId, answers),
723
725
  pauseQueue: (sessionId) => this.workspaceQueueWorkbenchService.pauseSessionQueue(sessionId),
724
726
  runInterrupted: (sessionId) => {
@@ -972,6 +974,7 @@ export class NodeConnector {
972
974
  projection: this.nativeSessionProjectionService,
973
975
  resolveForCommand: (sessionId) => this.externalSessionResumeService.resolveForMessage(sessionId),
974
976
  profiles: () => this.runnerService.profiles(),
977
+ mcps: (workspaceId) => this.database?.effectiveMcpInstallations(workspaceId) ?? [],
975
978
  present: (session) => this.sessionPresentationService.present(session),
976
979
  resumeQueueAfterCommandRun: (sessionId) => this.workspaceQueueWorkbenchService.resumeAfterCommandRun(sessionId),
977
980
  invalidateHistory: (sessionId) => this.nativeSessionWatchService.invalidate(sessionId),
@@ -1556,8 +1559,7 @@ export class NodeConnector {
1556
1559
  emitWatchEvent: (payload) => this.send('watch.event', payload),
1557
1560
  nextWorkbenchEvent: (payload) => this.workbenchEventService.next('hosted-conversation', payload),
1558
1561
  emitWorkbenchEvent: (event) => this.workbenchEventService.publish(event),
1559
- readImage: async (runId, imageIndex) => this.runAttachmentService.read(runId, imageIndex) ??
1560
- (await this.runAttachmentService.readNative(runId, imageIndex)),
1562
+ readImage: (runId, imageRef) => this.runAttachmentService.readNative(runId, imageRef),
1561
1563
  processExecutorFactory: (input) => new BubblewrapSandboxExecutor({
1562
1564
  profile: hostedConversationSandboxProfile(input)
1563
1565
  }),
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
3
3
  import { nodeExecutionEnvironment } from '../node-execution-environment.js';
4
+ import { canonicalJson } from '../util/canonical-json.js';
4
5
  /**
5
6
  * Stable lifecycle boundary for every Node Runner implementation.
6
7
  *
@@ -238,7 +239,7 @@ function resolvedMcpServers(raw, secrets) {
238
239
  continue;
239
240
  const configuration = isRecord(record['configuration']) ? record['configuration'] : {};
240
241
  const sourceIdentity = createHash('sha256')
241
- .update(JSON.stringify({
242
+ .update(canonicalJson({
242
243
  id: record['id'] ?? null,
243
244
  name: record['serverName'],
244
245
  runtime: runtime.data,
@@ -26,6 +26,7 @@ export function marAgentGeneratedImageSources(homeDirectory, sessionId, artifact
26
26
  {
27
27
  name: artifact.name,
28
28
  mime: artifact.mime,
29
+ sourceId: `${sessionId}:${artifact.fileName}`,
29
30
  localPath: resolve(directory, artifact.fileName)
30
31
  }
31
32
  ]
@@ -9,6 +9,7 @@ import { LocalAgentHost, createMarAgent, generateSessionTitle } from '@myagentro
9
9
  import { marAgentReasoningEffortSchema, marAgentRuntimeModelConfigurationSchema } from '@myagentroam/protocol';
10
10
  import { boundedConversationItemId } from '../codex/conversation-parser.js';
11
11
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
12
+ import { canonicalJson } from '../../util/canonical-json.js';
12
13
  import { MarAgentMcpClientCache } from './mcp-client-pool.js';
13
14
  import { MarAgentConversationNormalizer } from './conversation-normalizer.js';
14
15
  import { parseRunnerImageAttachments } from '../../util/node-operation-parsers.js';
@@ -29,6 +30,7 @@ export class MarAgentManagedRunController {
29
30
  #questions = new Map();
30
31
  #normalizers = new Map();
31
32
  #resourceIdleTtlMs;
33
+ #retainEnvironment;
32
34
  #createAgent;
33
35
  #modelFactory;
34
36
  #computerUse;
@@ -39,6 +41,7 @@ export class MarAgentManagedRunController {
39
41
  this.runner = runner;
40
42
  this.host = host;
41
43
  this.#resourceIdleTtlMs = options.resourceIdleTtlMs ?? 10 * 60_000;
44
+ this.#retainEnvironment = options.retainEnvironment;
42
45
  this.#createAgent = options.createAgent ?? createMarAgent;
43
46
  this.#modelFactory =
44
47
  options.modelFactory ??
@@ -139,7 +142,7 @@ export class MarAgentManagedRunController {
139
142
  mode: command.id,
140
143
  secretEnvironment: context.secretEnvironment,
141
144
  externalSessionId: session.externalSessionId,
142
- mcpServers: [],
145
+ mcpServers: this.#resolveMcpServers(context.mcpInstallations, context.secretEnvironment),
143
146
  images: [],
144
147
  ...(context.initiatedByUserId === undefined
145
148
  ? {}
@@ -173,13 +176,11 @@ export class MarAgentManagedRunController {
173
176
  try {
174
177
  secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
175
178
  const mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
176
- mcpServers = this.runner.mcpConfiguration(payload.mcpInstallations, {
179
+ mcpServers = this.#resolveMcpServers(payload.mcpInstallations, {
177
180
  ...secretEnvironment,
178
181
  ...mcpSecretEnvironment,
179
182
  MAR_USERNAME: secretEnvironment['MAR_USERNAME'] ?? ''
180
183
  });
181
- if (mcpServers.some((server) => server.name === 'computer'))
182
- throw new Error('MAR_AGENT_MCP_RESERVED_NAMESPACE');
183
184
  images = parseRunnerImageAttachments(payload.attachments);
184
185
  }
185
186
  catch {
@@ -566,7 +567,8 @@ export class MarAgentManagedRunController {
566
567
  throw error;
567
568
  }
568
569
  }
569
- const personalInstructions = input.personalInstructions ?? '';
570
+ const personalInstructions = input.personalInstructions ??
571
+ (input.mode === 'compact' ? existing.personalInstructions : '');
570
572
  if (existing.personalInstructions !== personalInstructions) {
571
573
  existing.agent.setSystemInstruction(personalInstructions || undefined);
572
574
  existing.personalInstructions = personalInstructions;
@@ -608,7 +610,9 @@ export class MarAgentManagedRunController {
608
610
  let agent;
609
611
  let runtime;
610
612
  let models = [];
613
+ let releaseEnvironment;
611
614
  try {
615
+ releaseEnvironment = this.#retainEnvironment?.(input.secretEnvironment ?? {});
612
616
  const mcp = mcpLease.pool;
613
617
  const localHost = new LocalAgentHost({
614
618
  mode: 'node',
@@ -646,6 +650,7 @@ export class MarAgentManagedRunController {
646
650
  host: localHost,
647
651
  normalizer: new MarAgentConversationNormalizer(),
648
652
  mcpLease,
653
+ releaseEnvironment,
649
654
  automaticTitleEnabled: input.externalSessionId === undefined,
650
655
  models,
651
656
  modelFingerprint: sessionModelFingerprint(input),
@@ -667,9 +672,16 @@ export class MarAgentManagedRunController {
667
672
  await agent?.dispose().catch(() => undefined);
668
673
  await closeModelCredentialProviders(models);
669
674
  await mcpLease.release().catch(() => undefined);
675
+ releaseEnvironment?.();
670
676
  throw error;
671
677
  }
672
678
  }
679
+ #resolveMcpServers(installations, environment) {
680
+ const servers = this.runner.mcpConfiguration(installations, environment);
681
+ if (servers.some((server) => server.name === 'computer'))
682
+ throw new Error('MAR_AGENT_MCP_RESERVED_NAMESPACE');
683
+ return servers;
684
+ }
673
685
  #runtimeInUse(runtime) {
674
686
  return (runtime.activeRunId !== undefined ||
675
687
  [...this.#runtimeAcquisitions.keys()].some((key) => this.#sessionRuntimes.get(key) === runtime));
@@ -737,6 +749,7 @@ export class MarAgentManagedRunController {
737
749
  }
738
750
  finally {
739
751
  await runtime.mcpLease.release().catch(() => undefined);
752
+ runtime.releaseEnvironment?.();
740
753
  for (const [sessionId, candidate] of this.#sessionRuntimes)
741
754
  if (candidate === runtime)
742
755
  this.#sessionRuntimes.delete(sessionId);
@@ -837,7 +850,7 @@ function isExecutionScopedMcp(server) {
837
850
  }
838
851
  function sessionHostFingerprint(input, hostConfiguration) {
839
852
  return createHash('sha256')
840
- .update(JSON.stringify({
853
+ .update(canonicalJson({
841
854
  cwd: input.cwd,
842
855
  mcpServers: input.mcpServers.filter((server) => !isExecutionScopedMcp(server)),
843
856
  environment: Object.entries(hostConfiguration.environment)
@@ -4,6 +4,7 @@ import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotoc
4
4
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5
5
  import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
6
6
  import { assertMcpToolDefinition, assertMcpToolCatalog, MarAgentError } from '@myagentroam/agent';
7
+ import { canonicalJson } from '../../util/canonical-json.js';
7
8
  import { ExecutorStdioClientTransport } from './executor-stdio-client-transport.js';
8
9
  export class MarAgentMcpClientCache {
9
10
  #entries = new Map();
@@ -444,9 +445,7 @@ export class MarAgentMcpClientPool {
444
445
  }
445
446
  }
446
447
  function fingerprint(value) {
447
- return createHash('sha256')
448
- .update(JSON.stringify(canonicalValue(value)))
449
- .digest('hex');
448
+ return createHash('sha256').update(canonicalJson(value)).digest('hex');
450
449
  }
451
450
  function singleUseLease(pool) {
452
451
  let released = false;
@@ -462,23 +461,14 @@ function singleUseLease(pool) {
462
461
  }
463
462
  function mcpConfigurationFingerprint(servers, cwd, authorizationScope) {
464
463
  return createHash('sha256')
465
- .update(JSON.stringify(canonicalValue({
464
+ .update(canonicalJson({
466
465
  cwd,
467
466
  authorizationScope,
468
467
  inheritedEnvironment: getDefaultEnvironment(),
469
468
  servers
470
- })))
469
+ }))
471
470
  .digest('hex');
472
471
  }
473
- function canonicalValue(value) {
474
- if (Array.isArray(value))
475
- return value.map(canonicalValue);
476
- if (value === null || typeof value !== 'object')
477
- return value;
478
- return Object.fromEntries(Object.entries(value)
479
- .sort(([left], [right]) => left.localeCompare(right))
480
- .map(([key, entry]) => [key, canonicalValue(entry)]));
481
- }
482
472
  function safeName(value) {
483
473
  return value.replace(/[^a-zA-Z0-9_-]/gu, '_').slice(0, 64);
484
474
  }
@@ -31,11 +31,13 @@ export class GitExecutionService {
31
31
  const parsed = personalGitRuntimeSchema.safeParse(configuration);
32
32
  if (!parsed.success)
33
33
  throw new Error('GIT_CONFIGURATION_INVALID');
34
+ // Execution credentials rotate independently of the user's stable Git capability.
35
+ const stableEnvironment = Object.fromEntries(Object.entries(baseEnvironment).filter(([name]) => name !== 'MAR_CTL_USER_TOKEN'));
34
36
  const key = createHash('sha256')
35
37
  .update(JSON.stringify([
36
38
  userId,
37
39
  parsed.data,
38
- Object.entries(baseEnvironment).sort(([a], [b]) => a.localeCompare(b))
40
+ Object.entries(stableEnvironment).sort(([a], [b]) => a.localeCompare(b))
39
41
  ]))
40
42
  .digest('hex');
41
43
  const endpoint = await this.start();
@@ -44,10 +46,10 @@ export class GitExecutionService {
44
46
  let context = this.contexts.get(key);
45
47
  if (context === undefined) {
46
48
  const token = randomBytes(32).toString('hex');
47
- const original = { ...process.env, ...baseEnvironment };
49
+ const original = { ...process.env, ...stableEnvironment };
48
50
  const binary = await gitExecutable(original);
49
51
  const environment = {
50
- ...baseEnvironment,
52
+ ...stableEnvironment,
51
53
  MAR_GIT_ENDPOINT: endpoint,
52
54
  MAR_GIT_TOKEN: token
53
55
  };
@@ -77,9 +79,12 @@ export class GitExecutionService {
77
79
  environment[pathKey] =
78
80
  `${await this.gitWrapper(binary)}${delimiter}${original[pathKey] ?? ''}`;
79
81
  const restore = Object.fromEntries(Object.keys(environment)
80
- .filter((name) => environment[name] !== baseEnvironment[name] || name.startsWith('MAR_GIT_'))
82
+ .filter((name) => environment[name] !== stableEnvironment[name] || name.startsWith('MAR_GIT_'))
81
83
  .map((name) => [name, name.startsWith('MAR_GIT_') ? null : (original[name] ?? null)]));
82
- context = {
84
+ if (this.closed)
85
+ throw new Error('GIT_EXECUTION_CLOSED');
86
+ // Binary discovery and wrapper creation yield; another acquire may have published it.
87
+ context = this.contexts.get(key) ?? {
83
88
  key,
84
89
  token,
85
90
  configuration: parsed.data,
@@ -89,9 +94,12 @@ export class GitExecutionService {
89
94
  references: 0
90
95
  };
91
96
  this.contexts.set(key, context);
92
- this.tokens.set(token, context);
97
+ this.tokens.set(context.token, context);
93
98
  }
94
- return { environment: context.environment, release: this.reference(context) };
99
+ return {
100
+ environment: Object.freeze({ ...baseEnvironment, ...context.environment }),
101
+ release: this.reference(context)
102
+ };
95
103
  }
96
104
  retain(environment) {
97
105
  if (environment.MAR_GIT_TOKEN === undefined)
@@ -112,7 +120,7 @@ export class GitExecutionService {
112
120
  released = true;
113
121
  if (--context.references !== 0 || this.closed)
114
122
  return;
115
- // This outlives the existing 60-second warm Runner window.
123
+ // Warm runtimes retain their own reference; this grace period starts after the last owner.
116
124
  context.timer = setTimeout(() => this.retire(context), this.options.idleTimeoutMs ?? 70_000);
117
125
  context.timer.unref();
118
126
  };
@@ -879,7 +879,7 @@ export class HostedConversationNodeService {
879
879
  this.options.store.requireConversation(payload.userId, input.conversationId);
880
880
  if (input.runId !== hostedConversationImageRunId(input.conversationId))
881
881
  throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
882
- const image = await this.options.readImage(input.runId, input.imageIndex);
882
+ const image = await this.options.readImage(input.runId, input.imageRef);
883
883
  if (image === undefined)
884
884
  throw new Error('RUN_IMAGE_NOT_FOUND');
885
885
  return { image };
@@ -1,10 +1,12 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { tmpdir } from 'node:os';
2
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
4
  import { lstat, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
5
+ import { imageRefSchema } from '@myagentroam/protocol';
4
6
  import { AttachmentDomainState } from './attachment-domain-state.js';
5
7
  const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
6
8
  const IMAGE_CACHE_TTL_MS = 60 * 60_000;
7
- const NATIVE_IMAGE_INDEX_TTL_MS = 60 * 60_000;
9
+ const NATIVE_IMAGE_SOURCE_TTL_MS = 60 * 60_000;
8
10
  export class RunAttachmentService {
9
11
  nativeImageRoots;
10
12
  state = new AttachmentDomainState();
@@ -140,7 +142,7 @@ export class RunAttachmentService {
140
142
  }
141
143
  registerNativeImages(runId, nativeRoots, images, options = {}) {
142
144
  const roots = nativeRoots.map((root) => resolve(root));
143
- const sources = this.state.nativeHistoryImages.get(runId) ?? [];
145
+ const sources = this.state.nativeHistoryImages.get(runId) ?? new Map();
144
146
  const attachments = [];
145
147
  for (const image of images) {
146
148
  const cached = options.reuseCachedRunImages === false || image.localPath === undefined
@@ -157,7 +159,11 @@ export class RunAttachmentService {
157
159
  attachments.push({ name: image.name, mime: image.mime });
158
160
  continue;
159
161
  }
160
- let imageIndex = sources.findIndex((source) => source.kind === 'reader' && source.sourceId === image.sourceId);
162
+ const imageRef = nativeImageReference('reader', image.sourceId);
163
+ if (imageRef === undefined) {
164
+ attachments.push({ name: image.name, mime: image.mime });
165
+ continue;
166
+ }
161
167
  const source = {
162
168
  kind: 'reader',
163
169
  name: image.name,
@@ -166,18 +172,13 @@ export class RunAttachmentService {
166
172
  sourceId: image.sourceId,
167
173
  read: image.read
168
174
  };
169
- if (imageIndex < 0) {
170
- imageIndex = sources.length;
171
- sources.push(source);
172
- }
173
- else
174
- sources[imageIndex] = source;
175
+ sources.set(imageRef, source);
175
176
  attachments.push({
176
177
  name: image.name,
177
178
  mime: image.mime,
178
179
  ...(image.byteSize === undefined ? {} : { byteSize: image.byteSize }),
179
180
  runId,
180
- imageIndex
181
+ imageRef
181
182
  });
182
183
  continue;
183
184
  }
@@ -198,14 +199,24 @@ export class RunAttachmentService {
198
199
  attachments.push({ name: image.name, mime: image.mime });
199
200
  continue;
200
201
  }
201
- let imageIndex = sources.findIndex((source) => source.kind === 'file' && source.path === path);
202
- if (imageIndex < 0) {
203
- imageIndex = sources.length;
204
- sources.push({ kind: 'file', name: image.name, mime: image.mime, path, root });
202
+ // Derive identity from the original source, never its registration position.
203
+ // The path digest hides physical paths; it is not a content/version hash.
204
+ const imageRef = nativeImageReference('file', image.sourceId === undefined
205
+ ? createHash('sha256').update(path).digest('hex')
206
+ : image.sourceId);
207
+ if (imageRef === undefined) {
208
+ attachments.push({ name: image.name, mime: image.mime });
209
+ continue;
205
210
  }
206
- attachments.push({ name: image.name, mime: image.mime, runId, imageIndex });
211
+ const previous = sources.get(imageRef);
212
+ if (previous !== undefined && (previous.kind !== 'file' || previous.path !== path)) {
213
+ attachments.push({ name: image.name, mime: image.mime });
214
+ continue;
215
+ }
216
+ sources.set(imageRef, { kind: 'file', name: image.name, mime: image.mime, path, root });
217
+ attachments.push({ name: image.name, mime: image.mime, runId, imageRef });
207
218
  }
208
- if (sources.length > 0) {
219
+ if (sources.size > 0) {
209
220
  this.state.nativeHistoryImages.set(runId, sources);
210
221
  const previousTimer = this.state.nativeHistoryImageTimers.get(runId);
211
222
  if (previousTimer !== undefined)
@@ -213,7 +224,7 @@ export class RunAttachmentService {
213
224
  const timer = setTimeout(() => {
214
225
  this.state.nativeHistoryImageTimers.delete(runId);
215
226
  this.state.nativeHistoryImages.delete(runId);
216
- }, NATIVE_IMAGE_INDEX_TTL_MS);
227
+ }, NATIVE_IMAGE_SOURCE_TTL_MS);
217
228
  timer.unref();
218
229
  this.state.nativeHistoryImageTimers.set(runId, timer);
219
230
  }
@@ -242,8 +253,8 @@ export class RunAttachmentService {
242
253
  }
243
254
  return undefined;
244
255
  }
245
- async readNative(runId, imageIndex) {
246
- const source = this.state.nativeHistoryImages.get(runId)?.[imageIndex];
256
+ async readNative(runId, imageRef) {
257
+ const source = this.state.nativeHistoryImages.get(runId)?.get(imageRef);
247
258
  if (source === undefined)
248
259
  return undefined;
249
260
  try {
@@ -308,6 +319,15 @@ export class RunAttachmentService {
308
319
  this.state.nativeHistoryImages.clear();
309
320
  }
310
321
  }
322
+ function nativeImageReference(kind, identity) {
323
+ try {
324
+ const reference = `${kind}:${encodeURIComponent(identity)}`;
325
+ return imageRefSchema.safeParse(reference).success ? reference : undefined;
326
+ }
327
+ catch {
328
+ return undefined;
329
+ }
330
+ }
311
331
  function canonicalBase64(value) {
312
332
  if (value.length === 0)
313
333
  return false;
@@ -1,3 +1,5 @@
1
+ import { runImageReadInputSchema } from '@myagentroam/protocol';
2
+ import { nodeOperationData } from '../util/node-operation-data.js';
1
3
  import { isTerminalRunStatus } from '../util/node-operation-parsers.js';
2
4
  import { isPlainRecord } from '../runner/codex/conversation-parser.js';
3
5
  import { isHostedConversationImageRunId } from '../runner/mar-agent/generated-images.js';
@@ -22,14 +24,17 @@ export class RunWorkbenchService {
22
24
  return { run: this.requireRun(input.runId) };
23
25
  }
24
26
  async image(input) {
25
- if (typeof input.runId !== 'string' ||
26
- typeof input.imageIndex !== 'number' ||
27
- !Number.isInteger(input.imageIndex) ||
28
- input.imageIndex < 0)
27
+ const parsed = runImageReadInputSchema.safeParse(nodeOperationData(input));
28
+ if (!parsed.success)
29
+ throw new Error('RUN_IMAGE_INVALID');
30
+ const reference = parsed.data;
31
+ if (isHostedConversationImageRunId(reference.runId))
29
32
  throw new Error('RUN_IMAGE_INVALID');
30
- if (isHostedConversationImageRunId(input.runId))
33
+ const native = reference.runId.startsWith('native-history:');
34
+ if (native !== 'imageRef' in reference)
31
35
  throw new Error('RUN_IMAGE_INVALID');
32
- const image = await this.options.readImage(input.runId, input.imageIndex);
36
+ const selector = 'imageRef' in reference ? reference.imageRef : reference.imageIndex;
37
+ const image = await this.options.readImage(reference.runId, selector);
33
38
  if (image === undefined)
34
39
  throw new Error('RUN_IMAGE_NOT_FOUND');
35
40
  return { image };
@@ -106,6 +106,7 @@ export class SessionCommandService {
106
106
  }
107
107
  const result = await runner.executeCommand(session, command, invocation.input, {
108
108
  secretEnvironment,
109
+ mcpInstallations: this.options.mcps(session.workspaceId),
109
110
  ...(input.marAgentModel === undefined ? {} : { marAgentModel: input.marAgentModel }),
110
111
  ...(input.marAgentModels === undefined ? {} : { marAgentModels: input.marAgentModels }),
111
112
  ...(input.marAgentRuntimeScopeId === undefined
@@ -0,0 +1,13 @@
1
+ /** Canonicalizes JSON dictionary keys without changing array order or values. */
2
+ export function canonicalJson(value) {
3
+ return JSON.stringify(canonicalValue(value));
4
+ }
5
+ function canonicalValue(value) {
6
+ if (Array.isArray(value))
7
+ return value.map(canonicalValue);
8
+ if (value === null || typeof value !== 'object')
9
+ return value;
10
+ return Object.fromEntries(Object.entries(value)
11
+ .sort(([left], [right]) => left.localeCompare(right))
12
+ .map(([key, entry]) => [key, canonicalValue(entry)]));
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.154",
3
+ "version": "0.9.156",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -36,10 +36,10 @@
36
36
  "playwright": "1.62.1",
37
37
  "ws": "^8.21.3",
38
38
  "zod": "4.4.3",
39
- "@myagentroam/agent": "0.9.154",
40
- "@myagentroam/protocol": "0.9.154",
39
+ "@myagentroam/agent": "0.9.156",
41
40
  "@myagentroam/marctl": "0.9.120",
42
- "@myagentroam/code-navigation": "0.9.129"
41
+ "@myagentroam/code-navigation": "0.9.129",
42
+ "@myagentroam/protocol": "0.9.156"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/ws": "^8.18.1"