@myagentroam/agent 0.9.162 → 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
+ }
@@ -1,2 +1,2 @@
1
- export declare const subagentPrompt = "# Subagents\nUse your own direct tools by default. Delegate only when a bounded task can proceed independently and parallelism has real value; do not delegate ordinary local search, a known-file read, a single verification command, a small edit, or sequential debugging. Give each child a self-contained objective, relevant paths and constraints, expected evidence, and a clear read/write boundary. Subagents cannot recurse and share the real filesystem; do not assign conflicting writes or assume isolation. Track background agent IDs, wait or cancel them, inspect their actual workspace effects, and verify every claim before incorporating it. Continue meaningful non-overlapping work after starting a background child. Wait for completion only when the next critical-path action is blocked on the final result, and then prefer one long wait measured in minutes rather than repeated short waits. Wait for a public message only when a fresh progress update would change the next decision; do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. The parent retains responsibility for the final result.";
1
+ export declare const subagentPrompt = "# Subagents\nCall agent_start, agent_wait, agent_message and agent_cancel directly, never inside code_exec; they are absent from the cell tools namespace. Use your own direct tools by default. Delegate only when a bounded task can proceed independently and parallelism has real value; do not delegate ordinary local search, a known-file read, a single verification command, a small edit, or sequential debugging. Give each child a self-contained objective, relevant paths and constraints, expected evidence, and a clear read/write boundary. Subagents cannot recurse and share the real filesystem; do not assign conflicting writes or assume isolation. Track background agent IDs, wait or cancel them, inspect their actual workspace effects, and verify every claim before incorporating it. Continue meaningful non-overlapping work after starting a background child. Wait for completion only when the next critical-path action is blocked on the final result, and then prefer one long wait measured in minutes rather than repeated short waits. Wait for a public message only when a fresh progress update would change the next decision; do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. The parent retains responsibility for the final result.";
2
2
  export declare const childSubagentInstruction = "You are a non-recursive subagent. Work only on the delegated objective and within its stated read/write scope. Do not broaden the parent task, ask the user, start another subagent, commit, or claim parent-level completion. Inspect relevant evidence, make only authorized changes, run proportional verification, and return a concise factual handoff containing findings, exact files changed, commands/tests actually run, failures or uncertainty, and anything the parent must verify.";
@@ -1,2 +1,2 @@
1
- export const subagentPrompt = '# Subagents\nUse your own direct tools by default. Delegate only when a bounded task can proceed independently and parallelism has real value; do not delegate ordinary local search, a known-file read, a single verification command, a small edit, or sequential debugging. Give each child a self-contained objective, relevant paths and constraints, expected evidence, and a clear read/write boundary. Subagents cannot recurse and share the real filesystem; do not assign conflicting writes or assume isolation. Track background agent IDs, wait or cancel them, inspect their actual workspace effects, and verify every claim before incorporating it. Continue meaningful non-overlapping work after starting a background child. Wait for completion only when the next critical-path action is blocked on the final result, and then prefer one long wait measured in minutes rather than repeated short waits. Wait for a public message only when a fresh progress update would change the next decision; do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. The parent retains responsibility for the final result.';
1
+ export const subagentPrompt = '# Subagents\nCall agent_start, agent_wait, agent_message and agent_cancel directly, never inside code_exec; they are absent from the cell tools namespace. Use your own direct tools by default. Delegate only when a bounded task can proceed independently and parallelism has real value; do not delegate ordinary local search, a known-file read, a single verification command, a small edit, or sequential debugging. Give each child a self-contained objective, relevant paths and constraints, expected evidence, and a clear read/write boundary. Subagents cannot recurse and share the real filesystem; do not assign conflicting writes or assume isolation. Track background agent IDs, wait or cancel them, inspect their actual workspace effects, and verify every claim before incorporating it. Continue meaningful non-overlapping work after starting a background child. Wait for completion only when the next critical-path action is blocked on the final result, and then prefer one long wait measured in minutes rather than repeated short waits. Wait for a public message only when a fresh progress update would change the next decision; do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. The parent retains responsibility for the final result.';
2
2
  export const childSubagentInstruction = 'You are a non-recursive subagent. Work only on the delegated objective and within its stated read/write scope. Do not broaden the parent task, ask the user, start another subagent, commit, or claim parent-level completion. Inspect relevant evidence, make only authorized changes, run proportional verification, and return a concise factual handoff containing findings, exact files changed, commands/tests actually run, failures or uncertainty, and anything the parent must verify.';
@@ -50,7 +50,7 @@ export class RolloutBudget {
50
50
  }
51
51
  assertAvailable() {
52
52
  if (this.stage === 'exhausted')
53
- throw new MarAgentError('MAR_AGENT_ROLLOUT_BUDGET_EXCEEDED', 'The shared execution token budget has been exhausted.');
53
+ throw new MarAgentError('MAR_AGENT_ROLLOUT_BUDGET_EXCEEDED', 'The execution token budget has been exhausted.');
54
54
  }
