@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.
@@ -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. Use before updating when the revision may have changed. Do not read an empty or new Todo merely to decide whether straightforward bounded work needs one.',
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 using optimistic expectedRevision. 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.',
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: ['expectedRevision', 'items'],
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) => !['expectedRevision', 'items'].includes(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 { expectedRevision: input.expectedRevision, items };
59
+ return { items };
63
60
  }
64
61
  function invalid() {
65
62
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Todo input is invalid.');
@@ -12,7 +12,6 @@ export declare class TodoStore {
12
12
  }[];
13
13
  };
14
14
  write(input: {
15
- expectedRevision: number;
16
15
  items: readonly TodoItem[];
17
16
  }): {
18
17
  revision: number;
@@ -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
  }
@@ -9,6 +9,7 @@ export type ExecInput = {
9
9
  action?: 'start';
10
10
  command: string;
11
11
  cwd?: string;
12
+ login?: boolean;
12
13
  timeoutMs?: number;
13
14
  yieldMs?: number;
14
15
  } | {
@@ -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
- const command = sandboxCommand(request, mounts);
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, request, runtimeMounts);
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' })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.163",
3
+ "version": "0.9.164",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",