@myagentroam/agent 0.11.35 → 0.11.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import { policyInvalid } from './sandbox-error.js';
2
+ export function seccompFilter(architecture) {
3
+ const policy = syscallPolicy(architecture);
4
+ const filters = [
5
+ [0x20, 0, 0, 4],
6
+ [0x15, 1, 0, policy.auditArchitecture],
7
+ [0x06, 0, 0, 0x80000000],
8
+ [0x20, 0, 0, 0]
9
+ ];
10
+ if (architecture === 'x64')
11
+ filters.push([0x54, 0, 0, 0x40000000], [0x15, 1, 0, 0], [0x06, 0, 0, 0x80000000], [0x20, 0, 0, 0]);
12
+ for (const syscall of policy.blockedSyscalls)
13
+ filters.push([0x15, 0, 1, syscall], [0x06, 0, 0, 0x00050001]);
14
+ filters.push([0x06, 0, 0, 0x7fff0000]);
15
+ const buffer = Buffer.alloc(filters.length * 8);
16
+ for (const [index, [code, jumpTrue, jumpFalse, value]] of filters.entries()) {
17
+ const offset = index * 8;
18
+ buffer.writeUInt16LE(code, offset);
19
+ buffer.writeUInt8(jumpTrue, offset + 2);
20
+ buffer.writeUInt8(jumpFalse, offset + 3);
21
+ buffer.writeUInt32LE(value >>> 0, offset + 4);
22
+ }
23
+ return buffer;
24
+ }
25
+ function syscallPolicy(architecture) {
26
+ if (architecture === 'x64')
27
+ return {
28
+ auditArchitecture: 0xc000003e,
29
+ blockedSyscalls: [
30
+ 101, 155, 165, 166, 175, 176, 246, 248, 249, 250, 272, 298, 304, 308, 313, 321, 323, 428,
31
+ 429, 430, 431, 432, 442
32
+ ]
33
+ };
34
+ if (architecture === 'arm64')
35
+ return {
36
+ auditArchitecture: 0xc00000b7,
37
+ blockedSyscalls: [
38
+ 39, 40, 41, 97, 104, 105, 106, 117, 217, 218, 219, 241, 265, 268, 273, 280, 282, 428, 429,
39
+ 430, 431, 432, 442
40
+ ]
41
+ };
42
+ throw policyInvalid();
43
+ }
@@ -1,2 +1,3 @@
1
- export { BubblewrapSandboxExecutor, probeBubblewrapSandbox } from './bubblewrap-sandbox-executor.js';
1
+ export { BubblewrapSandboxExecutor } from './bubblewrap-sandbox-executor.js';
2
+ export { probeBubblewrapSandbox } from './bubblewrap-probe.js';
2
3
  export { LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION, type BubblewrapSandboxExecutorOptions, type BubblewrapSandboxProfile, type LinuxSandboxMount, type LinuxSandboxNetworkMode, type LinuxSandboxTemporaryDirectory, type LinuxSandboxWorkingDirectory } from './profile.js';
@@ -1,2 +1,3 @@
1
- export { BubblewrapSandboxExecutor, probeBubblewrapSandbox } from './bubblewrap-sandbox-executor.js';
1
+ export { BubblewrapSandboxExecutor } from './bubblewrap-sandbox-executor.js';
2
+ export { probeBubblewrapSandbox } from './bubblewrap-probe.js';
2
3
  export { LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION } from './profile.js';