55
55
  }
56
56
  export function resolveRolloutBudgetOptions(options) {
@@ -9,7 +9,7 @@ import type { CoreAgentsBackend } from '../extensions/agents.js';
9
9
  import type { HostDescription, MarAgentHost } from '../host/contracts.js';
10
10
  import { type MarAgentReasoningEffort } from '../model/configuration.js';
11
11
  import type { ModelAdapter, ModelMessage } from '../model/contracts.js';
12
- import { RolloutBudget, resolveRolloutBudgetOptions } from '../runtime/rollout-budget.js';
12
+ import { resolveRolloutBudgetOptions } from '../runtime/rollout-budget.js';
13
13
  import { JsonlSessionStore } from '../session/jsonl-store.js';
14
14
  import { SubagentSessionController, type SubagentMailboxMessage, type SubagentMessageDelivery, type SubagentTurnHandle } from '../subagent/session-controller.js';
15
15
  import { createSessionToolScheduler } from '../tools/scheduler.js';
@@ -50,7 +50,6 @@ interface InternalMarAgentSession extends MarAgentSession {
50
50
  readonly initialMailbox: readonly SubagentMailboxMessage[];
51
51
  readonly turnId: string;
52
52
  readonly delivery?: SubagentMessageDelivery;
53
- readonly sharedRolloutBudget?: RolloutBudget;
54
53
  readonly mcpExecutionScopeId?: string;
55
54
  readonly origin?: CoreOperationContext;
56
55
  }): SubagentTurnHandle;
@@ -328,9 +328,9 @@ export function createSessionOpener(services) {
328
328
  let finalAnswer = '';
329
329
  let executionLoop;
330
330
  let manualCompaction;
331
- const rolloutBudget = internal.sharedRolloutBudget ??
332
- (rolloutBudgetOptions ? new RolloutBudget(rolloutBudgetOptions) : undefined);
333
- const ownsRolloutBudget = rolloutBudget !== undefined && internal.sharedRolloutBudget === undefined;
331
+ const rolloutBudget = sessionSource.type === 'user' && rolloutBudgetOptions
332
+ ? new RolloutBudget(rolloutBudgetOptions)
333
+ : undefined;
334
334
  let releaseSession;
335
335
  let executionToolSources;
336
336
  let selectedTurn;
@@ -583,8 +583,7 @@ export function createSessionOpener(services) {
583
583
  turnId: turn.turnId,
584
584
  ...(turn.origin === undefined ? {} : { origin: turn.origin }),
585
585
  mcpExecutionScopeId: executionId,
586
- ...(turn.delivery ? { delivery: turn.delivery } : {}),
587
- ...(rolloutBudget === undefined ? {} : { sharedRolloutBudget: rolloutBudget })
586
+ ...(turn.delivery ? { delivery: turn.delivery } : {})
588
587
  });
589
588
  }
590
589
  });
@@ -1466,8 +1465,7 @@ export function createSessionOpener(services) {
1466
1465
  if (sessionRuntime.toolSources === executionToolSources)
1467
1466
  delete sessionRuntime.toolSources;
1468
1467
  internal.signal?.removeEventListener('abort', abortSource);
1469
- if (ownsRolloutBudget)
1470
- rolloutBudget.discard();
1468
+ rolloutBudget?.discard();
1471
1469
  await manualCompaction?.dispose();
1472
1470
  await selectedTurn?.close().catch(() => undefined);
1473
1471
  try {
@@ -1778,10 +1776,7 @@ export function createSessionOpener(services) {
1778
1776
  ...(turn.mcpExecutionScopeId === undefined
1779
1777
  ? {}
1780
1778
  : { mcpExecutionScopeId: turn.mcpExecutionScopeId }),
1781
- ...(turn.delivery ? { delivery: turn.delivery } : {}),
1782
- ...(turn.sharedRolloutBudget === undefined
1783
- ? {}
1784
- : { sharedRolloutBudget: turn.sharedRolloutBudget })
1779
+ ...(turn.delivery ? { delivery: turn.delivery } : {})
1785
1780
  })
1786
1781
  };
1787
1782
  };
@@ -1,9 +1,9 @@
1
1
  import { MarAgentError } from '../../error.js';
