@myagentroam/agent 0.11.35 → 0.11.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.
@@ -1,18 +1,9 @@
1
- import { type ChildProcessWithoutNullStreams } from 'node:child_process';
2
- import type { LocalProcessExecutor, LocalProcessSpawnRequest } from '../../host/local-process-executor.js';
3
- import { type BubblewrapSandboxExecutorOptions } from './profile.js';
4
- /** Compiles one process request into the fixed Linux Bubblewrap policy. */
1
+ import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import type { LocalProcessExecutor, LocalProcessSpawnRequest } from '../../process/contracts.js';
3
+ import type { BubblewrapSandboxExecutorOptions } from './profile.js';
5
4
  export declare class BubblewrapSandboxExecutor implements LocalProcessExecutor {
6
5
  #private;
7
6
  private readonly options;
8
7
  constructor(options: BubblewrapSandboxExecutorOptions);
9
8
  spawn(request: LocalProcessSpawnRequest): Promise<ChildProcessWithoutNullStreams>;
10
9
  }
11
- export declare function probeBubblewrapSandbox(options?: {
12
- readonly bubblewrapPath?: string;
13
- readonly profile?: BubblewrapSandboxExecutorOptions['profile'];
14
- readonly cwd?: string;
15
- }): Promise<{
16
- readonly version: string;
17
- readonly policyVersion: number;
18
- }>;
@@ -1,648 +1,26 @@
1
- import { execFile, spawn } from 'node:child_process';
2
- import { closeSync, existsSync, lstatSync, openSync, readlinkSync, rmSync } from 'node:fs';
3
- import { lstat, 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
1
  import { AgentError } from '../../error.js';
8
- import { prepareLocalShellProcess } from '../../host/local-shell.js';
9
- import { LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION } from './profile.js';
10
- const execFileAsync = promisify(execFile);
11
- const DEFAULT_BUBBLEWRAP_PATH = '/usr/bin/bwrap';
12
- const SANDBOX_PATH = '/runtime/node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
13
- const CURSOR_SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u;
14
- const SANDBOX_CPU_SECONDS = 300;
15
- const SANDBOX_PROCESS_LIMIT = 64;
16
- const SANDBOX_OPEN_FILE_LIMIT = 256;
17
- const SANDBOX_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
18
- const SAFE_ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/u;
19
- const GENERATED_IMAGES_SANDBOX_ROOT = '/home/mar/.mar/agent/sessions';
20
- // Fontconfig and LibreOffice resolve required host configuration through absolute /etc paths
21
- // and /usr symlinks. Keep this allowlist read-only and never broaden it to the full /etc tree.
22
- const READ_ONLY_SYSTEM_CONFIGURATION_DIRECTORIES = ['/etc/fonts', '/etc/libreoffice'];
23
- const FORBIDDEN_ENVIRONMENT_NAMES = new Set([
24
- 'PATH',
25
- 'HOME',
26
- 'TMPDIR',
27
- 'LD_PRELOAD',
28
- 'LD_LIBRARY_PATH',
29
- 'NODE_OPTIONS',
30
- 'PYTHONPATH',
31
- 'PYTHONHOME',
32
- 'BASH_ENV',
33
- 'ENV',
34
- 'SSH_AUTH_SOCK'
35
- ]);
36
- const RESERVED_SANDBOX_ROOTS = [
37
- '/home/mar',
38
- '/runtime',
39
- '/usr',
40
- '/bin',
41
- '/sbin',
42
- '/lib',
43
- '/lib64',
44
- '/etc',
45
- '/proc',
46
- '/dev',
47
- '/run',
48
- '/tmp'
49
- ];
50
- /** Compiles one process request into the fixed Linux Bubblewrap policy. */
2
+ import { launchBubblewrap } from './bubblewrap-launch.js';
3
+ import { compileBubblewrapPolicy } from './bubblewrap-policy.js';
4
+ import { policyInvalid, unavailable } from './sandbox-error.js';
51
5
  export class BubblewrapSandboxExecutor {
52
6
  options;
53
7
  #bubblewrapPath;
54
8
  constructor(options) {
55
9
  this.options = options;
56
- this.#bubblewrapPath = options.bubblewrapPath ?? DEFAULT_BUBBLEWRAP_PATH;
10
+ this.#bubblewrapPath = options.bubblewrapPath ?? '/usr/bin/bwrap';
57
11
  }
58
12
  async spawn(request) {
59
- request.signal?.throwIfAborted();
60
- if (process.platform !== 'linux')
61
- throw unavailable();
62
- validateProfile(this.options.profile);
63
- await validateBackend(this.#bubblewrapPath);
64
- const mounts = await this.#mounts();
65
- const runtimeMounts = await trustedRuntimeMounts(request.runtimeDirectories ?? [], mounts);
66
- const cwd = await workingDirectory(request, mounts, this.options.profile.workingDirectory);
67
- // Apply the sandbox policy before creating private startup-override variables.
68
- const environment = Object.fromEntries(explicitEnvironment(request));
69
- const shellRequest = prepareLocalShellProcess({
70
- ...request,
71
- environment,
72
- environmentKeys: Object.keys(environment),
73
- environmentOverrides: Object.fromEntries(Object.entries(request.environmentOverrides ?? {})
74
- .filter(([name, value]) => SAFE_ENVIRONMENT_NAME.test(name) &&
75
- !FORBIDDEN_ENVIRONMENT_NAMES.has(name) &&
76
- (value === undefined || Object.hasOwn(environment, name)))
77
- .map(([name, value]) => [name, value === undefined ? undefined : environment[name]]))
78
- });
79
- const command = sandboxCommand(shellRequest, mounts);
80
- const launchDirectory = await mkdtemp(join(tmpdir(), 'mar-agent-bwrap-launch-'));
81
- const argumentPath = join(launchDirectory, 'arguments');
82
- const seccompPath = join(launchDirectory, 'seccomp.bpf');
13
+ const signal = request.signal;
14
+ let spec;
83
15
  try {
84
- const arguments_ = this.#arguments(mounts, cwd, shellRequest, runtimeMounts);
85
- await Promise.all([
86
- writeFile(argumentPath, `${arguments_.join('\0')}\0`, { mode: 0o600, flag: 'wx' }),
87
- writeFile(seccompPath, seccompFilter(process.arch), { mode: 0o600, flag: 'wx' })
88
- ]);
16
+ spec = await compileBubblewrapPolicy(this.options, this.#bubblewrapPath, request);
89
17
  }
90
18
  catch (error) {
91
- await rm(launchDirectory, { recursive: true, force: true }).catch(() => undefined);
92
- throw error instanceof AgentError ? error : policyInvalid(error);
93
- }
94
- let argumentFd;
95
- let seccompFd;
96
- try {
97
- argumentFd = openSync(argumentPath, 'r');
98
- seccompFd = openSync(seccompPath, 'r');
99
- rmSync(launchDirectory, { recursive: true, force: true });
100
- request.signal?.throwIfAborted();
101
- try {
102
- const child = spawn(this.#bubblewrapPath, ['--args', '3', '--', ...command], {
103
- cwd: '/',
104
- shell: false,
105
- stdio: ['pipe', 'pipe', 'pipe', argumentFd, seccompFd],
106
- windowsHide: true,
107
- detached: request.detached,
108
- env: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' }
109
- });
110
- await waitForSpawn(child);
111
- return child;
112
- }
113
- catch (error) {
114
- request.signal?.throwIfAborted();
115
- throw unavailable(error);
116
- }
117
- }
118
- finally {
119
- if (argumentFd !== undefined)
120
- closeSync(argumentFd);
121
- if (seccompFd !== undefined)
122
- closeSync(seccompFd);
123
- rmSync(launchDirectory, { recursive: true, force: true });
124
- }
125
- }
126
- async #mounts() {
127
- const mounts = [];
128
- for (const definition of this.options.profile.mounts ?? []) {
129
- mounts.push({
130
- source: await secureDirectory(definition.source),
131
- destination: sandboxDestination(definition.destination, definition.access),
132
- access: definition.access
133
- });
134
- }
135
- if (this.options.profile.temporaryDirectory?.kind === 'MOUNT')
136
- mounts.push({
137
- source: await secureDirectory(this.options.profile.temporaryDirectory.source),
138
- destination: '/tmp',
139
- access: 'READ_WRITE'
140
- });
141
- for (const [index, mount] of mounts.entries()) {
142
- for (const other of mounts.slice(index + 1)) {
143
- if (mount.destination === other.destination)
144
- throw policyInvalid();
145
- const parent = withinSandboxPath(mount.destination, other.destination)
146
- ? mount
147
- : withinSandboxPath(other.destination, mount.destination)
148
- ? other
149
- : undefined;
150
- if (parent !== undefined) {
151
- const child = parent === mount ? other : mount;
152
- if (parent.access !== 'READ_ONLY' || child.access !== 'READ_WRITE')
153
- throw policyInvalid();
154
- }
155
- if (mount.source === other.source) {
156
- if (mount.access !== other.access)
157
- throw policyInvalid();
158
- continue;
159
- }
160
- if (within(mount.source, other.source) || within(other.source, mount.source))
161
- throw policyInvalid();
162
- }
163
- }
164
- const ordered = [...mounts].sort(compareMountDestinations);
165
- await prepareNestedMountDestinations(ordered);
166
- return ordered;
167
- }
168
- #arguments(mounts, cwd, request, runtimeMounts) {
169
- const networkMode = this.options.profile.networkMode ?? 'NONE';
170
- const temporaryDirectory = this.options.profile.temporaryDirectory ?? { kind: 'EPHEMERAL' };
171
- const args = [
172
- '--unshare-user',
173
- '--unshare-pid',
174
- ...(networkMode === 'UNRESTRICTED' ? [] : ['--unshare-net']),
175
- '--unshare-uts',
176
- '--unshare-ipc',
177
- '--unshare-cgroup-try',
178
- '--disable-userns',
179
- '--die-with-parent',
180
- '--new-session',
181
- '--cap-drop',
182
- 'ALL',
183
- '--hostname',
184
- 'mar-hosted',
185
- '--clearenv',
186
- '--setenv',
187
- 'PATH',
188
- SANDBOX_PATH,
189
- '--setenv',
190
- 'HOME',
191
- '/home/mar',
192
- '--setenv',
193
- 'LANG',
194
- 'C.UTF-8',
195
- '--setenv',
196
- 'LC_ALL',
197
- 'C.UTF-8',
198
- '--setenv',
199
- 'TMPDIR',
200
- '/tmp',
201
- '--tmpfs',
202
- '/',
203
- ...runtimeMountArguments(),
204
- ...sandboxDirectoryArguments(runtimeMounts),
205
- ...runtimeMounts.flatMap((mount) => ['--ro-bind', mount.source, mount.destination]),
206
- ...sandboxDirectoryArguments(mounts),
207
- ...(temporaryDirectory.kind === 'EPHEMERAL' ? ['--tmpfs', '/tmp'] : []),
208
- '--proc',
209
- '/proc',
210
- '--dev',
211
- '/dev',
212
- '--tmpfs',
213
- '/run'
214
- ];
215
- for (const mount of mounts)
216
- args.push(mount.access === 'READ_WRITE' ? '--bind' : '--ro-bind', mount.source, mount.destination);
217
- if (request.owner !== undefined) {
218
- if (!CURSOR_SAFE_ID.test(request.owner.sessionId) ||
219
- (request.owner.executionId !== undefined && !CURSOR_SAFE_ID.test(request.owner.executionId)))
19
+ signal?.throwIfAborted();
20
+ if (error instanceof AgentError && error.code === 'SANDBOX_POLICY_INVALID')
220
21
  throw policyInvalid();
221
- args.push('--setenv', 'MAR_AGENT_SESSION_ID', request.owner.sessionId);
222
- if (request.owner.executionId !== undefined)
223
- args.push('--setenv', 'MAR_AGENT_EXECUTION_ID', request.owner.executionId);
224
- }
225
- for (const [name, value] of explicitEnvironment(request))
226
- args.push('--setenv', name, value);
227
- args.push('--seccomp', '4', '--chdir', cwd);
228
- return args;
229
- }
230
- }
231
- export async function probeBubblewrapSandbox(options = {}) {
232
- if (process.platform !== 'linux')
233
- throw unavailable();
234
- const bubblewrapPath = options.bubblewrapPath ?? DEFAULT_BUBBLEWRAP_PATH;
235
- const versionOutput = await execFileAsync(bubblewrapPath, ['--version'], {
236
- timeout: 5_000,
237
- windowsHide: true
238
- }).catch((error) => {
239
- throw unavailable(error);
240
- });
241
- const version = /^bubblewrap\s+([0-9]+(?:\.[0-9]+){1,3})\s*$/u.exec(versionOutput.stdout)?.[1];
242
- if (version === undefined)
243
- throw unavailable();
244
- const executor = new BubblewrapSandboxExecutor({
245
- bubblewrapPath,
246
- profile: options.profile ?? {
247
- policyVersion: LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION
248
- }
249
- });
250
- const child = await executor.spawn({
251
- command: '/bin/true',
252
- cwd: options.cwd ?? '/',
253
- shell: false,
254
- environment: {},
255
- detached: true,
256
- owner: { sessionId: 'sandbox-probe', executionId: 'sandbox-probe' }
257
- });
258
- const result = await waitForProbe(child);
259
- if (result.code !== 0)
260
- throw unavailable(new Error(result.stderr));
261
- return { version, policyVersion: LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION };
262
- }
263
- async function waitForProbe(child) {
264
- const chunks = [];
265
- let bytes = 0;
266
- child.stderr.on('data', (chunk) => {
267
- if (bytes >= 8_192)
268
- return;
269
- const remaining = 8_192 - bytes;
270
- chunks.push(chunk.subarray(0, remaining));
271
- bytes += Math.min(chunk.length, remaining);
272
- });
273
- child.stdin.end();
274
- return await new Promise((resolve, reject) => {
275
- const timer = setTimeout(() => {
276
- child.kill('SIGKILL');
277
- reject(unavailable(new Error('Sandbox probe timed out.')));
278
- }, 5_000);
279
- timer.unref();
280
- child.once('error', (error) => {
281
- clearTimeout(timer);
282
- reject(unavailable(error));
283
- });
284
- child.once('close', (code) => {
285
- clearTimeout(timer);
286
- resolve({ code, stderr: Buffer.concat(chunks).toString('utf8') });
287
- });
288
- });
289
- }
290
- async function waitForSpawn(child) {
291
- await new Promise((resolve, reject) => {
292
- const spawned = () => {
293
- child.off('error', failed);
294
- resolve();
295
- };
296
- const failed = (error) => {
297
- child.off('spawn', spawned);
298
- reject(unavailable(error));
299
- };
300
- child.once('spawn', spawned);
301
- child.once('error', failed);
302
- });
303
- }
304
- async function validateBackend(path) {
305
- try {
306
- const metadata = await lstat(path);
307
- if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o111) === 0)
308
22
  throw unavailable();
