@myagentroam/node 0.9.77 → 0.9.78

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
@@ -79,7 +79,8 @@ import { NodeUpgradeCoordinator, nodeUpgradeRequestPath } from './service/node-u
79
79
  import { HostedConversationStore } from './service/hosted-conversation-store.js';
80
80
  import { ConversationTemplateNodeService } from './service/conversation-template-node-service.js';
81
81
  import { HostedConversationNodeService } from './service/hosted-conversation-node-service.js';
82
- import { BubblewrapSandboxExecutor } from './service/bubblewrap-sandbox-executor.js';
82
+ import { BubblewrapSandboxExecutor } from '@myagentroam/agent/sandbox/linux';
83
+ import { hostedConversationSandboxProfile } from './service/hosted-conversation-sandbox-profile.js';
83
84
  import { MarAgentHostedConversationRuntime } from './runner/mar-agent/hosted-conversation-runtime.js';
84
85
  import { conversationHostingCapability, probeBubblewrap } from './service/conversation-hosting-capability-service.js';
85
86
  import { ConversationHostingMaintenanceService } from './service/conversation-hosting-maintenance-service.js';
@@ -103,6 +104,7 @@ const HOSTED_CONVERSATION_OPERATIONS = [
103
104
  'conversation.hosted.list',
104
105
  'conversation.hosted.create',
105
106
  'conversation.hosted.get',
107
+ 'conversation.hosted.history',
106
108
  'conversation.hosted.watch',
107
109
  'conversation.hosted.unwatch',
108
110
  'conversation.hosted.message',
@@ -1310,7 +1312,9 @@ export class NodeConnector {
1310
1312
  emitWorkbenchEvent: (event) => this.workbenchEventService.publish(event),
1311
1313
  readImage: async (runId, imageIndex) => this.runAttachmentService.read(runId, imageIndex) ??
1312
1314
  (await this.runAttachmentService.readNative(runId, imageIndex)),
1313
- processExecutorFactory: (input) => new BubblewrapSandboxExecutor(input),
1315
+ processExecutorFactory: (input) => new BubblewrapSandboxExecutor({
1316
+ profile: hostedConversationSandboxProfile(input)
1317
+ }),
1314
1318
  assertCanCreate: () => {
1315
1319
  if (maintenanceService === undefined)
1316
1320
  throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
@@ -55,25 +55,36 @@ export class MarAgentHostedConversationRuntime {
55
55
  async history(input) {
56
56
  const catalog = await this.#openCatalog({ homeDirectory: input.homeDirectory });
57
57
  try {
58
- let cursor;
58
+ const limit = Math.min(Math.max(input.limit ?? 10, 1), 100);
59
+ let cursor = input.cursor === undefined
60
+ ? undefined
61
+ : decodeHostedHistoryCursor(input.cursor, input.sessionId);
59
62
  let entries = [];
63
+ let turns = projectMarAgentHistory(historySession(input), entries);
60
64
  do {
61
65
  const page = await catalog.history(input.sessionId, {
62
- limit: 1000,
66
+ limit: Math.min(1000, Math.max(100, limit * 20)),
63
67
  ...(cursor === undefined ? {} : { cursor })
64
68
  });
65
69
  entries = [...page.entries, ...entries];
66
70
  cursor = page.nextCursor;
67
- } while (cursor !== undefined);
68
- return projectMarAgentHistory({
69
- id: input.sessionId,
70
- model: input.modelId ?? null,
71
- effort: null,
72
- access: 'full-access'
73
- }, entries, {
71
+ turns = projectMarAgentHistory(historySession(input), entries);
72
+ } while (cursor !== undefined && turns.length <= limit);
73
+ const selected = turns.slice(-limit);
74
+ const earliestTurnId = selected[0]?.id;
75
+ const earliestSequence = earliestTurnId === undefined
76
+ ? undefined
77
+ : entries.find((entry) => historyEntryTurnId(entry) === earliestTurnId)?.sequence;
78
+ const projected = projectMarAgentHistory(historySession(input), entries, {
74
79
  registerInputImages: (entry) => this.#registerInputImages(input, entry),
75
80
  registerGeneratedImages: (sessionId, images) => this.#registerGeneratedImages(input, sessionId, images)
76
- });
81
+ }).slice(-limit);
82
+ return {
83
+ turns: projected,
84
+ nextCursor: turns.length > selected.length && earliestSequence !== undefined
85
+ ? encodeHostedHistoryCursor(input.sessionId, earliestSequence)
86
+ : null
87
+ };
77
88
  }
78
89
  finally {
79
90
  await catalog.close();
@@ -342,6 +353,36 @@ export class MarAgentHostedConversationRuntime {
342
353
  }
343
354
  }
344
355
  }
356
+ function historySession(input) {
357
+ return {
358
+ id: input.sessionId,
359
+ model: input.modelId ?? null,
360
+ effort: null,
361
+ access: 'full-access'
362
+ };
363
+ }
364
+ function historyEntryTurnId(entry) {
365
+ return entry.kind === 'user' ? entry.turnId : entry.event.turnId;
366
+ }
367
+ function encodeHostedHistoryCursor(sessionId, beforeSequence) {
368
+ return Buffer.from(JSON.stringify({ version: 1, sessionId, beforeSequence }), 'utf8').toString('base64url');
369
+ }
370
+ function decodeHostedHistoryCursor(cursor, sessionId) {
371
+ try {
372
+ const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
373
+ if (parsed.version !== 1 ||
374
+ parsed.sessionId !== sessionId ||
375
+ !Number.isSafeInteger(parsed.beforeSequence) ||
376
+ parsed.beforeSequence <= 0)
377
+ throw new Error('HOSTED_CONVERSATION_HISTORY_CURSOR_INVALID');
378
+ return String(parsed.beforeSequence);
379
+ }
380
+ catch (error) {
381
+ if (error instanceof Error && error.message === 'HOSTED_CONVERSATION_HISTORY_CURSOR_INVALID')
382
+ throw error;
383
+ throw new Error('HOSTED_CONVERSATION_HISTORY_CURSOR_INVALID', { cause: error });
384
+ }
385
+ }
345
386
  function projectHostedEvent(normalizer, event, userMessage, registerGeneratedImages) {
346
387
  const createdAt = Date.parse(event.timestamp);
347
388
  const base = {
@@ -1,16 +1,41 @@
1
+ import { LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION, probeBubblewrapSandbox } from '@myagentroam/agent/sandbox/linux';
2
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
1
5
  import { safeErrorCode } from '../util/safe-error.js';
2
- import { probeBubblewrapSandbox } from './bubblewrap-sandbox-executor.js';
6
+ import { hostedConversationSandboxProfile } from './hosted-conversation-sandbox-profile.js';
3
7
  const CONVERSATION_HOSTING_API_VERSION = 1;
4
- const SANDBOX_POLICY_VERSION = 1;
5
8
  /** Runs the same bounded Bubblewrap policy used by hosted local processes. */
6
9
  export async function probeBubblewrap() {
10
+ const root = await mkdtemp(join(tmpdir(), 'mar-node-bwrap-probe-')).catch(() => undefined);
11
+ if (root === undefined)
12
+ return { available: false, reasonCode: 'SANDBOX_UNAVAILABLE' };
13
+ const workspaceDirectory = join(root, 'workspace');
14
+ const inboxDirectory = join(root, 'inbox');
15
+ const outputDirectory = join(root, 'output');
16
+ const temporaryDirectory = join(root, 'tmp');
7
17
  try {
8
- const result = await probeBubblewrapSandbox();
18
+ await Promise.all([workspaceDirectory, inboxDirectory, outputDirectory, temporaryDirectory].map((path) => mkdir(path, { mode: 0o700 })));
19
+ await Promise.all(['inbox', 'output'].map((name) => mkdir(join(workspaceDirectory, name), { recursive: true, mode: 0o700 })));
20
+ const result = await probeBubblewrapSandbox({
21
+ profile: hostedConversationSandboxProfile({
22
+ workspaceDirectory,
23
+ sourceWorkspacePath: '/home/mar-sandbox-probe/workspace',
24
+ inboxDirectory,
25
+ outputDirectory,
26
+ temporaryDirectory,
27
+ networkMode: 'NONE'
28
+ }),
29
+ cwd: workspaceDirectory
30
+ });
9
31
  return { available: true, version: result.version };
10
32
  }
11
33
  catch {
12
34
  return { available: false, reasonCode: 'SANDBOX_UNAVAILABLE' };
13
35
  }
36
+ finally {
37
+ await rm(root, { recursive: true, force: true }).catch(() => undefined);
38
+ }
14
39
  }
15
40
  export async function conversationHostingCapability(capabilities, config, storageReady, storageFailureCode, probe = probeBubblewrap, capacity = 'AVAILABLE') {
16
41
  if (capabilities.platform !== 'linux' || config?.conversationHosting?.enabled !== true) {
@@ -34,7 +59,7 @@ export async function conversationHostingCapability(capabilities, config, storag
34
59
  state: 'AVAILABLE',
35
60
  apiVersion: CONVERSATION_HOSTING_API_VERSION,
36
61
  sandboxBackend: 'BUBBLEWRAP',
37
- sandboxPolicyVersion: SANDBOX_POLICY_VERSION,
62
+ sandboxPolicyVersion: LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION,
38
63
  acceptsNewConversations: capacity !== 'FULL',
39
64
  supportsExistingConversations: true,
40
65
  capacity
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { constants } from 'node:fs';
3
3
  import { lstat, mkdir, open, readFile, rename, rm, utimes } from 'node:fs/promises';
4
4
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
5
- import { conversationTurnSchema, conversationTemplateDraftSchema, conversationTemplateSummarySchema, hostedConversationConfigUpdateInputSchema, hostedConversationCreateInputSchema, hostedConversationDeleteResultSchema, hostedConversationFileDeleteInputSchema, hostedConversationFileDeleteResultSchema, hostedConversationFileReadInputSchema, hostedConversationFileReadResultSchema, hostedConversationFilesSchema, hostedConversationIdInputSchema, hostedConversationImageReadInputSchema, hostedConversationListInputSchema, hostedConversationListPageSchema, hostedConversationMessageInputSchema, hostedConversationMetadataUpdateInputSchema, hostedConversationRollbackInputSchema, hostedConversationSnapshotSchema, hostedConversationSummarySchema, hostedConversationUnwatchInputSchema, hostedConversationUploadChunkInputSchema, hostedConversationUploadControlInputSchema, hostedConversationUploadCreateInputSchema, hostedConversationUploadPreflightInputSchema, hostedConversationUploadPreflightResultSchema, hostedConversationUploadSchema, hostedConversationWatchInputSchema, marAgentRuntimeModelConfigurationSchema, mcpInstallationConfigurationSchema, mcpRuntimeDescriptorSchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
5
+ import { conversationTurnSchema, conversationTemplateDraftSchema, conversationTemplateSummarySchema, hostedConversationConfigUpdateInputSchema, hostedConversationCreateInputSchema, hostedConversationDeleteResultSchema, hostedConversationFileDeleteInputSchema, hostedConversationFileDeleteResultSchema, hostedConversationFileReadInputSchema, hostedConversationFileReadResultSchema, hostedConversationFilesSchema, hostedConversationHistoryInputSchema, hostedConversationHistoryPageSchema, hostedConversationIdInputSchema, hostedConversationImageReadInputSchema, hostedConversationListInputSchema, hostedConversationListPageSchema, hostedConversationMessageInputSchema, hostedConversationMetadataUpdateInputSchema, hostedConversationRollbackInputSchema, hostedConversationSnapshotSchema, hostedConversationSummarySchema, hostedConversationUnwatchInputSchema, hostedConversationUploadChunkInputSchema, hostedConversationUploadControlInputSchema, hostedConversationUploadCreateInputSchema, hostedConversationUploadPreflightInputSchema, hostedConversationUploadPreflightResultSchema, hostedConversationUploadSchema, hostedConversationWatchInputSchema, marAgentRuntimeModelConfigurationSchema, mcpInstallationConfigurationSchema, mcpRuntimeDescriptorSchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
6
6
  import { hostedConversationGeneratedImagesDisplayDirectory, hostedConversationImageRunId, marAgentGeneratedImagesDirectory } from '../runner/mar-agent/generated-images.js';
7
7
  import { sessionTitleFromFirstMessage } from '../util/node-operation-parsers.js';
8
8
  import { safeErrorCode } from '../util/safe-error.js';
@@ -61,6 +61,7 @@ export class HostedConversationNodeService {
61
61
  'conversation.hosted.list': (raw) => this.list(raw),
62
62
  'conversation.hosted.create': (raw) => this.create(raw),
63
63
  'conversation.hosted.get': (raw) => this.get(raw),
64
+ 'conversation.hosted.history': (raw) => this.history(raw),
64
65
  'conversation.hosted.watch': (raw) => this.watch(raw),
65
66
  'conversation.hosted.unwatch': (raw) => this.unwatch(raw),
66
67
  'conversation.hosted.message': (raw) => this.message(raw),
@@ -216,9 +217,24 @@ export class HostedConversationNodeService {
216
217
  const input = hostedConversationIdInputSchema.parse(payload.data);
217
218
  return this.snapshot(payload.userId, input.conversationId, runtimeModels(payload.models), payload.templateDeleted);
218
219
  }
219
- async snapshot(userId, conversationId, availableModels, templateDeleted = false, includeActiveHistoryMutation = false, turnsOverride) {
220
+ async history(raw) {
221
+ const payload = hostedRequest(raw);
222
+ const input = hostedConversationHistoryInputSchema.parse(payload.data);
223
+ const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
224
+ const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
225
+ const page = await this.options.runtime.history({
226
+ conversationId: conversation.id,
227
+ sessionId: conversation.sessionId,
228
+ homeDirectory: paths.homeDirectory,
229
+ modelId: conversation.modelId,
230
+ limit: input.limit,
231
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor })
232
+ });
233
+ return hostedConversationHistoryPageSchema.parse(page);
234
+ }
235
+ async snapshot(userId, conversationId, availableModels, templateDeleted = false, includeActiveHistoryMutation = false, historyOverride) {
220
236
  if (includeActiveHistoryMutation)
221
- return this.readSnapshot(userId, conversationId, availableModels, templateDeleted, turnsOverride);
237
+ return this.readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride);
222
238
  while (true) {
223
239
  const version = this.historyVersions.get(conversationId) ?? 0;
224
240
  const mutation = this.historyMutations.get(conversationId);
@@ -232,7 +248,7 @@ export class HostedConversationNodeService {
232
248
  return snapshot;
233
249
  }
234
250
  }
235
- async readSnapshot(userId, conversationId, availableModels, templateDeleted, turnsOverride) {
251
+ async readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride) {
236
252
  let conversation = this.options.store.requireConversation(userId, conversationId);
237
253
  if (templateDeleted)
238
254
  conversation = await this.options.store.retainConversationTemplate(userId, conversationId, this.now());
@@ -243,19 +259,21 @@ export class HostedConversationNodeService {
243
259
  const version = await this.loadTemplateVersion(displayVersionId, conversation.templateId);
244
260
  const models = networkModels(allowedModels(version, availableModels), version.draft.networkMode);
245
261
  const paths = this.options.store.pathsForConversation(userId, conversation.id);
246
- const turns = turnsOverride ??
262
+ const history = historyOverride ??
247
263
  (await this.options.runtime.history({
248
264
  conversationId: conversation.id,
249
265
  sessionId: conversation.sessionId,
250
266
  homeDirectory: paths.homeDirectory,
251
- modelId: conversation.modelId
267
+ modelId: conversation.modelId,
268
+ limit: 10
252
269
  }));
253
270
  return hostedConversationSnapshotSchema.parse({
254
271
  nodeAppVersion: this.options.nodeAppVersion?.() ?? 'unknown',
255
272
  conversation: this.summary(conversation),
256
273
  template: templateSummary(conversation, version.draft),
257
274
  run: this.activeRuns.get(conversation.id)?.run ?? null,
258
- turns,
275
+ turns: history.turns,
276
+ historyNextCursor: history.nextCursor,
259
277
  files: this.options.store.listFiles(userId, conversation.id),
260
278
  models: models.map(modelSummary),
261
279
  error: null,
@@ -650,12 +668,7 @@ export class HostedConversationNodeService {
650
668
  }
651
669
  async rollbackConversation(payload, conversation, input) {
652
670
  const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
653
- const turns = (await this.options.runtime.history({
654
- conversationId: conversation.id,
655
- sessionId: conversation.sessionId,
656
- homeDirectory: paths.homeDirectory,
657
- modelId: conversation.modelId
658
- })).map((turn) => conversationTurnSchema.parse(turn));
671
+ const turns = await this.readAllHistory(conversation, paths);
659
672
  const targetIndex = turns.findIndex((turn) => turn.id === input.turnId);
660
673
  const target = turns[targetIndex];
661
674
  const userMessage = target?.items.find((item) => item.kind === 'user_message' && item.status === 'COMPLETED');
@@ -681,10 +694,27 @@ export class HostedConversationNodeService {
681
694
  }
682
695
  this.historyVersions.set(conversation.id, (this.historyVersions.get(conversation.id) ?? 0) + 1);
683
696
  await this.options.store.advanceLastActivity(conversation.id, Math.max(this.now(), rolledBack.lastActivityAt));
684
- const snapshot = await this.snapshot(payload.userId, conversation.id, runtimeModels(payload.models), payload.templateDeleted, true, turns.slice(0, targetIndex));
697
+ const snapshot = await this.snapshot(payload.userId, conversation.id, runtimeModels(payload.models), payload.templateDeleted, true);
685
698
  this.publishHostedSnapshot(conversation.id, snapshot);
686
699
  return snapshot;
687
700
  }
701
+ async readAllHistory(conversation, paths) {
702
+ const turns = [];
703
+ let cursor;
704
+ do {
705
+ const page = await this.options.runtime.history({
706
+ conversationId: conversation.id,
707
+ sessionId: conversation.sessionId,
708
+ homeDirectory: paths.homeDirectory,
709
+ modelId: conversation.modelId,
710
+ limit: 100,
711
+ ...(cursor === undefined ? {} : { cursor })
712
+ });
713
+ turns.unshift(...page.turns.map((turn) => conversationTurnSchema.parse(turn)));
714
+ cursor = page.nextCursor ?? undefined;
715
+ } while (cursor !== undefined);
716
+ return turns;
717
+ }
688
718
  async finishRun(userId, conversationId, clientMessageId, run, completion) {
689
719
  const interruptionKey = runInterruptionKey(conversationId, run.id);
690
720
  const expectedInterruption = this.expectedInterruptions.has(interruptionKey);
@@ -701,7 +731,7 @@ export class HostedConversationNodeService {
701
731
  completedAt: completion.completedAt
702
732
  }
703
733
  });
704
- void this.refreshReadyWatchSnapshots(conversationId);
734
+ void this.refreshReadyWatchSnapshots(conversationId, run.id);
705
735
  try {
706
736
  const files = await this.options.store.reconcileFiles(userId, conversationId);
707
737
  this.publishHostedEvent(conversationId, {
@@ -1178,12 +1208,16 @@ export class HostedConversationNodeService {
1178
1208
  this.removeWatchLease(lease.ownerKey);
1179
1209
  }
1180
1210
  }
1181
- async refreshReadyWatchSnapshots(conversationId) {
1211
+ async refreshReadyWatchSnapshots(conversationId, completedRunId) {
1182
1212
  const leases = [...this.watchLeases.values()].filter((lease) => lease.conversationId === conversationId && lease.ready);
1183
1213
  await Promise.allSettled(leases.map(async (lease) => {
1184
1214
  const snapshot = await this.snapshot(lease.userId, conversationId, lease.models);
1185
1215
  if (this.watchLeases.get(lease.ownerKey) !== lease || !lease.ready)
1186
1216
  return;
1217
+ const activeRunId = this.activeRuns.get(conversationId)?.run.id;
1218
+ if ((activeRunId !== undefined && activeRunId !== completedRunId) ||
1219
+ (activeRunId === undefined && this.activity(conversationId).status !== 'IDLE'))
1220
+ return;
1187
1221
  this.options.emitWatchEvent?.({
1188
1222
  type: 'watch.hosted-conversation.snapshot',
1189
1223
  workbenchConnectionId: lease.workbenchConnectionId,
@@ -1496,7 +1530,6 @@ export class HostedConversationNodeService {
1496
1530
  }
1497
1531
  processExecutor(paths, version, temporaryDirectory = paths.temporaryRootDirectory, generatedImagesDirectory, generatedImagesLogicalPath) {
1498
1532
  return this.options.processExecutorFactory({
1499
- policyVersion: HOST_POLICY_VERSION,
1500
1533
  workspaceDirectory: version.templateDirectory,
1501
1534
  ...(version.sourceWorkspacePath === undefined
1502
1535
  ? {}
@@ -0,0 +1,57 @@
1
+ import { LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION } from '@myagentroam/agent/sandbox/linux';
2
+ import { MarAgentError } from '@myagentroam/agent';
3
+ import { posix } from 'node:path';
4
+ /** Compiles Node-owned hosted-conversation directories into the generic Agent sandbox profile. */
5
+ export function hostedConversationSandboxProfile(input) {
6
+ if ((input.generatedImagesDirectory === undefined) !==
7
+ (input.generatedImagesLogicalPath === undefined))
8
+ throw new MarAgentError('SANDBOX_POLICY_INVALID', 'Sandbox policy is invalid.');
9
+ const mounts = [
10
+ {
11
+ source: input.workspaceDirectory,
12
+ destination: '/workspace',
13
+ access: 'READ_ONLY'
14
+ },
15
+ {
16
+ source: input.inboxDirectory,
17
+ destination: '/workspace/inbox',
18
+ access: 'READ_WRITE'
19
+ },
20
+ {
21
+ source: input.outputDirectory,
22
+ destination: '/workspace/output',
23
+ access: 'READ_WRITE'
24
+ }
25
+ ];
26
+ if (input.generatedImagesDirectory !== undefined &&
27
+ input.generatedImagesLogicalPath !== undefined)
28
+ mounts.splice(1, 0, {
29
+ source: input.generatedImagesDirectory,
30
+ destination: input.generatedImagesLogicalPath,
31
+ access: 'READ_ONLY'
32
+ });
33
+ if (input.sourceWorkspacePath !== undefined)
34
+ mounts.splice(1, 0, {
35
+ source: input.workspaceDirectory,
36
+ destination: input.sourceWorkspacePath,
37
+ access: 'READ_ONLY'
38
+ }, {
39
+ source: input.inboxDirectory,
40
+ destination: posix.join(input.sourceWorkspacePath, 'inbox'),
41
+ access: 'READ_WRITE'
42
+ }, {
43
+ source: input.outputDirectory,
44
+ destination: posix.join(input.sourceWorkspacePath, 'output'),
45
+ access: 'READ_WRITE'
46
+ });
47
+ return {
48
+ policyVersion: LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION,
49
+ networkMode: input.networkMode,
50
+ mounts,
51
+ temporaryDirectory: {
52
+ kind: 'MOUNT',
53
+ source: input.temporaryDirectory
54
+ },
55
+ workingDirectory: { kind: 'REQUEST' }
56
+ };
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.77",
3
+ "version": "0.9.78",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -28,8 +28,8 @@
28
28
  "node-pty": "1.1.0",
29
29
  "ws": "^8.21.3",
30
30
  "zod": "4.4.3",
31
- "@myagentroam/agent": "0.9.77",
32
- "@myagentroam/protocol": "0.9.77"
31
+ "@myagentroam/agent": "0.9.78",
32
+ "@myagentroam/protocol": "0.9.78"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/ws": "^8.18.1"
@@ -1,543 +0,0 @@
1
- import { execFile, spawn } from 'node:child_process';
2
- import { closeSync, existsSync, lstatSync, openSync, readlinkSync, rmSync } from 'node:fs';
3
- import { lstat, mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
6
- import { promisify } from 'node:util';
7
- import { MarAgentError } from '@myagentroam/agent';
8
- const execFileAsync = promisify(execFile);
9
- const DEFAULT_BUBBLEWRAP_PATH = '/usr/bin/bwrap';
10
- const SANDBOX_POLICY_VERSION = 1;
11
- const SANDBOX_PATH = '/runtime/node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
12
- const CURSOR_SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u;
13
- const SANDBOX_CPU_SECONDS = 300;
14
- const SANDBOX_PROCESS_LIMIT = 64;
15
- const SANDBOX_OPEN_FILE_LIMIT = 256;
16
- const SANDBOX_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
17
- const SAFE_ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/u;
18
- const GENERATED_IMAGES_SANDBOX_ROOT = '/home/mar/.mar/agent/sessions';
19
- // Fontconfig and LibreOffice resolve required host configuration through absolute /etc paths
20
- // and /usr symlinks. Keep this allowlist read-only and never broaden it to the full /etc tree.
21
- const READ_ONLY_SYSTEM_CONFIGURATION_DIRECTORIES = ['/etc/fonts', '/etc/libreoffice'];
22
- const FORBIDDEN_ENVIRONMENT_NAMES = new Set([
23
- 'PATH',
24
- 'HOME',
25
- 'TMPDIR',
26
- 'LD_PRELOAD',
27
- 'LD_LIBRARY_PATH',
28
- 'NODE_OPTIONS',
29
- 'PYTHONPATH',
30
- 'PYTHONHOME',
31
- 'BASH_ENV',
32
- 'ENV',
33
- 'SSH_AUTH_SOCK'
34
- ]);
35
- const RESERVED_SANDBOX_ROOTS = [
36
- '/workspace',
37
- '/home/mar',
38
- '/runtime',
39
- '/usr',
40
- '/bin',
41
- '/sbin',
42
- '/lib',
43
- '/lib64',
44
- ...READ_ONLY_SYSTEM_CONFIGURATION_DIRECTORIES,
45
- '/proc',
46
- '/dev',
47
- '/run'
48
- ];
49
- /** Compiles one hosted-conversation process request into the fixed Linux Bubblewrap policy. */
50
- export class BubblewrapSandboxExecutor {
51
- options;
52
- #bubblewrapPath;
53
- constructor(options) {
54
- this.options = options;
55
- this.#bubblewrapPath = options.bubblewrapPath ?? DEFAULT_BUBBLEWRAP_PATH;
56
- }
57
- async spawn(request) {
58
- request.signal?.throwIfAborted();
59
- if (process.platform !== 'linux')
60
- throw unavailable();
61
- if (this.options.sourceWorkspacePath !== undefined)
62
- sandboxCompatibilityPath(this.options.sourceWorkspacePath);
63
- if (this.options.policyVersion !== SANDBOX_POLICY_VERSION ||
64
- !['NONE', 'UNRESTRICTED'].includes(this.options.networkMode))
65
- throw policyInvalid();
66
- if ((this.options.generatedImagesDirectory === undefined) !==
67
- (this.options.generatedImagesLogicalPath === undefined))
68
- throw policyInvalid();
69
- if (this.options.generatedImagesLogicalPath !== undefined)
70
- generatedImagesSandboxPath(this.options.generatedImagesLogicalPath);
71
- await validateBackend(this.#bubblewrapPath);
72
- const mounts = await this.#mounts();
73
- const cwd = await sandboxPath(request.cwd, mounts);
74
- const command = sandboxCommand(request, mounts);
75
- const launchDirectory = await mkdtemp(join(tmpdir(), 'mar-node-bwrap-launch-'));
76
- const argumentPath = join(launchDirectory, 'arguments');
77
- const seccompPath = join(launchDirectory, 'seccomp.bpf');
78
- try {
79
- const arguments_ = this.#arguments(mounts, cwd, request);
80
- await Promise.all([
81
- writeFile(argumentPath, `${arguments_.join('\0')}\0`, { mode: 0o600, flag: 'wx' }),
82
- writeFile(seccompPath, seccompFilter(process.arch), { mode: 0o600, flag: 'wx' })
83
- ]);
84
- }
85
- catch (error) {
86
- await rm(launchDirectory, { recursive: true, force: true }).catch(() => undefined);
87
- throw error instanceof MarAgentError ? error : policyInvalid(error);
88
- }
89
- let argumentFd;
90
- let seccompFd;
91
- try {
92
- argumentFd = openSync(argumentPath, 'r');
93
- seccompFd = openSync(seccompPath, 'r');
94
- rmSync(launchDirectory, { recursive: true, force: true });
95
- request.signal?.throwIfAborted();
96
- try {
97
- const child = spawn(this.#bubblewrapPath, ['--args', '3', '--', ...command], {
98
- cwd: '/',
99
- shell: false,
100
- stdio: ['pipe', 'pipe', 'pipe', argumentFd, seccompFd],
101
- windowsHide: true,
102
- detached: request.detached,
103
- env: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' }
104
- });
105
- return child;
106
- }
107
- catch (error) {
108
- throw unavailable(error);
109
- }
110
- }
111
- finally {
112
- if (argumentFd !== undefined)
113
- closeSync(argumentFd);
114
- if (seccompFd !== undefined)
115
- closeSync(seccompFd);
116
- rmSync(launchDirectory, { recursive: true, force: true });
117
- }
118
- }
119
- async #mounts() {
120
- const definitions = [
121
- [this.options.workspaceDirectory, '/workspace', false],
122
- [this.options.inboxDirectory, '/workspace/inbox', true],
123
- [this.options.outputDirectory, '/workspace/output', true],
124
- [this.options.temporaryDirectory, '/tmp', true]
125
- ];
126
- if (this.options.generatedImagesDirectory !== undefined &&
127
- this.options.generatedImagesLogicalPath !== undefined)
128
- definitions.splice(1, 0, [
129
- this.options.generatedImagesDirectory,
130
- this.options.generatedImagesLogicalPath,
131
- false
132
- ]);
133
- if (this.options.sourceWorkspacePath !== undefined)
134
- definitions.splice(1, 0, [this.options.workspaceDirectory, this.options.sourceWorkspacePath, false], [this.options.inboxDirectory, posix.join(this.options.sourceWorkspacePath, 'inbox'), true], [this.options.outputDirectory, posix.join(this.options.sourceWorkspacePath, 'output'), true]);
135
- const mounts = [];
136
- for (const [source, destination, writable] of definitions) {
137
- mounts.push({ source: await secureDirectory(source), destination, writable });
138
- }
139
- for (const [index, mount] of mounts.entries()) {
140
- for (const other of mounts.slice(index + 1)) {
141
- if (mount.source === other.source) {
142
- if (mount.writable !== other.writable)
143
- throw policyInvalid();
144
- continue;
145
- }
146
- if (within(mount.source, other.source) || within(other.source, mount.source))
147
- throw policyInvalid();
148
- }
149
- }
150
- const ordered = [...mounts].sort(compareMountDestinations);
151
- await prepareNestedMountDestinations(ordered);
152
- return ordered;
153
- }
154
- #arguments(mounts, cwd, request) {
155
- const args = [
156
- '--unshare-user',
157
- '--unshare-pid',
158
- ...(this.options.networkMode === 'UNRESTRICTED' ? [] : ['--unshare-net']),
159
- '--unshare-uts',
160
- '--unshare-ipc',
161
- '--unshare-cgroup-try',
162
- '--disable-userns',
163
- '--die-with-parent',
164
- '--new-session',
165
- '--cap-drop',
166
- 'ALL',
167
- '--hostname',
168
- 'mar-hosted',
169
- '--clearenv',
170
- '--setenv',
171
- 'PATH',
172
- SANDBOX_PATH,
173
- '--setenv',
174
- 'HOME',
175
- '/home/mar',
176
- '--setenv',
177
- 'LANG',
178
- 'C.UTF-8',
179
- '--setenv',
180
- 'LC_ALL',
181
- 'C.UTF-8',
182
- '--setenv',
183
- 'TMPDIR',
184
- '/tmp',
185
- '--tmpfs',
186
- '/',
187
- ...runtimeMountArguments(),
188
- ...sandboxDirectoryArguments(mounts),
189
- '--proc',
190
- '/proc',
191
- '--dev',
192
- '/dev',
193
- '--tmpfs',
194
- '/run'
195
- ];
196
- for (const mount of mounts)
197
- args.push(mount.writable ? '--bind' : '--ro-bind', mount.source, mount.destination);
198
- if (request.owner !== undefined) {
199
- if (!CURSOR_SAFE_ID.test(request.owner.sessionId) ||
200
- !CURSOR_SAFE_ID.test(request.owner.executionId))
201
- throw policyInvalid();
202
- args.push('--setenv', 'MAR_AGENT_SESSION_ID', request.owner.sessionId, '--setenv', 'MAR_AGENT_EXECUTION_ID', request.owner.executionId);
203
- }
204
- for (const [name, value] of explicitEnvironment(request))
205
- args.push('--setenv', name, value);
206
- args.push('--seccomp', '4', '--chdir', cwd);
207
- return args;
208
- }
209
- }
210
- export async function probeBubblewrapSandbox(bubblewrapPath = DEFAULT_BUBBLEWRAP_PATH) {
211
- if (process.platform !== 'linux')
212
- throw unavailable();
213
- const versionOutput = await execFileAsync(bubblewrapPath, ['--version'], {
214
- timeout: 5_000,
215
- windowsHide: true
216
- }).catch((error) => {
217
- throw unavailable(error);
218
- });
219
- const version = /^bubblewrap\s+([0-9]+(?:\.[0-9]+){1,3})\s*$/u.exec(versionOutput.stdout)?.[1];
220
- if (version === undefined)
221
- throw unavailable();
222
- const root = await mkdtemp(join(tmpdir(), 'mar-node-bwrap-probe-'));
223
- const directories = Object.fromEntries(['workspace', 'inbox', 'output', 'tmp'].map((name) => [name, join(root, name)]));
224
- try {
225
- await Promise.all(Object.values(directories).map((path) => mkdir(path, { mode: 0o700 })));
226
- await Promise.all(['inbox', 'output'].map((name) => mkdir(join(directories.workspace, name), { recursive: true, mode: 0o700 })));
227
- const executor = new BubblewrapSandboxExecutor({
228
- bubblewrapPath,
229
- policyVersion: SANDBOX_POLICY_VERSION,
230
- workspaceDirectory: directories.workspace,
231
- sourceWorkspacePath: '/home/mar-sandbox-probe/workspace',
232
- inboxDirectory: directories.inbox,
233
- outputDirectory: directories.output,
234
- temporaryDirectory: directories.tmp,
235
- networkMode: 'NONE'
236
- });
237
- const child = await executor.spawn({
238
- command: '/bin/true',
239
- cwd: directories.workspace,
240
- shell: false,
241
- environment: {},
242
- detached: true,
243
- owner: { sessionId: 'sandbox-probe', executionId: 'sandbox-probe' }
244
- });
245
- const result = await waitForProbe(child);
246
- if (result.code !== 0)
247
- throw unavailable(new Error(result.stderr));
248
- return { version };
249
- }
250
- finally {
251
- await rm(root, { recursive: true, force: true }).catch(() => undefined);
252
- }
253
- }
254
- async function waitForProbe(child) {
255
- const chunks = [];
256
- let bytes = 0;
257
- child.stderr.on('data', (chunk) => {
258
- if (bytes >= 8_192)
259
- return;
260
- const remaining = 8_192 - bytes;
261
- chunks.push(chunk.subarray(0, remaining));
262
- bytes += Math.min(chunk.length, remaining);
263
- });
264
- child.stdin.end();
265
- return await new Promise((resolve, reject) => {
266
- const timer = setTimeout(() => {
267
- child.kill('SIGKILL');
268
- reject(unavailable(new Error('Sandbox probe timed out.')));
269
- }, 5_000);
270
- timer.unref();
271
- child.once('error', (error) => {
272
- clearTimeout(timer);
273
- reject(unavailable(error));
274
- });
275
- child.once('close', (code) => {
276
- clearTimeout(timer);
277
- resolve({ code, stderr: Buffer.concat(chunks).toString('utf8') });
278
- });
279
- });
280
- }
281
- async function validateBackend(path) {
282
- try {
283
- const metadata = await lstat(path);
284
- if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o111) === 0)
285
- throw unavailable();
286
- }
287
- catch (error) {
288
- if (error instanceof MarAgentError)
289
- throw error;
290
- throw unavailable(error);
291
- }
292
- }
293
- async function secureDirectory(path) {
294
- if (!isAbsolute(path))
295
- throw policyInvalid();
296
- try {
297
- const metadata = await lstat(path);
298
- if (!metadata.isDirectory() || metadata.isSymbolicLink())
299
- throw policyInvalid();
300
- const canonical = await realpath(path);
301
- if (canonical !== resolve(path))
302
- throw policyInvalid();
303
- return canonical;
304
- }
305
- catch (error) {
306
- if (error instanceof MarAgentError)
307
- throw error;
308
- throw policyInvalid(error);
309
- }
310
- }
311
- async function sandboxPath(path, mounts) {
312
- const canonical = await secureDirectory(path);
313
- const mount = [...mounts]
314
- .sort((left, right) => right.source.length - left.source.length)
315
- .find((candidate) => canonical === candidate.source || within(candidate.source, canonical));
316
- if (mount === undefined)
317
- throw policyInvalid();
318
- const suffix = relative(mount.source, canonical).split(sep).filter(Boolean);
319
- return suffix.length === 0 ? mount.destination : posix.join(mount.destination, ...suffix);
320
- }
321
- function sandboxCommand(request, mounts) {
322
- const command = request.shell === undefined || request.shell === false
323
- ? [sandboxExecutable(request.command, mounts), ...(request.args ?? [])]
324
- : [
325
- sandboxExecutable(typeof request.shell === 'string' ? request.shell : '/bin/sh', mounts),
326
- '-c',
327
- request.command,
328
- ...(request.args ?? [])
329
- ];
330
- return [
331
- '/usr/bin/prlimit',
332
- `--cpu=${SANDBOX_CPU_SECONDS}:${SANDBOX_CPU_SECONDS}`,
333
- `--nproc=${SANDBOX_PROCESS_LIMIT}:${SANDBOX_PROCESS_LIMIT}`,
334
- `--nofile=${SANDBOX_OPEN_FILE_LIMIT}:${SANDBOX_OPEN_FILE_LIMIT}`,
335
- `--fsize=${SANDBOX_FILE_SIZE_BYTES}:${SANDBOX_FILE_SIZE_BYTES}`,
336
- '--',
337
- ...command
338
- ];
339
- }
340
- function sandboxExecutable(command, mounts) {
341
- if (!isAbsolute(command))
342
- return command;
343
- const canonical = resolve(command);
344
- const nodeRuntimeDirectory = resolve(process.execPath, '..', '..');
345
- if (nodeRuntimeDirectory !== '/usr' &&
346
- (canonical === nodeRuntimeDirectory || within(nodeRuntimeDirectory, canonical))) {
347
- const suffix = relative(nodeRuntimeDirectory, canonical).split(sep).filter(Boolean);
348
- return suffix.length === 0 ? '/runtime/node' : posix.join('/runtime/node', ...suffix);
349
- }
350
- const mount = [...mounts]
351
- .sort((left, right) => right.source.length - left.source.length)
352
- .find((candidate) => canonical === candidate.source || within(candidate.source, canonical));
353
- if (mount !== undefined) {
354
- const suffix = relative(mount.source, canonical).split(sep).filter(Boolean);
355
- return suffix.length === 0 ? mount.destination : posix.join(mount.destination, ...suffix);
356
- }
357
- if (['/usr', '/bin', '/sbin', '/lib', '/lib64'].some((root) => canonical === root || within(root, canonical)))
358
- return canonical;
359
- throw policyInvalid();
360
- }
361
- function explicitEnvironment(request) {
362
- const entries = [];
363
- const seen = new Set();
364
- for (const name of request.environmentKeys ?? []) {
365
- if (seen.has(name))
366
- continue;
367
- seen.add(name);
368
- if (!SAFE_ENVIRONMENT_NAME.test(name) || FORBIDDEN_ENVIRONMENT_NAMES.has(name))
369
- throw policyInvalid();
370
- const value = request.environment[name];
371
- if (typeof value !== 'string' || value.includes('\0') || Buffer.byteLength(value) > 32_768)
372
- throw policyInvalid();
373
- entries.push([name, value]);
374
- }
375
- return entries;
376
- }
377
- function sandboxCompatibilityPath(path) {
378
- if (!isAbsolute(path) ||
379
- path.includes('\0') ||
380
- path === '/' ||
381
- path === '/tmp' ||
382
- resolve(path) !== path ||
383
- RESERVED_SANDBOX_ROOTS.some((root) => path === root || within(root, path) || within(path, root)))
384
- throw policyInvalid();
385
- return path;
386
- }
387
- function generatedImagesSandboxPath(path) {
388
- const segments = relative(GENERATED_IMAGES_SANDBOX_ROOT, path).split(sep).filter(Boolean);
389
- if (!isAbsolute(path) ||
390
- path.includes('\0') ||
391
- resolve(path) !== path ||
392
- !within(GENERATED_IMAGES_SANDBOX_ROOT, path) ||
393
- segments.length !== 2 ||
394
- !safePathSegment(segments[0]) ||
395
- segments[1] !== 'generated_images')
396
- throw policyInvalid();
397
- return path;
398
- }
399
- function safePathSegment(value) {
400
- return (value.length > 0 &&
401
- value.length <= 255 &&
402
- value !== '.' &&
403
- value !== '..' &&
404
- !value.includes('/') &&
405
- !value.includes('\\'));
406
- }
407
- function compareMountDestinations(left, right) {
408
- const depth = left.destination.split('/').filter(Boolean).length -
409
- right.destination.split('/').filter(Boolean).length;
410
- return depth === 0 ? left.destination.localeCompare(right.destination, 'en-US') : depth;
411
- }
412
- async function prepareNestedMountDestinations(mounts) {
413
- for (const [index, mount] of mounts.entries()) {
414
- const parent = mounts
415
- .slice(0, index)
416
- .filter((candidate) => within(candidate.destination, mount.destination))
417
- .sort((left, right) => right.destination.length - left.destination.length)[0];
418
- if (parent === undefined)
419
- continue;
420
- const suffix = relative(parent.destination, mount.destination).split(sep).filter(Boolean);
421
- const target = join(parent.source, ...suffix);
422
- if (parent.writable) {
423
- await mkdir(target, { recursive: true, mode: 0o700 });
424
- continue;
425
- }
426
- try {
427
- const metadata = await lstat(target);
428
- if (!metadata.isDirectory() ||
429
- metadata.isSymbolicLink() ||
430
- (await realpath(target)) !== resolve(target))
431
- throw policyInvalid();
432
- }
433
- catch (error) {
434
- if (error instanceof MarAgentError)
435
- throw error;
436
- throw policyInvalid(error);
437
- }
438
- }
439
- }
440
- function sandboxDirectoryArguments(mounts) {
441
- const directories = new Set(['/home', '/home/mar']);
442
- for (const mount of mounts) {
443
- const segments = mount.destination.split('/').filter(Boolean);
444
- let current = '';
445
- for (const segment of segments) {
446
- current = `${current}/${segment}`;
447
- directories.add(current);
448
- }
449
- }
450
- return [...directories]
451
- .sort((left, right) => {
452
- const depth = left.split('/').filter(Boolean).length - right.split('/').filter(Boolean).length;
453
- return depth === 0 ? left.localeCompare(right, 'en-US') : depth;
454
- })
455
- .flatMap((directory) => ['--dir', directory]);
456
- }
457
- function runtimeMountArguments() {
458
- const args = [];
459
- const nodeRuntimeDirectory = resolve(process.execPath, '..', '..');
460
- if (nodeRuntimeDirectory !== '/usr')
461
- args.push('--dir', '/runtime', '--dir', '/runtime/node', '--ro-bind', nodeRuntimeDirectory, '/runtime/node');
462
- for (const path of ['/usr', '/bin', '/sbin', '/lib', '/lib64']) {
463
- if (!existsSync(path))
464
- continue;
465
- const metadata = lstatSync(path);
466
- if (metadata.isSymbolicLink())
467
- args.push('--symlink', readlinkSync(path), path);
468
- else if (metadata.isDirectory())
469
- args.push('--ro-bind', path, path);
470
- }
471
- for (const path of READ_ONLY_SYSTEM_CONFIGURATION_DIRECTORIES) {
472
- if (!existsSync(path) || !lstatSync(path).isDirectory())
473
- continue;
474
- args.push('--ro-bind', path, path);
475
- }
476
- for (const path of [
477
- '/etc/host.conf',
478
- '/etc/hosts',
479
- '/etc/ld.so.cache',
480
- '/etc/resolv.conf',
481
- '/etc/passwd',
482
- '/etc/group',
483
- '/etc/nsswitch.conf',
484
- '/etc/localtime',
485
- '/etc/ssl/certs'
486
- ]) {
487
- if (existsSync(path))
488
- args.push('--ro-bind', path, path);
489
- }
490
- return args;
491
- }
492
- function seccompFilter(architecture) {
493
- const policy = syscallPolicy(architecture);
494
- const filters = [
495
- [0x20, 0, 0, 4],
496
- [0x15, 1, 0, policy.auditArchitecture],
497
- [0x06, 0, 0, 0x80000000],
498
- [0x20, 0, 0, 0]
499
- ];
500
- if (architecture === 'x64')
501
- filters.push([0x54, 0, 0, 0x40000000], [0x15, 1, 0, 0], [0x06, 0, 0, 0x80000000], [0x20, 0, 0, 0]);
502
- for (const syscall of policy.blockedSyscalls)
503
- filters.push([0x15, 0, 1, syscall], [0x06, 0, 0, 0x00050001]);
504
- filters.push([0x06, 0, 0, 0x7fff0000]);
505
- const buffer = Buffer.alloc(filters.length * 8);
506
- for (const [index, [code, jumpTrue, jumpFalse, value]] of filters.entries()) {
507
- const offset = index * 8;
508
- buffer.writeUInt16LE(code, offset);
509
- buffer.writeUInt8(jumpTrue, offset + 2);
510
- buffer.writeUInt8(jumpFalse, offset + 3);
511
- buffer.writeUInt32LE(value >>> 0, offset + 4);
512
- }
513
- return buffer;
514
- }
515
- function syscallPolicy(architecture) {
516
- if (architecture === 'x64')
517
- return {
518
- auditArchitecture: 0xc000003e,
519
- blockedSyscalls: [
520
- 101, 155, 165, 166, 175, 176, 246, 248, 249, 250, 272, 298, 304, 308, 313, 321, 323, 428,
521
- 429, 430, 431, 432, 442
522
- ]
523
- };
524
- if (architecture === 'arm64')
525
- return {
526
- auditArchitecture: 0xc00000b7,
527
- blockedSyscalls: [
528
- 39, 40, 41, 97, 104, 105, 106, 117, 217, 218, 219, 241, 265, 268, 273, 280, 282, 428, 429,
529
- 430, 431, 432, 442
530
- ]
531
- };
532
- throw policyInvalid();
533
- }
534
- function within(parent, candidate) {
535
- const relation = relative(parent, candidate);
536
- return relation.length > 0 && relation !== '..' && !relation.startsWith(`..${sep}`);
537
- }
538
- function unavailable(cause) {
539
- return new MarAgentError('SANDBOX_UNAVAILABLE', 'Bubblewrap sandbox is unavailable.', { cause });
540
- }
541
- function policyInvalid(cause) {
542
- return new MarAgentError('SANDBOX_POLICY_INVALID', 'Sandbox policy is invalid.', { cause });
543
- }