@myagentroam/agent 0.9.163 → 0.9.164
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/agent-core/host/local-process-executor.d.ts +3 -1
- package/dist/agent-core/host/local-process-executor.js +2 -0
- package/dist/agent-core/host/local-session-environment.js +11 -0
- package/dist/agent-core/host/local-shell.d.ts +3 -0
- package/dist/agent-core/host/local-shell.js +78 -0
- package/dist/agent-core/tools/agent/todo-read.js +1 -1
- package/dist/agent-core/tools/agent/todo-write.js +4 -7
- package/dist/agent-core/tools/agent/todo.d.ts +0 -1
- package/dist/agent-core/tools/agent/todo.js +1 -8
- package/dist/agent-core/tools/local/exec.d.ts +1 -0
- package/dist/agent-core/tools/local/exec.js +10 -1
- package/dist/sandbox/linux/bubblewrap-sandbox-executor.js +15 -2
- package/package.json +1 -1
|
@@ -4,9 +4,11 @@ export interface LocalProcessSpawnRequest {
|
|
|
4
4
|
readonly args?: readonly string[];
|
|
5
5
|
readonly cwd: string;
|
|
6
6
|
readonly shell?: boolean | string;
|
|
7
|
+
/** Shell startup for exec only; native processes and workers omit this field. */
|
|
8
|
+
readonly shellLogin?: boolean;
|
|
7
9
|
readonly environment: NodeJS.ProcessEnv;
|
|
8
10
|
/** Explicit per-start values take precedence over execution-bound Host environment. */
|
|
9
|
-
readonly environmentOverrides?: Readonly<Record<string, string>>;
|
|
11
|
+
readonly environmentOverrides?: Readonly<Record<string, string | undefined>>;
|
|
10
12
|
readonly environmentKeys?: readonly string[];
|
|
11
13
|
/** Trusted, bounded read-only worker assets; never supplied by model tool input. */
|
|
12
14
|
readonly runtimeDirectories?: readonly string[];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { prepareLocalShellProcess } from './local-shell.js';
|
|
2
3
|
/** Preserves the direct local execution behavior used by the CLI and normal Node Workbench. */
|
|
3
4
|
export const directLocalProcessExecutor = {
|
|
4
5
|
async spawn(request) {
|
|
6
|
+
request = prepareLocalShellProcess(request);
|
|
5
7
|
return spawn(request.command, [...(request.args ?? [])], {
|
|
6
8
|
cwd: request.cwd,
|
|
7
9
|
shell: request.shell ?? false,
|
|
@@ -50,12 +50,22 @@ export class LocalSessionEnvironment {
|
|
|
50
50
|
(this.platform !== 'win32' || request.environmentOverrides === undefined))
|
|
51
51
|
return request;
|
|
52
52
|
const environment = { ...request.environment };
|
|
53
|
+
const overrides = {};
|
|
53
54
|
const assign = (key, value) => {
|
|
54
55
|
if (this.platform === 'win32') {
|
|
55
56
|
for (const current of Object.keys(environment))
|
|
56
57
|
if (this.#key(current) === this.#key(key))
|
|
57
58
|
delete environment[current];
|
|
59
|
+
for (const current of Object.keys(overrides))
|
|
60
|
+
if (this.#key(current) === this.#key(key))
|
|
61
|
+
delete overrides[current];
|
|
58
62
|
}
|
|
63
|
+
Object.defineProperty(overrides, key, {
|
|
64
|
+
value,
|
|
65
|
+
enumerable: true,
|
|
66
|
+
writable: true,
|
|
67
|
+
configurable: true
|
|
68
|
+
});
|
|
59
69
|
if (value === undefined)
|
|
60
70
|
delete environment[key];
|
|
61
71
|
else
|
|
@@ -75,6 +85,7 @@ export class LocalSessionEnvironment {
|
|
|
75
85
|
return {
|
|
76
86
|
...request,
|
|
77
87
|
environment,
|
|
88
|
+
environmentOverrides: overrides,
|
|
78
89
|
...(request.environmentKeys === undefined
|
|
79
90
|
? {}
|
|
80
91
|
: {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { LocalProcessSpawnRequest } from './local-process-executor.js';
|
|
2
|
+
/** Resolve Shell startup in the executor, after its environment policy has been applied. */
|
|
3
|
+
export declare function prepareLocalShellProcess(request: LocalProcessSpawnRequest, platform?: NodeJS.Platform): LocalProcessSpawnRequest;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { basename, win32 } from 'node:path';
|
|
3
|
+
/** Resolve Shell startup in the executor, after its environment policy has been applied. */
|
|
4
|
+
export function prepareLocalShellProcess(request, platform = process.platform) {
|
|
5
|
+
if (!request.shell || request.shellLogin === undefined)
|
|
6
|
+
return request;
|
|
7
|
+
const shell = typeof request.shell === 'string'
|
|
8
|
+
? request.shell
|
|
9
|
+
: platform === 'win32'
|
|
10
|
+
? (request.environment.ComSpec ?? request.environment.COMSPEC ?? 'cmd.exe')
|
|
11
|
+
: '/bin/sh';
|
|
12
|
+
const name = (platform === 'win32' ? win32.basename(shell) : basename(shell))
|
|
13
|
+
.replace(/\.exe$/iu, '')
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
const powershell = name === 'pwsh' || name === 'powershell';
|
|
16
|
+
if (!powershell && !['bash', 'zsh', 'sh'].includes(name))
|
|
17
|
+
return request;
|
|
18
|
+
const login = request.shellLogin;
|
|
19
|
+
const environment = { ...request.environment };
|
|
20
|
+
const script = [];
|
|
21
|
+
if (login && name === 'bash') {
|
|
22
|
+
script.push('if [ -z "${BASH_ENV-}" ] && [ -n "${HOME-}" ] && [ -r "$HOME/.bashrc" ]; then', ' . "$HOME/.bashrc"', 'fi');
|
|
23
|
+
}
|
|
24
|
+
else if (login && name === 'zsh') {
|
|
25
|
+
script.push('if [[ -n "${ZDOTDIR-}" ]]; then', ' [[ ! -r "$ZDOTDIR/.zshrc" ]] || . "$ZDOTDIR/.zshrc"', 'elif [[ -n "${HOME-}" ]]; then', ' [[ ! -r "$HOME/.zshrc" ]] || . "$HOME/.zshrc"', 'fi');
|
|
26
|
+
}
|
|
27
|
+
// Keep values out of argv and tool-visible command history, including quoting-sensitive values.
|
|
28
|
+
const privatePrefix = `__MAR_SHELL_${randomUUID().replaceAll('-', '')}_`;
|
|
29
|
+
const restoration = [];
|
|
30
|
+
let index = 0;
|
|
31
|
+
for (const [key, value] of Object.entries(request.environmentOverrides ?? {})) {
|
|
32
|
+
if (!powershell && !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key))
|
|
33
|
+
continue;
|
|
34
|
+
const quotedKey = `'${key.replaceAll("'", "''")}'`;
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
restoration.push(powershell
|
|
37
|
+
? `[Environment]::SetEnvironmentVariable(${quotedKey}, $null, 'Process')`
|
|
38
|
+
: `if [ "\${${key}+x}" = x ]; then unset ${key} || exit; fi`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const privateKey = `${privatePrefix}${index++}`;
|
|
42
|
+
environment[privateKey] = value;
|
|
43
|
+
restoration.push(powershell
|
|
44
|
+
? `if (-not (Test-Path Env:${privateKey})) { throw 'Shell startup removed a protected environment value.' }`
|
|
45
|
+
: `[ "\${${privateKey}+x}" = x ] || { printf '%s\\n' 'Shell startup removed a protected environment value.' >&2; exit 1; }`);
|
|
46
|
+
restoration.push(powershell
|
|
47
|
+
? `[Environment]::SetEnvironmentVariable(${quotedKey}, $env:${privateKey}, 'Process'); Remove-Item Env:${privateKey}`
|
|
48
|
+
: `if [ "\${${key}+x}" != x ] || [ "\${${key}-}" != "\$${privateKey}" ]; then export ${key}="\$${privateKey}" || exit; else export ${key} || exit; fi; unset ${privateKey}`);
|
|
49
|
+
}
|
|
50
|
+
if (restoration.length && !powershell) {
|
|
51
|
+
const traceKey = `${privatePrefix}TRACE`;
|
|
52
|
+
// A profile may enable xtrace; do not print expanded private values during restoration.
|
|
53
|
+
script.push(`case $- in *x*) set +x; ${traceKey}=1 ;; *) ${traceKey}= ;; esac`, ...restoration, `if [ "\$${traceKey}" = 1 ]; then unset ${traceKey}; set -x; else unset ${traceKey}; fi`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
script.push(...restoration);
|
|
57
|
+
}
|
|
58
|
+
script.push(request.command);
|
|
59
|
+
return {
|
|
60
|
+
...request,
|
|
61
|
+
command: shell,
|
|
62
|
+
args: [
|
|
63
|
+
...(powershell ? [...(login ? [] : ['-NoProfile']), '-Command'] : [login ? '-lc' : '-c']),
|
|
64
|
+
script.join('\n'),
|
|
65
|
+
...(request.args ?? [])
|
|
66
|
+
],
|
|
67
|
+
shell: false,
|
|
68
|
+
environment,
|
|
69
|
+
...(request.environmentKeys === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: {
|
|
72
|
+
environmentKeys: [
|
|
73
|
+
...request.environmentKeys,
|
|
74
|
+
...Object.keys(environment).filter((key) => key.startsWith(privatePrefix))
|
|
75
|
+
]
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -2,7 +2,7 @@ export const todoReadToolDefinition = {
|
|
|
2
2
|
name: 'todo_read',
|
|
3
3
|
allowedCallers: ['direct', 'programmatic'],
|
|
4
4
|
parallelSafety: 'safe',
|
|
5
|
-
description: 'Read this session’s current Todo revision and items.
|
|
5
|
+
description: 'Read this session’s current Todo revision and items when you need the existing list. A prior read is not required to write Todo. Do not read an empty or new Todo merely to decide whether straightforward bounded work needs one.',
|
|
6
6
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
7
7
|
};
|
|
8
8
|
export function executeTodoRead(context) {
|
|
@@ -4,11 +4,10 @@ export const todoWriteToolDefinition = {
|
|
|
4
4
|
name: 'todo_write',
|
|
5
5
|
allowedCallers: ['direct', 'programmatic'],
|
|
6
6
|
parallelSafety: 'serial',
|
|
7
|
-
description: 'Replace this session’s entire Todo list
|
|
7
|
+
description: 'Replace this session’s entire Todo list with the supplied items; no prior read is required. Preserve stable IDs and statuses only when extending the same objective. For a materially different objective, include only the new objective’s items. Do not retain completed items from an earlier objective; when all current items are completed, default a substantive new request to replacement unless the user explicitly asks to continue, extend, verify, or follow up. Use only after Todo has been chosen for coordination-heavy work; straightforward bounded work does not need it. Keep at most one in_progress item and update statuses after meaningful stage changes. When another substantive tool call is already ready and does not depend on the Todo result, include both in the same model response instead of spending a response only for Todo bookkeeping.',
|
|
8
8
|
inputSchema: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
11
|
-
expectedRevision: { type: 'integer' },
|
|
12
11
|
items: {
|
|
13
12
|
type: 'array',
|
|
14
13
|
maxItems: TOOL_EXECUTION_LIMITS.todoMaxItems,
|
|
@@ -24,7 +23,7 @@ export const todoWriteToolDefinition = {
|
|
|
24
23
|
}
|
|
25
24
|
}
|
|
26
25
|
},
|
|
27
|
-
required: ['
|
|
26
|
+
required: ['items'],
|
|
28
27
|
additionalProperties: false
|
|
29
28
|
}
|
|
30
29
|
};
|
|
@@ -38,9 +37,7 @@ function parseTodoWriteInput(value) {
|
|
|
38
37
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
39
38
|
return invalid();
|
|
40
39
|
const input = value;
|
|
41
|
-
if (Object.keys(input).some((key) =>
|
|
42
|
-
!Number.isInteger(input.expectedRevision) ||
|
|
43
|
-
input.expectedRevision < 0 ||
|
|
40
|
+
if (Object.keys(input).some((key) => key !== 'items') ||
|
|
44
41
|
!Array.isArray(input.items) ||
|
|
45
42
|
input.items.length > TOOL_EXECUTION_LIMITS.todoMaxItems)
|
|
46
43
|
return invalid();
|
|
@@ -59,7 +56,7 @@ function parseTodoWriteInput(value) {
|
|
|
59
56
|
return invalid();
|
|
60
57
|
return { id: record.id, step: record.step, status: record.status };
|
|
61
58
|
});
|
|
62
|
-
return {
|
|
59
|
+
return { items };
|
|
63
60
|
}
|
|
64
61
|
function invalid() {
|
|
65
62
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Todo input is invalid.');
|
|
@@ -14,10 +14,6 @@ export class TodoStore {
|
|
|
14
14
|
return { revision: this.#revision, items: this.#items.map((item) => ({ ...item })) };
|
|
15
15
|
}
|
|
16
16
|
write(input) {
|
|
17
|
-
if (input.expectedRevision !== this.#revision)
|
|
18
|
-
throw new MarAgentError('MAR_AGENT_TODO_CONFLICT', 'Todo revision is stale.', {
|
|
19
|
-
details: { revision: this.#revision }
|
|
20
|
-
});
|
|
21
17
|
this.#items = normalizeTodoItems(input.items);
|
|
22
18
|
this.#revision++;
|
|
23
19
|
return this.read();
|
|
@@ -44,10 +40,7 @@ export function restoreTodoStore(snapshots) {
|
|
|
44
40
|
const items = todoItemsFromSnapshot(snapshot);
|
|
45
41
|
if (!items)
|
|
46
42
|
continue;
|
|
47
|
-
todo.write({
|
|
48
|
-
expectedRevision: todo.read().revision,
|
|
49
|
-
items
|
|
50
|
-
});
|
|
43
|
+
todo.write({ items });
|
|
51
44
|
}
|
|
52
45
|
return todo;
|
|
53
46
|
}
|
|
@@ -53,6 +53,11 @@ export const execToolDefinition = {
|
|
|
53
53
|
type: 'string',
|
|
54
54
|
description: 'Optional relative/absolute start directory; defaults to workspace. Command-internal relative paths resolve from this directory.'
|
|
55
55
|
},
|
|
56
|
+
login: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
default: true,
|
|
59
|
+
description: 'Use the Shell login/profile initialization. Defaults to true; false skips login startup. Does not force an interactive Shell.'
|
|
60
|
+
},
|
|
56
61
|
timeoutMs: {
|
|
57
62
|
type: 'integer',
|
|
58
63
|
minimum: 1,
|
|
@@ -105,7 +110,7 @@ export function execToolInput(name, value) {
|
|
|
105
110
|
return invalidExecInput();
|
|
106
111
|
const input = value;
|
|
107
112
|
const allowed = name === 'exec'
|
|
108
|
-
? ['command', 'cwd', 'timeoutMs', 'yieldMs']
|
|
113
|
+
? ['command', 'cwd', 'login', 'timeoutMs', 'yieldMs']
|
|
109
114
|
: ['processId', 'action', 'yieldMs', 'data'];
|
|
110
115
|
if (Object.keys(input).some((key) => !allowed.includes(key)))
|
|
111
116
|
return invalidExecInput();
|
|
@@ -174,6 +179,7 @@ export class ExecTool {
|
|
|
174
179
|
command: input.command,
|
|
175
180
|
cwd: cwd.physicalPath,
|
|
176
181
|
shell: this.shell ?? true,
|
|
182
|
+
shellLogin: input.login ?? true,
|
|
177
183
|
environment: this.environment,
|
|
178
184
|
displayCwd: cwd.displayPath,
|
|
179
185
|
detached: process.platform !== 'win32',
|
|
@@ -662,11 +668,14 @@ function parseExecInput(value) {
|
|
|
662
668
|
return invalidExecInput();
|
|
663
669
|
if (input.cwd !== undefined && input.cwd !== null && typeof input.cwd !== 'string')
|
|
664
670
|
return invalidExecInput();
|
|
671
|
+
if (input.login !== undefined && input.login !== null && typeof input.login !== 'boolean')
|
|
672
|
+
return invalidExecInput();
|
|
665
673
|
const timeoutMs = optionalBoundedInteger(input.timeoutMs, 1, TOOL_EXECUTION_LIMITS.execMaxTimeoutMs);
|
|
666
674
|
return {
|
|
667
675
|
...(input.action === 'start' ? { action: 'start' } : {}),
|
|
668
676
|
command: input.command,
|
|
669
677
|
...(typeof input.cwd === 'string' ? { cwd: input.cwd } : {}),
|
|
678
|
+
...(typeof input.login === 'boolean' ? { login: input.login } : {}),
|
|
670
679
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
671
680
|
...(yieldMs === undefined ? {} : { yieldMs })
|
|
672
681
|
};
|
|
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { MarAgentError } from '../../agent-core/error.js';
|
|
8
|
+
import { prepareLocalShellProcess } from '../../agent-core/host/local-shell.js';
|
|
8
9
|
import { LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION } from './profile.js';
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
const DEFAULT_BUBBLEWRAP_PATH = '/usr/bin/bwrap';
|
|
@@ -63,12 +64,24 @@ export class BubblewrapSandboxExecutor {
|
|
|
63
64
|
const mounts = await this.#mounts();
|
|
64
65
|
const runtimeMounts = await trustedRuntimeMounts(request.runtimeDirectories ?? [], mounts);
|
|
65
66
|
const cwd = await workingDirectory(request, mounts, this.options.profile.workingDirectory);
|
|
66
|
-
|
|
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);
|
|
67
80
|
const launchDirectory = await mkdtemp(join(tmpdir(), 'mar-agent-bwrap-launch-'));
|
|
68
81
|
const argumentPath = join(launchDirectory, 'arguments');
|
|
69
82
|
const seccompPath = join(launchDirectory, 'seccomp.bpf');
|
|
70
83
|
try {
|
|
71
|
-
const arguments_ = this.#arguments(mounts, cwd,
|
|
84
|
+
const arguments_ = this.#arguments(mounts, cwd, shellRequest, runtimeMounts);
|
|
72
85
|
await Promise.all([
|
|
73
86
|
writeFile(argumentPath, `${arguments_.join('\0')}\0`, { mode: 0o600, flag: 'wx' }),
|
|
74
87
|
writeFile(seccompPath, seccompFilter(process.arch), { mode: 0o600, flag: 'wx' })
|