309
- }
310
- catch (error) {
311
- if (error instanceof AgentError)
312
- throw error;
313
- throw unavailable(error);
314
- }
315
- }
316
- async function secureDirectory(path) {
317
- if (!isAbsolute(path))
318
- throw policyInvalid();
319
- try {
320
- const metadata = await lstat(path);
321
- if (!metadata.isDirectory() || metadata.isSymbolicLink())
322
- throw policyInvalid();
323
- const canonical = await realpath(path);
324
- if (canonical !== resolve(path))
325
- throw policyInvalid();
326
- return canonical;
327
- }
328
- catch (error) {
329
- if (error instanceof AgentError)
330
- throw error;
331
- throw policyInvalid(error);
332
- }
333
- }
334
- function validateProfile(profile) {
335
- if (profile === null ||
336
- typeof profile !== 'object' ||
337
- profile.policyVersion !== LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION ||
338
- (profile.networkMode !== undefined &&
339
- profile.networkMode !== 'NONE' &&
340
- profile.networkMode !== 'UNRESTRICTED') ||
341
- (profile.mounts !== undefined && !Array.isArray(profile.mounts)))
342
- throw policyInvalid();
343
- for (const mount of profile.mounts ?? []) {
344
- if (mount === null ||
345
- typeof mount !== 'object' ||
346
- typeof mount.source !== 'string' ||
347
- typeof mount.destination !== 'string' ||
348
- (mount.access !== 'READ_ONLY' && mount.access !== 'READ_WRITE'))
349
- throw policyInvalid();
350
- }
351
- const temporaryDirectory = profile.temporaryDirectory;
352
- if (temporaryDirectory !== undefined &&
353
- (temporaryDirectory === null ||
354
- typeof temporaryDirectory !== 'object' ||
355
- (temporaryDirectory.kind !== 'EPHEMERAL' &&
356
- (temporaryDirectory.kind !== 'MOUNT' || typeof temporaryDirectory.source !== 'string'))))
357
- throw policyInvalid();
358
- const workingDirectory = profile.workingDirectory;
359
- if (workingDirectory !== undefined &&
360
- (workingDirectory === null ||
361
- typeof workingDirectory !== 'object' ||
362
- (workingDirectory.kind !== 'REQUEST' &&
363
- (workingDirectory.kind !== 'SANDBOX' || typeof workingDirectory.path !== 'string'))))
364
- throw policyInvalid();
365
- }
366
- async function workingDirectory(request, mounts, configuration) {
367
- const selected = configuration ?? { kind: 'SANDBOX', path: '/tmp' };
368
- if (selected.kind === 'REQUEST')
369
- return await sandboxPath(request.cwd, mounts);
370
- const path = normalizedSandboxPath(selected.path);
371
- if (path === '/tmp' || path === '/home/mar')
372
- return path;
373
- const mount = [...mounts]
374
- .sort((left, right) => right.destination.length - left.destination.length)
375
- .find((candidate) => path === candidate.destination || withinSandboxPath(candidate.destination, path));
376
- if (mount === undefined)
377
- throw policyInvalid();
378
- const suffix = posix.relative(mount.destination, path).split('/').filter(Boolean);
379
- await secureDirectory(join(mount.source, ...suffix));
380
- return path;
381
- }
382
- async function sandboxPath(path, mounts) {
383
- const canonical = await secureDirectory(path);
384
- const mount = [...mounts]
385
- .sort((left, right) => right.source.length - left.source.length)
386
- .find((candidate) => canonical === candidate.source || within(candidate.source, canonical));
387
- if (mount === undefined)
388
- throw policyInvalid();
389
- const suffix = relative(mount.source, canonical).split(sep).filter(Boolean);
390
- return suffix.length === 0 ? mount.destination : posix.join(mount.destination, ...suffix);
391
- }
392
- function sandboxDestination(path, access) {
393
- const normalized = normalizedSandboxPath(path);
394
- if (generatedImagesSandboxPath(normalized)) {
395
- if (access !== 'READ_ONLY')
396
- throw policyInvalid();
397
- return normalized;
398
- }
399
- if (normalized === '/' ||
400
- RESERVED_SANDBOX_ROOTS.some((root) => normalized === root ||
401
- withinSandboxPath(root, normalized) ||
402
- withinSandboxPath(normalized, root)))
403
- throw policyInvalid();
404
- return normalized;
405
- }
406
- function normalizedSandboxPath(path) {
407
- if (!path.startsWith('/') ||
408
- path.includes('\0') ||
409
- path.includes('\\') ||
410
- posix.normalize(path) !== path ||
411
- posix.resolve(path) !== path)
412
- throw policyInvalid();
413
- return path;
414
- }
415
- function sandboxCommand(request, mounts) {
416
- const command = request.shell === undefined || request.shell === false
417
- ? [sandboxExecutable(request.command, mounts), ...(request.args ?? [])]
418
- : [
419
- sandboxExecutable(typeof request.shell === 'string' ? request.shell : '/bin/sh', mounts),
420
- '-c',
421
- request.command,
422
- ...(request.args ?? [])
423
- ];
424
- return [
425
- '/usr/bin/prlimit',
426
- `--cpu=${SANDBOX_CPU_SECONDS}:${SANDBOX_CPU_SECONDS}`,
427
- `--nproc=${SANDBOX_PROCESS_LIMIT}:${SANDBOX_PROCESS_LIMIT}`,
428
- `--nofile=${SANDBOX_OPEN_FILE_LIMIT}:${SANDBOX_OPEN_FILE_LIMIT}`,
429
- `--fsize=${SANDBOX_FILE_SIZE_BYTES}:${SANDBOX_FILE_SIZE_BYTES}`,
430
- '--',
431
- ...command
432
- ];
433
- }
434
- function sandboxExecutable(command, mounts) {
435
- if (!isAbsolute(command))
436
- return command;
437
- const canonical = resolve(command);
438
- const nodeRuntimeDirectory = resolve(process.execPath, '..', '..');
439
- if (nodeRuntimeDirectory !== '/usr' &&
440
- (canonical === nodeRuntimeDirectory || within(nodeRuntimeDirectory, canonical))) {
441
- const suffix = relative(nodeRuntimeDirectory, canonical).split(sep).filter(Boolean);
442
- return suffix.length === 0 ? '/runtime/node' : posix.join('/runtime/node', ...suffix);
443
- }
444
- const mount = [...mounts]
445
- .sort((left, right) => right.source.length - left.source.length)
446
- .find((candidate) => canonical === candidate.source || within(candidate.source, canonical));
447
- if (mount !== undefined) {
448
- const suffix = relative(mount.source, canonical).split(sep).filter(Boolean);
449
- return suffix.length === 0 ? mount.destination : posix.join(mount.destination, ...suffix);
450
- }
451
- if (['/usr', '/bin', '/sbin', '/lib', '/lib64'].some((root) => canonical === root || within(root, canonical)))
452
- return canonical;
453
- throw policyInvalid();
454
- }
455
- function explicitEnvironment(request) {
456
- const entries = [];
457
- const seen = new Set();
458
- for (const name of request.environmentKeys ?? []) {
459
- if (seen.has(name))
460
- continue;
461
- seen.add(name);
462
- if (!SAFE_ENVIRONMENT_NAME.test(name))
463
- throw policyInvalid();
464
- if (FORBIDDEN_ENVIRONMENT_NAMES.has(name))
465
- continue;
466
- const value = request.environment[name];
467
- if (typeof value !== 'string' || value.includes('\0') || Buffer.byteLength(value) > 32_768)
468
- throw policyInvalid();
469
- entries.push([name, value]);
470
- }
471
- return entries;
472
- }
473
- function generatedImagesSandboxPath(path) {
474
- const segments = posix.relative(GENERATED_IMAGES_SANDBOX_ROOT, path).split('/').filter(Boolean);
475
- return (withinSandboxPath(GENERATED_IMAGES_SANDBOX_ROOT, path) &&
476
- segments.length === 2 &&
477
- safePathSegment(segments[0]) &&
478
- segments[1] === 'generated_images');
479
- }
480
- function safePathSegment(value) {
481
- return (value.length > 0 &&
482
- value.length <= 255 &&
483
- value !== '.' &&
484
- value !== '..' &&
485
- !value.includes('/') &&
486
- !value.includes('\\'));
487
- }
488
- function compareMountDestinations(left, right) {
489
- const depth = left.destination.split('/').filter(Boolean).length -
490
- right.destination.split('/').filter(Boolean).length;
491
- return depth === 0 ? left.destination.localeCompare(right.destination, 'en-US') : depth;
492
- }
493
- async function prepareNestedMountDestinations(mounts) {
494
- for (const [index, mount] of mounts.entries()) {
495
- const parent = mounts
496
- .slice(0, index)
497
- .filter((candidate) => withinSandboxPath(candidate.destination, mount.destination))
498
- .sort((left, right) => right.destination.length - left.destination.length)[0];
499
- if (parent === undefined)
500
- continue;
501
- const suffix = relative(parent.destination, mount.destination).split(sep).filter(Boolean);
502
- const target = join(parent.source, ...suffix);
503
- try {
504
- const metadata = await lstat(target);
505
- if (!metadata.isDirectory() ||
506
- metadata.isSymbolicLink() ||
507
- (await realpath(target)) !== resolve(target))
508
- throw policyInvalid();
509
- }
510
- catch (error) {
511
- if (error instanceof AgentError)
512
- throw error;
513
- throw policyInvalid(error);
514
23
  }
24
+ return launchBubblewrap(spec);
515
25
  }
516
26
  }