@@ -0,0 +1,4 @@
1
+ import { AgentError } from '../../error.js';
2
+ export declare function unavailable(_cause?: unknown): AgentError;
3
+ export declare function policyInvalid(_cause?: unknown): AgentError;
4
+ export declare function withCleanupDiagnostics(error: AgentError, failures: readonly ('close' | 'remove' | 'terminate' | 'exit_timeout')[]): AgentError;
@@ -0,0 +1,14 @@
1
+ import { AgentError } from '../../error.js';
2
+ export function unavailable(_cause) {
3
+ return new AgentError('SANDBOX_UNAVAILABLE', 'Bubblewrap sandbox is unavailable.');
4
+ }
5
+ export function policyInvalid(_cause) {
6
+ return new AgentError('SANDBOX_POLICY_INVALID', 'Sandbox policy is invalid.');
7
+ }
8
+ export function withCleanupDiagnostics(error, failures) {
9
+ if (failures.length === 0)
10
+ return error;
11
+ return new AgentError(error.code, error.publicMessage, {
12
+ details: { cleanup: [...new Set(failures)] }
13
+ });
14
+ }
@@ -0,0 +1,13 @@
1
+ import type { LocalProcessSpawnRequest } from '../../process/contracts.js';
2
+ import type { BubblewrapSandboxProfile, BubblewrapSandboxExecutorOptions, LinuxSandboxMount } from './profile.js';
3
+ export interface SandboxMount {
4
+ readonly source: string;
5
+ readonly destination: string;
6
+ readonly access: LinuxSandboxMount['access'];
7
+ }
8
+ export declare function resolveMounts(profile: BubblewrapSandboxProfile): Promise<readonly SandboxMount[]>;
9
+ export declare function workingDirectory(request: LocalProcessSpawnRequest, mounts: readonly SandboxMount[], configuration: BubblewrapSandboxExecutorOptions['profile']['workingDirectory']): Promise<string>;
10
+ export declare function sandboxDirectoryArguments(mounts: readonly SandboxMount[]): string[];
11
+ /** Host assets retain absolute module URLs, but cannot overlay or widen profile mounts. */
12
+ export declare function trustedRuntimeMounts(directories: readonly string[], mounts: readonly SandboxMount[]): Promise<readonly SandboxMount[]>;
13
+ export declare function within(parent: string, candidate: string): boolean;
@@ -0,0 +1,221 @@
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
3
+ import { AgentError } from '../../error.js';
4
+ import { policyInvalid } from './sandbox-error.js';
5
+ const GENERATED_IMAGES_SANDBOX_ROOT = '/home/mar/.mar/agent/sessions';
6
+ const RESERVED_SANDBOX_ROOTS = [
7
+ '/home/mar',
8
+ '/runtime',
9
+ '/usr',
10
+ '/bin',
11
+ '/sbin',
12
+ '/lib',
13
+ '/lib64',
14
+ '/etc',
15
+ '/proc',
16
+ '/dev',
17
+ '/run',
18
+ '/tmp'
19
+ ];
20
+ export async function resolveMounts(profile) {
21
+ const mounts = [];
22
+ for (const definition of profile.mounts ?? []) {
23
+ mounts.push({
24
+ source: await secureDirectory(definition.source),
25
+ destination: sandboxDestination(definition.destination, definition.access),
26
+ access: definition.access
27
+ });
28
+ }
29
+ if (profile.temporaryDirectory?.kind === 'MOUNT')
30
+ mounts.push({
31
+ source: await secureDirectory(profile.temporaryDirectory.source),
32
+ destination: '/tmp',
33
+ access: 'READ_WRITE'
34
+ });
35
+ for (const [index, mount] of mounts.entries()) {
36
+ for (const other of mounts.slice(index + 1)) {
37
+ if (mount.destination === other.destination)
38
+ throw policyInvalid();
39
+ const parent = withinSandboxPath(mount.destination, other.destination)
40
+ ? mount
41
+ : withinSandboxPath(other.destination, mount.destination)
42
+ ? other
43
+ : undefined;
44
+ if (parent !== undefined) {
45
+ const child = parent === mount ? other : mount;
46
+ if (parent.access !== 'READ_ONLY' || child.access !== 'READ_WRITE')
47
+ throw policyInvalid();
48
+ }
49
+ if (mount.source === other.source) {
50
+ if (mount.access !== other.access)
51
+ throw policyInvalid();
52
+ continue;
53
+ }
54
+ if (within(mount.source, other.source) || within(other.source, mount.source))
55
+ throw policyInvalid();
56
+ }
57
+ }
58
+ const ordered = [...mounts].sort(compareMountDestinations);
59
+ await prepareNestedMountDestinations(ordered);
60
+ return ordered;
61
+ }
62
+ async function secureDirectory(path) {
63
+ if (!isAbsolute(path))
64
+ throw policyInvalid();
65
+ try {
66
+ const metadata = await lstat(path);
67
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
68
+ throw policyInvalid();
69
+ const canonical = await realpath(path);
70
+ if (canonical !== resolve(path))
71
+ throw policyInvalid();
72
+ return canonical;
73
+ }
74
+ catch (error) {
75
+ if (error instanceof AgentError)
76
+ throw error;
77
+ throw policyInvalid(error);
78
+ }
79
+ }
80
+ export async function workingDirectory(request, mounts, configuration) {
81
+ const selected = configuration ?? { kind: 'SANDBOX', path: '/tmp' };
82
+ if (selected.kind === 'REQUEST')
83
+ return await sandboxPath(request.cwd, mounts);
84
+ const path = normalizedSandboxPath(selected.path);
85
+ if (path === '/tmp' || path === '/home/mar')
86
+ return path;
87
+ const mount = [...mounts]
88
+ .sort((left, right) => right.destination.length - left.destination.length)
89
+ .find((candidate) => path === candidate.destination || withinSandboxPath(candidate.destination, path));
90
+ if (mount === undefined)
91
+ throw policyInvalid();
92
+ const suffix = posix.relative(mount.destination, path).split('/').filter(Boolean);
93
+ await secureDirectory(join(mount.source, ...suffix));
94
+ return path;
95
+ }
96
+ async function sandboxPath(path, mounts) {
97
+ const canonical = await secureDirectory(path);
98
+ const mount = [...mounts]
99
+ .sort((left, right) => right.source.length - left.source.length)
100
+ .find((candidate) => canonical === candidate.source || within(candidate.source, canonical));
101
+ if (mount === undefined)
102
+ throw policyInvalid();
103
+ const suffix = relative(mount.source, canonical).split(sep).filter(Boolean);
104
+ return suffix.length === 0 ? mount.destination : posix.join(mount.destination, ...suffix);
105
+ }
106
+ function sandboxDestination(path, access) {
107
+ const normalized = normalizedSandboxPath(path);
108
+ if (generatedImagesSandboxPath(normalized)) {
109
+ if (access !== 'READ_ONLY')
110
+ throw policyInvalid();
111
+ return normalized;
112
+ }
113
+ if (normalized === '/' ||
114
+ RESERVED_SANDBOX_ROOTS.some((root) => normalized === root ||
115
+ withinSandboxPath(root, normalized) ||
116
+ withinSandboxPath(normalized, root)))
117
+ throw policyInvalid();
118
+ return normalized;
119
+ }
120
+ function normalizedSandboxPath(path) {
121
+ if (!path.startsWith('/') ||
122
+ path.includes('\0') ||
123
+ path.includes('\\') ||
124
+ posix.normalize(path) !== path ||
125
+ posix.resolve(path) !== path)
126
+ throw policyInvalid();
127
+ return path;
128
+ }
129
+ function generatedImagesSandboxPath(path) {
130
+ const segments = posix.relative(GENERATED_IMAGES_SANDBOX_ROOT, path).split('/').filter(Boolean);
131
+ return (withinSandboxPath(GENERATED_IMAGES_SANDBOX_ROOT, path) &&
132
+ segments.length === 2 &&
133
+ safePathSegment(segments[0]) &&
134
+ segments[1] === 'generated_images');
135
+ }
136
+ function safePathSegment(value) {
137
+ return (value.length > 0 &&
138
+ value.length <= 255 &&
139
+ value !== '.' &&
140
+ value !== '..' &&
141
+ !value.includes('/') &&
142
+ !value.includes('\\'));
143
+ }
144
+ function compareMountDestinations(left, right) {
145
+ const depth = left.destination.split('/').filter(Boolean).length -
146
+ right.destination.split('/').filter(Boolean).length;
147
+ return depth === 0 ? left.destination.localeCompare(right.destination, 'en-US') : depth;
148
+ }
149
+ async function prepareNestedMountDestinations(mounts) {
150
+ for (const [index, mount] of mounts.entries()) {
151
+ const parent = mounts
152
+ .slice(0, index)
153
+ .filter((candidate) => withinSandboxPath(candidate.destination, mount.destination))
154
+ .sort((left, right) => right.destination.length - left.destination.length)[0];
155
+ if (parent === undefined)
156
+ continue;
157
+ const suffix = relative(parent.destination, mount.destination).split(sep).filter(Boolean);
158
+ const target = join(parent.source, ...suffix);
159
+ try {
160
+ const metadata = await lstat(target);
161
+ if (!metadata.isDirectory() ||
162
+ metadata.isSymbolicLink() ||
163
+ (await realpath(target)) !== resolve(target))
164
+ throw policyInvalid();
165
+ }
166
+ catch (error) {
167
+ if (error instanceof AgentError)
168
+ throw error;
169
+ throw policyInvalid(error);
170
+ }
171
+ }
172
+ }
173
+ export function sandboxDirectoryArguments(mounts) {
174
+ const directories = new Set(['/home', '/home/mar']);
175
+ for (const mount of mounts) {
176
+ const segments = mount.destination.split('/').filter(Boolean);
177
+ let current = '';
178
+ for (const segment of segments) {
179
+ current = `${current}/${segment}`;
180
+ directories.add(current);
181
+ }
182
+ }
183
+ return [...directories]
184
+ .sort((left, right) => {
185
+ const depth = left.split('/').filter(Boolean).length - right.split('/').filter(Boolean).length;
186
+ return depth === 0 ? left.localeCompare(right, 'en-US') : depth;
187
+ })
188
+ .flatMap((directory) => ['--dir', directory]);
189
+ }
190
+ /** Host assets retain absolute module URLs, but cannot overlay or widen profile mounts. */
191
+ export async function trustedRuntimeMounts(directories, mounts) {
192
+ if (!Array.isArray(directories) || directories.length > 16)
193
+ throw policyInvalid();
194
+ const result = [];
195
+ for (const path of directories) {
196
+ if (typeof path !== 'string' || path.length > 4096)
197
+ throw policyInvalid();
198
+ const source = await secureDirectory(path);
199
+ if (source.split('/').filter(Boolean).length < 3)
200
+ throw policyInvalid();
201
+ const overlaps = (left, right) => left === right || within(left, right) || within(right, left);
202
+ if (mounts.some((mount) => overlaps(source, mount.source) || overlaps(source, mount.destination)) ||
203
+ ['/home/mar', '/runtime', '/etc', '/proc', '/dev', '/run', '/tmp'].some((root) => overlaps(source, root)))
204
+ throw policyInvalid();
205
+ // These trees are already exposed read-only by the fixed runtime policy.
206
+ if (['/usr', '/bin', '/sbin', '/lib', '/lib64'].some((root) => within(root, source)))
207
+ continue;
208
+ if (result.some((mount) => overlaps(source, mount.source)))
209
+ throw policyInvalid();
210
+ result.push({ source, destination: source, access: 'READ_ONLY' });
211
+ }
212
+ return result;
213
+ }
214
+ export function within(parent, candidate) {
215
+ const relation = relative(parent, candidate);
216
+ return relation.length > 0 && relation !== '..' && !relation.startsWith(`..${sep}`);
217
+ }
218
+ function withinSandboxPath(parent, candidate) {
219
+ const relation = posix.relative(parent, candidate);
220
+ return relation.length > 0 && relation !== '..' && !relation.startsWith('../');
221
+ }
@@ -0,0 +1,3 @@
1
+ import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ /** Signal only the detached group created for this child; never signal the host's group. */
3
+ export declare function terminateSandboxChild(child: ChildProcessWithoutNullStreams, detached: boolean): boolean;
@@ -0,0 +1,32 @@
1
+ /** Signal only the detached group created for this child; never signal the host's group. */
2
+ export function terminateSandboxChild(child, detached) {
3
+ const pid = child.pid;
4
+ if (pid !== undefined && (!Number.isInteger(pid) || pid <= 1 || pid === process.pid))
5
+ return false;
6
+ if (detached && pid !== undefined && pid > 1) {
7
+ try {
8
+ process.kill(-pid, 'SIGKILL');
9
+ return true;
10
+ }
11
+ catch (error) {
12
+ if (error.code !== 'ESRCH') {
13
+ // Even if group termination failed, do not abandon a live direct child.
14
+ if (child.exitCode === null && child.signalCode === null) {
15
+ try {
16
+ child.kill('SIGKILL');
17
+ }
18
+ catch {
19
+ // The false result reports the unconfirmed group and child termination.
20
+ }
21
+ }
22
+ return false;
23
+ }
24
+ if (child.exitCode !== null || child.signalCode !== null)
25
+ return true;
26
+ // A spawn failure may not have created the detached group yet.
27
+ }
28
+ }
29
+ if (child.exitCode !== null || child.signalCode !== null)
30
+ return true;
31
+ return child.kill('SIGKILL');
32
+ }
@@ -35,7 +35,7 @@ export async function copyGeneratedImages(sourceSessionDirectory, targetSessionD
35
35
  for (const fileName of fileNames) {
36
36
  const path = join(target, fileName);
37
37
  await copyFile(join(source, fileName), path);
38
- const handle = await open(path, 'r');
38
+ const handle = await open(path, 'r+');
39
39
  try {
40
40
  await handle.sync();
41
41
  }
@@ -97,7 +97,7 @@ async function copyOptionalFile(source, target) {
97
97
  if (!sourceInfo.isFile())
98
98
  return;
99
99
  await copyFile(source, target);
100
- const handle = await open(target, 'r');
100
+ const handle = await open(target, 'r+');
101
101
  await syncAndClose(handle);
102
102
  }
103
103
  function corruptHistory() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.11.35",
3
+ "version": "0.11.37",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",