2
2
  export const agentCancelToolDefinition = {
3
3
  name: 'agent_cancel',
4
- allowedCallers: ['direct', 'programmatic'],
4
+ allowedCallers: ['direct'],
5
5
  parallelSafety: 'serial',
6
- description: 'Idempotently cancel a child agent using the agentId returned by agent_start. For an exec processId, use exec with action:"cancel" instead. Waits for a running task to reach cancelled and returns its agentId and description.',
6
+ description: 'Idempotently cancel a child agent using the agentId returned by agent_start. For an exec processId, use exec_wait with action:"cancel" instead (tools.exec_wait in Code Mode). Waits for a running task to reach cancelled and returns its agentId and description.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: { agentId: { type: 'string' } },
@@ -2,7 +2,7 @@ import { MarAgentError } from '../../error.js';
2
2
  import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
3
3
  export const agentMessageToolDefinition = {
4
4
  name: 'agent_message',
5
- allowedCallers: ['direct', 'programmatic'],
5
+ allowedCallers: ['direct'],
6
6
  parallelSafety: 'serial',
7
7
  description: 'Send a real user message to an existing child agent. Default delivery=append inserts guidance into a running child turn at the next safe boundary after the current model response or complete tool batch. Explicit delivery=replace immediately requests cancellation, waits for the old turn to close, then starts a new turn in the same Session. When idle, either mode starts a new turn using persisted history. Returns messageId, turnId, deliveryStatus, current status, and model metadata without repeating the child’s previous latestMessage, latestActivity, summary, or verification evidence. accepted means queued in memory; applied means persisted in model context, not that the guidance has been executed. Use agent_wait for subsequent progress or completion. Does not wait for the new task to complete or roll back existing side effects.',
8
8
  inputSchema: {
@@ -3,7 +3,7 @@ import { marAgentReasoningEffortSchema } from '../../model/configuration.js';
3
3
  import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
4
4
  export const agentStartToolDefinition = {
5
5
  name: 'agent_start',
6
- allowedCallers: ['direct', 'programmatic'],
6
+ allowedCallers: ['direct'],
7
7
  parallelSafety: 'serial',
8
8
  description: 'Start one bounded, non-recursive child agent with self-contained context. Use when an independent investigation would consume substantial main-context space or when safe parallel work has real value; keep small or tightly coupled work in the main context. All agents share the filesystem, so avoid conflicting writes. The optional reasoningEffort overrides the parent effort and must be supported by the selected model; omit it to inherit. Returns agentId for agent_wait, agent_message, or agent_cancel, description, queued/running/completed/failed/cancelled status, bounded summary, changedFiles, verification, and read evidence; background returns immediately while foreground waits for a terminal result.',
9
9
  inputSchema: {
@@ -2,7 +2,7 @@ import { MarAgentError } from '../../error.js';
2
2
  import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
3
3
  export const agentWaitToolDefinition = {
4
4
  name: 'agent_wait',
5
- allowedCallers: ['direct', 'programmatic'],
5
+ allowedCallers: ['direct'],
6
6
  parallelSafety: 'serial',
7
7
  description: 'Read or wait a bounded time for a child agent result. waitFor defaults to "completion"; use "message" only when a fresh public commentary/final message would change the next decision. Omit agentId to wait for whichever live child returns first, matching Codex-style untargeted waiting and avoiding UUID transcription; provide the exact agentId returned by agent_start only when observing a specific child. If no child is live, untargeted waiting reads the most recently created child. For an exec processId, use exec_wait instead (tools.exec_wait in Code Mode). Omitting waitMs waits up to 30 seconds by default; waitMs=0 returns the current snapshot immediately. Positive waits range up to 5 minutes, and positive values below 10 seconds are raised to 10 seconds. When blocked on a live child result, prefer one 300000 ms completion wait rather than repeated short waits. Every result includes waitOutcome as snapshot, message, completion, or timeout plus the real agentId and description. A message, timeout with unread progress, or first terminal completion result includes the newly observed latest public commentary/final message; snapshots and repeated observations omit previously observed message text. latestActivity can still identify a public message or bounded tool started/completed/failed status. A queued or running result is not completion. A message wait consumes only the latest unread public message in the current Session runtime; tool activity does not wake it, and parent cancellation preserves unread progress for a later Execution. Do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. First terminal results include bounded summary, changedFiles, verification, and read evidence with path/line ranges; use that evidence to avoid repeating the full exploration, while independently checking consequential edits and claims.',
8
8
  inputSchema: {
@@ -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
  }
@@ -13,7 +13,17 @@ export function toolCallers(tool) {
13
13
  export function allowsToolCaller(tool, caller) {
14
14
  // Control tools cannot be widened by a Host override or historical definition.
15
15
  if (caller === 'programmatic' &&
16
- ['code_exec', 'code_exec_wait', 'tool_search', 'question', 'skill'].includes(tool.name))
16
+ [
17
+ 'code_exec',
18
+ 'code_exec_wait',
19
+ 'tool_search',
20
+ 'question',
21
+ 'skill',
22
+ 'agent_start',
23
+ 'agent_wait',
24
+ 'agent_message',
25
+ 'agent_cancel'
26
+ ].includes(tool.name))
17
27
  return false;
18
28
  return toolCallers(tool).includes(caller);
19
29
  }
@@ -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.162",
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",