517
- function sandboxDirectoryArguments(mounts) {
518
- const directories = new Set(['/home', '/home/mar']);
519
- for (const mount of mounts) {
520
- const segments = mount.destination.split('/').filter(Boolean);
521
- let current = '';
522
- for (const segment of segments) {
523
- current = `${current}/${segment}`;
524
- directories.add(current);
525
- }
526
- }
527
- return [...directories]
528
- .sort((left, right) => {
529
- const depth = left.split('/').filter(Boolean).length - right.split('/').filter(Boolean).length;
530
- return depth === 0 ? left.localeCompare(right, 'en-US') : depth;
531
- })
532
- .flatMap((directory) => ['--dir', directory]);
533
- }
534
- /** Host assets retain absolute module URLs, but cannot overlay or widen profile mounts. */
535
- async function trustedRuntimeMounts(directories, mounts) {
536
- if (!Array.isArray(directories) || directories.length > 16)
537
- throw policyInvalid();
538
- const result = [];
539
- for (const path of directories) {
540
- if (typeof path !== 'string' || path.length > 4096)
541
- throw policyInvalid();
542
- const source = await secureDirectory(path);
543
- if (source.split('/').filter(Boolean).length < 3)
544
- throw policyInvalid();
545
- const overlaps = (left, right) => left === right || within(left, right) || within(right, left);
546
- if (mounts.some((mount) => overlaps(source, mount.source) || overlaps(source, mount.destination)) ||
547
- ['/home/mar', '/runtime', '/etc', '/proc', '/dev', '/run', '/tmp'].some((root) => overlaps(source, root)))
548
- throw policyInvalid();
549
- // These trees are already exposed read-only by the fixed runtime policy.
550
- if (['/usr', '/bin', '/sbin', '/lib', '/lib64'].some((root) => within(root, source)))
551
- continue;
552
- if (result.some((mount) => overlaps(source, mount.source)))
553
- throw policyInvalid();
554
- result.push({ source, destination: source, access: 'READ_ONLY' });
555
- }
556
- return result;
557
- }
558
- function runtimeMountArguments() {
559
- const args = [];
560
- const nodeRuntimeDirectory = resolve(process.execPath, '..', '..');
561
- if (nodeRuntimeDirectory !== '/usr')
562
- args.push('--dir', '/runtime', '--dir', '/runtime/node', '--ro-bind', nodeRuntimeDirectory, '/runtime/node');
563
- for (const path of ['/usr', '/bin', '/sbin', '/lib', '/lib64']) {
564
- if (!existsSync(path))
565
- continue;
566
- const metadata = lstatSync(path);
567
- if (metadata.isSymbolicLink())
568
- args.push('--symlink', readlinkSync(path), path);
569
- else if (metadata.isDirectory())
570
- args.push('--ro-bind', path, path);
571
- }
572
- for (const path of READ_ONLY_SYSTEM_CONFIGURATION_DIRECTORIES) {
573
- if (!existsSync(path) || !lstatSync(path).isDirectory())
574
- continue;
575
- args.push('--ro-bind', path, path);
576
- }
577
- for (const path of [
578
- '/etc/host.conf',
579
- '/etc/hosts',
580
- '/etc/ld.so.cache',
581
- '/etc/resolv.conf',
582
- '/etc/passwd',
583
- '/etc/group',
584
- '/etc/nsswitch.conf',
585
- '/etc/localtime',
586
- '/etc/ssl/certs'
587
- ]) {
588
- if (existsSync(path))
589
- args.push('--ro-bind', path, path);
590
- }
591
- return args;
592
- }
593
- function seccompFilter(architecture) {
594
- const policy = syscallPolicy(architecture);
595
- const filters = [
596
- [0x20, 0, 0, 4],
597
- [0x15, 1, 0, policy.auditArchitecture],
598
- [0x06, 0, 0, 0x80000000],
599
- [0x20, 0, 0, 0]
600
- ];
601
- if (architecture === 'x64')
602
- filters.push([0x54, 0, 0, 0x40000000], [0x15, 1, 0, 0], [0x06, 0, 0, 0x80000000], [0x20, 0, 0, 0]);
603
- for (const syscall of policy.blockedSyscalls)
604
- filters.push([0x15, 0, 1, syscall], [0x06, 0, 0, 0x00050001]);
605
- filters.push([0x06, 0, 0, 0x7fff0000]);
606
- const buffer = Buffer.alloc(filters.length * 8);
607
- for (const [index, [code, jumpTrue, jumpFalse, value]] of filters.entries()) {
608
- const offset = index * 8;
609
- buffer.writeUInt16LE(code, offset);
610
- buffer.writeUInt8(jumpTrue, offset + 2);
611
- buffer.writeUInt8(jumpFalse, offset + 3);
612
- buffer.writeUInt32LE(value >>> 0, offset + 4);
613
- }
614
- return buffer;
615
- }
616
- function syscallPolicy(architecture) {
617
- if (architecture === 'x64')
618
- return {
619
- auditArchitecture: 0xc000003e,
620
- blockedSyscalls: [
621
- 101, 155, 165, 166, 175, 176, 246, 248, 249, 250, 272, 298, 304, 308, 313, 321, 323, 428,
622
- 429, 430, 431, 432, 442
623
- ]
624
- };
625
- if (architecture === 'arm64')
626
- return {
627
- auditArchitecture: 0xc00000b7,
628
- blockedSyscalls: [
629
- 39, 40, 41, 97, 104, 105, 106, 117, 217, 218, 219, 241, 265, 268, 273, 280, 282, 428, 429,
630
- 430, 431, 432, 442
631
- ]
632
- };
633
- throw policyInvalid();
634
- }
635
- function within(parent, candidate) {
636
- const relation = relative(parent, candidate);
637
- return relation.length > 0 && relation !== '..' && !relation.startsWith(`..${sep}`);
638
- }
639
- function withinSandboxPath(parent, candidate) {
640
- const relation = posix.relative(parent, candidate);
641
- return relation.length > 0 && relation !== '..' && !relation.startsWith('../');
642
- }
643
- function unavailable(_cause) {
644
- return new AgentError('SANDBOX_UNAVAILABLE', 'Bubblewrap sandbox is unavailable.');
645
- }
646
- function policyInvalid(_cause) {
647
- return new AgentError('SANDBOX_POLICY_INVALID', 'Sandbox policy is invalid.');
648
- }
@@ -0,0 +1 @@
1
+ export declare function seccompFilter(architecture: string): Buffer;