@sema-agent/core 2.13.0 → 4.0.0

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.
Files changed (184) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +6 -1
  3. package/dist/agents/cascade.js +76 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +110 -3
  10. package/dist/agents/retain-ledger.d.ts +20 -2
  11. package/dist/agents/retain-ledger.js +38 -8
  12. package/dist/agents/roster-store.d.ts +4 -0
  13. package/dist/agents/roster-store.js +20 -1
  14. package/dist/agents/subagent.js +66 -64
  15. package/dist/agents/teacher.d.ts +1 -9
  16. package/dist/agents/teacher.js +23 -24
  17. package/dist/agents/team.js +7 -4
  18. package/dist/agents/verify.d.ts +15 -2
  19. package/dist/agents/verify.js +39 -3
  20. package/dist/bin/sema-tb.js +2 -2
  21. package/dist/brain/anthropic.js +24 -21
  22. package/dist/brain/circuit-breaker.d.ts +1 -0
  23. package/dist/brain/circuit-breaker.js +26 -9
  24. package/dist/brain/degrading.js +13 -1
  25. package/dist/brain/failover.js +2 -0
  26. package/dist/brain/openai.js +24 -21
  27. package/dist/brain/repetition.js +10 -1
  28. package/dist/brain/routing.js +5 -1
  29. package/dist/brain/status-sink.js +5 -1
  30. package/dist/brain/stream-engine.js +5 -2
  31. package/dist/brain/stream-shared.d.ts +1 -0
  32. package/dist/brain/stream-shared.js +14 -0
  33. package/dist/brain/terminal-cause.d.ts +4 -0
  34. package/dist/brain/terminal-cause.js +12 -0
  35. package/dist/brain/tool-call-id.d.ts +1 -0
  36. package/dist/brain/tool-call-id.js +3 -0
  37. package/dist/brain/tool-call-repair.js +34 -15
  38. package/dist/core/auto-compaction.js +33 -32
  39. package/dist/core/background-agent-store.d.ts +22 -22
  40. package/dist/core/background-agent-store.js +19 -23
  41. package/dist/core/checkpoint-store.d.ts +7 -13
  42. package/dist/core/checkpoint-store.js +39 -15
  43. package/dist/core/consolidate-scope.js +4 -3
  44. package/dist/core/context-edit.js +3 -0
  45. package/dist/core/context-guard.js +3 -0
  46. package/dist/core/file-snapshot-store.d.ts +8 -0
  47. package/dist/core/file-snapshot-store.js +4 -3
  48. package/dist/core/git-worktree-env.d.ts +6 -1
  49. package/dist/core/git-worktree-env.js +12 -1
  50. package/dist/core/hooks.d.ts +4 -1
  51. package/dist/core/hooks.js +35 -7
  52. package/dist/core/lsp-diagnostics.js +13 -7
  53. package/dist/core/lsp-protocol.d.ts +1 -1
  54. package/dist/core/lsp-protocol.js +20 -8
  55. package/dist/core/lsp-session.d.ts +12 -2
  56. package/dist/core/lsp-session.js +114 -49
  57. package/dist/core/lsp.d.ts +13 -1
  58. package/dist/core/lsp.js +65 -9
  59. package/dist/core/mailbox-store.d.ts +7 -10
  60. package/dist/core/mcp.d.ts +7 -10
  61. package/dist/core/mcp.js +7 -3
  62. package/dist/core/memory-engine/data-plane.js +4 -0
  63. package/dist/core/memory-engine/engine.d.ts +2 -2
  64. package/dist/core/memory-engine/engine.js +96 -31
  65. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  66. package/dist/core/memory-engine/file-backend.js +38 -39
  67. package/dist/core/memory-engine/layout.d.ts +8 -0
  68. package/dist/core/memory-engine/layout.js +71 -4
  69. package/dist/core/memory-engine/scope-contract.js +9 -3
  70. package/dist/core/memory-engine/types.d.ts +1 -1
  71. package/dist/core/memory.js +3 -0
  72. package/dist/core/permission-rules.d.ts +2 -1
  73. package/dist/core/permission-rules.js +24 -3
  74. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  75. package/dist/core/runner/active-skill-scope.js +1 -15
  76. package/dist/core/runner/assemble-result.d.ts +3 -8
  77. package/dist/core/runner/assemble-result.js +3 -5
  78. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  79. package/dist/core/runner/compaction-call-options.js +30 -0
  80. package/dist/core/runner/prepare-task.d.ts +6 -23
  81. package/dist/core/runner/prepare-task.js +31 -53
  82. package/dist/core/runner/runtask.d.ts +1 -0
  83. package/dist/core/runner/runtask.js +86 -178
  84. package/dist/core/runner/session-rule-policy.js +4 -1
  85. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  86. package/dist/core/runner/teardown-bounded.js +36 -0
  87. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  88. package/dist/core/runner/turn-attachments.d.ts +5 -15
  89. package/dist/core/runner/turn-attachments.js +1 -1
  90. package/dist/core/runner/usage-accounting.d.ts +6 -0
  91. package/dist/core/runner/usage-accounting.js +9 -8
  92. package/dist/core/safe-notify.d.ts +17 -0
  93. package/dist/core/safe-notify.js +59 -0
  94. package/dist/core/secret-env.js +5 -1
  95. package/dist/core/sensitive-path-policy.js +11 -4
  96. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  97. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  98. package/dist/core/stub-env.d.ts +2 -6
  99. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  100. package/dist/core/surrogate-safe-slice.js +18 -0
  101. package/dist/core/task-registry-monitor.js +15 -14
  102. package/dist/core/task-registry-shared.d.ts +22 -0
  103. package/dist/core/task-registry-shared.js +16 -0
  104. package/dist/core/task-registry.d.ts +1 -0
  105. package/dist/core/task-registry.js +40 -57
  106. package/dist/core/tool-errors.js +5 -4
  107. package/dist/core/tool-name-aliases.js +2 -1
  108. package/dist/core/tool-policy.js +72 -19
  109. package/dist/core/tool-result-store.d.ts +1 -0
  110. package/dist/core/tool-result-store.js +17 -1
  111. package/dist/core/trace.d.ts +3 -9
  112. package/dist/core/types.d.ts +16 -12
  113. package/dist/core/untrusted-egress.js +7 -5
  114. package/dist/core/workflow-journal-store.d.ts +9 -20
  115. package/dist/core/workflow-run-store.d.ts +11 -10
  116. package/dist/core/workflow-run-store.js +22 -16
  117. package/dist/engine/compaction/compaction.d.ts +1 -0
  118. package/dist/engine/compaction/compaction.js +14 -5
  119. package/dist/engine/compaction/utils.js +4 -2
  120. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  121. package/dist/engine/harness/agent-harness.d.ts +1 -0
  122. package/dist/engine/harness/agent-harness.js +13 -33
  123. package/dist/engine/harness/types.d.ts +29 -67
  124. package/dist/engine/llm/types.d.ts +1 -1
  125. package/dist/engine/loop/agent-loop.js +11 -4
  126. package/dist/engine/loop/types.d.ts +11 -9
  127. package/dist/engine/lsp/frame-decoder.js +6 -3
  128. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  129. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  130. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  131. package/dist/engine/session/memory-repo.d.ts +2 -6
  132. package/dist/engine/session/session.d.ts +5 -30
  133. package/dist/index.d.ts +3 -1
  134. package/dist/index.js +2 -1
  135. package/dist/internal/harness-types.d.ts +3 -1
  136. package/dist/internal/harness.d.ts +1 -1
  137. package/dist/internal/harness.js +1 -1
  138. package/dist/orchestration/run-spec.js +7 -4
  139. package/dist/orchestration/run-workflow-tool.js +3 -35
  140. package/dist/orchestration/workflow-governance.js +43 -9
  141. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  142. package/dist/orchestration/workflow-script-store.js +34 -4
  143. package/dist/orchestration/workflow.d.ts +3 -11
  144. package/dist/orchestration/workflow.js +164 -204
  145. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  146. package/dist/prompt-assembly/artifact-store.js +39 -24
  147. package/dist/prompt-assembly/assemble.js +38 -7
  148. package/dist/prompt-assembly/epoch.js +19 -2
  149. package/dist/prompt-assembly/event-registry.js +3 -3
  150. package/dist/prompt-assembly/explain.js +1 -1
  151. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  152. package/dist/prompt-assembly/tool-catalog.js +16 -1
  153. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  154. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  155. package/dist/stores/file/background-agent-store.d.ts +3 -12
  156. package/dist/stores/file/background-agent-store.js +3 -41
  157. package/dist/stores/file/checkpoint-store.js +6 -6
  158. package/dist/stores/file/fs-atomic.d.ts +3 -0
  159. package/dist/stores/file/fs-atomic.js +53 -8
  160. package/dist/stores/file/index.d.ts +1 -0
  161. package/dist/stores/file/index.js +7 -0
  162. package/dist/stores/file/mailbox-store.d.ts +2 -6
  163. package/dist/stores/file/session-policy-store.js +15 -1
  164. package/dist/stores/file/session-store.d.ts +4 -6
  165. package/dist/stores/file/session-store.js +28 -1
  166. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  167. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  168. package/dist/stores/file/workflow-run-store.js +4 -17
  169. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -0
  170. package/dist/tools/fs/bash-readonly-classifier.js +73 -21
  171. package/dist/tools/fs/fs-bash.d.ts +1 -1
  172. package/dist/tools/fs/fs-bash.js +407 -91
  173. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  174. package/dist/tools/fs/fs-shared.js +2 -2
  175. package/dist/tools/fs/notebook.d.ts +2 -5
  176. package/dist/tools/fs/safety.d.ts +3 -0
  177. package/dist/tools/fs/safety.js +43 -14
  178. package/dist/tools/fs/search.js +5 -5
  179. package/dist/tools/loop-tick.js +1 -1
  180. package/dist/tools/monitor.d.ts +1 -0
  181. package/dist/tools/monitor.js +57 -9
  182. package/dist/tools/worktree.d.ts +4 -7
  183. package/dist/tools/worktree.js +235 -143
  184. package/package.json +9 -2
@@ -1,11 +1,13 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../../core/tools.js";
3
3
  import { defaultTaskRegistry } from "../../core/task-registry.js";
4
+ import { accountDroppedBytes, spoolDropNote } from "../../core/task-registry-shared.js";
4
5
  import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_ALIASES, TASK_STOP_ALIASES, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, ENV_DIRECT_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "../../core/task-tool-shape.js";
5
6
  import { hasBackgroundShell } from "../../core/background-shell.js";
6
7
  import { delimitUntrusted } from "../../core/untrusted-text.js";
7
8
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
8
9
  import { imageMagicMatches, withinAnyRoot } from "./safety.js";
10
+ import { isRemoteExecutionEnv } from "../../core/remote-env.js";
9
11
  import { ghRateLimitHint } from "./gh-rate-limit.js";
10
12
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
11
13
  import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
@@ -137,7 +139,71 @@ function commandBasename(token) {
137
139
  const idx = token.lastIndexOf("/");
138
140
  return idx === -1 ? token : token.slice(idx + 1);
139
141
  }
140
- export function canAutoBackground(command) {
142
+ const COMMAND_STRING_SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash"]);
143
+ function commandStringPayload(tokens) {
144
+ const word = commandBasename(tokens[0] ?? "");
145
+ if (word === "eval")
146
+ return tokens.slice(1).join(" ");
147
+ if (!COMMAND_STRING_SHELLS.has(word))
148
+ return undefined;
149
+ const flagIdx = tokens.findIndex((t, i) => i > 0 && /^-[A-Za-z]*c[A-Za-z]*$/.test(t));
150
+ return flagIdx === -1 ? undefined : tokens.slice(flagIdx + 1).join(" ");
151
+ }
152
+ function unquotePayload(payload) {
153
+ const s = payload.trim();
154
+ const ansiC = /^\$'([\s\S]*)'$/.exec(s);
155
+ if (ansiC)
156
+ return (ansiC[1] ?? "").replace(/\\n/g, "\n").replace(/\\t/g, "\t");
157
+ const quoted = /^(['"])([\s\S]*)\1$/.exec(s);
158
+ return quoted ? (quoted[2] ?? "") : s;
159
+ }
160
+ const AUTO_BACKGROUND_MAX_WRAPPER_DEPTH = 3;
161
+ function splitTopLevelStatements(command) {
162
+ const out = [];
163
+ let cur = "";
164
+ let quote;
165
+ for (let i = 0; i < command.length; i++) {
166
+ const c = command[i];
167
+ if (quote !== undefined) {
168
+ cur += c;
169
+ if (c === quote)
170
+ quote = undefined;
171
+ continue;
172
+ }
173
+ if (c === '"' || c === "'") {
174
+ quote = c;
175
+ cur += c;
176
+ continue;
177
+ }
178
+ if (c === ";" || c === "\n" || c === "|" || (c === "&" && command[i + 1] === "&")) {
179
+ if ((c === "&" || c === "|") && command[i + 1] === c)
180
+ i++;
181
+ out.push(cur);
182
+ cur = "";
183
+ continue;
184
+ }
185
+ if (c === "\r" && command[i + 1] === "\n") {
186
+ out.push(cur);
187
+ cur = "";
188
+ i++;
189
+ continue;
190
+ }
191
+ cur += c;
192
+ }
193
+ out.push(cur);
194
+ return out.map((s) => s.trim()).filter((s) => s.length > 0);
195
+ }
196
+ export function canAutoBackground(command, depth = 0) {
197
+ if (depth < AUTO_BACKGROUND_MAX_WRAPPER_DEPTH) {
198
+ for (const stmt of splitTopLevelStatements(command)) {
199
+ const tokens = stripAutoBackgroundWrapperPrefixes(stmt).split(/\s+/);
200
+ for (let i = 0; i < tokens.length; i++) {
201
+ const stmtPayload = commandStringPayload(tokens.slice(i));
202
+ if (stmtPayload !== undefined && stmtPayload.trim() !== "" && !canAutoBackground(unquotePayload(stmtPayload), depth + 1))
203
+ return false;
204
+ }
205
+ }
206
+ }
141
207
  const segments = command
142
208
  .split(/&&|\|\||;|\||\r?\n/)
143
209
  .map((s) => s.trim())
@@ -149,6 +215,9 @@ export function canAutoBackground(command) {
149
215
  const word = commandBasename(tokens[0] ?? "");
150
216
  if (word === "git")
151
217
  return false;
218
+ const payload = depth < AUTO_BACKGROUND_MAX_WRAPPER_DEPTH ? commandStringPayload(tokens) : undefined;
219
+ if (payload !== undefined && payload.trim() !== "" && !canAutoBackground(unquotePayload(payload), depth + 1))
220
+ return false;
152
221
  if (word === "xargs" && tokens.slice(1).some((t) => commandBasename(t) === "git"))
153
222
  return false;
154
223
  }
@@ -389,6 +458,53 @@ ${coAuthorLines}
389
458
  - The shell is NOT sandboxed by this tool: a command can read or write any reachable path and use the network. Every call is subject to the deployment's approval policy (it may be denied or require confirmation); a denied call should not be retried verbatim.
390
459
  - Treat evidence of a deployment restriction — "Operation not permitted", "Read-only file system", a denied path, or a network failure — as a boundary, not a bug: adjust within the allowed scope or report the limit; don't reach for a destructive or privilege-escalating workaround.`;
391
460
  }
461
+ async function adoptBackgroundShell(req) {
462
+ const { env, shellId, registry, description, toolUseId, ctxTaskId, ctxPrincipal, wiring } = req;
463
+ const sessionScoped = wiring.sessionId !== undefined;
464
+ const owner = sessionScoped ? wiring.sessionId : (ctxTaskId ?? wiring.taskOwner);
465
+ const scope = ctxPrincipal ?? wiring.taskScope;
466
+ const onNotify = wiring.onNotify;
467
+ let outputFile;
468
+ if (onNotify !== undefined) {
469
+ const tf = await env.createTempFile({ prefix: "bash-task-", suffix: ".output" });
470
+ if (tf.ok) {
471
+ const tfCanon = await env.canonicalPath(tf.value);
472
+ outputFile = tfCanon.ok ? tfCanon.value : tf.value;
473
+ }
474
+ }
475
+ try {
476
+ const taskId = registry.registerBackgroundBash({
477
+ env,
478
+ shellId,
479
+ description,
480
+ toolUseId,
481
+ ...(owner !== undefined ? { owner } : {}),
482
+ ...(scope !== undefined ? { scope } : {}),
483
+ ...(sessionScoped ? { sessionScoped: true } : {}),
484
+ ...(outputFile !== undefined ? { outputFile } : {}),
485
+ ...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
486
+ });
487
+ return { ok: true, taskId, outputFile };
488
+ }
489
+ catch (e) {
490
+ const reason = e instanceof Error ? e.message : String(e);
491
+ let kill;
492
+ try {
493
+ kill = await env.killBackground(shellId);
494
+ }
495
+ catch {
496
+ kill = undefined;
497
+ }
498
+ if (kill !== undefined && (kill.ok || kill.error.code === "not_found"))
499
+ return { ok: false, reason, killed: true };
500
+ return { ok: false, reason, killed: false, killMsg: kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw" };
501
+ }
502
+ }
503
+ function backgroundLifetimeNote(caps) {
504
+ return caps.retainBackgroundProcesses === true
505
+ ? ""
506
+ : ` NOTE: background processes do NOT survive the session (reaped at task end). If this is a deliverable service that must stay alive afterwards, host it with a FOREGROUND command instead: \`nohup cmd >log 2>&1 &\` (self-detaching — survives the session).`;
507
+ }
392
508
  export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = { current: rootCanonical }, taskOpts = {}) {
393
509
  const timeoutCaps = resolveBashTimeoutCaps(taskOpts);
394
510
  const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
@@ -456,57 +572,33 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
456
572
  ? ` Time budget: requested ${requestedTimeoutSec}s, capped at ${bgCaps.maxBgTimeoutSec}s (env ceiling: requests above ${bgCaps.maxBgTimeoutSec}s are reduced to it) — auto-terminates if still running after ${appliedTimeoutSec}s.`
457
573
  : ` Time budget: auto-terminates if still running after ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).`
458
574
  : "";
459
- const lifetimeNote = bgCaps.retainBackgroundProcesses === true
460
- ? ""
461
- : ` NOTE: background processes do NOT survive the session (reaped at task end). If this is a deliverable service that must stay alive afterwards, host it with a FOREGROUND command instead: \`nohup cmd >log 2>&1 &\` (self-detaching — survives the session).`;
462
- if (taskOpts.taskRegistry) {
463
- const sessionScoped = taskOpts.sessionId !== undefined;
464
- const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
465
- const scope = ctx.principal ?? taskOpts.taskScope;
575
+ const lifetimeNote = backgroundLifetimeNote(bgCaps);
576
+ const registry = taskOpts.taskRegistry;
577
+ if (registry !== undefined) {
466
578
  const onNotify = taskOpts.onTaskNotification;
467
- let outputFile;
468
- if (onNotify !== undefined) {
469
- const tf = await env.createTempFile({ prefix: "bash-task-", suffix: ".output" });
470
- if (tf.ok) {
471
- const tfCanon = await env.canonicalPath(tf.value);
472
- outputFile = tfCanon.ok ? tfCanon.value : tf.value;
473
- }
474
- }
475
- let taskId;
476
- try {
477
- taskId = taskOpts.taskRegistry.registerBackgroundBash({
478
- env,
479
- shellId: r.value.shellId,
480
- description: description ?? command,
481
- toolUseId: ctx.toolCallId,
482
- ...(owner !== undefined ? { owner } : {}),
483
- ...(scope !== undefined ? { scope } : {}),
484
- ...(sessionScoped ? { sessionScoped: true } : {}),
485
- ...(outputFile !== undefined ? { outputFile } : {}),
486
- ...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
487
- });
488
- }
489
- catch (e) {
490
- const reason = e instanceof Error ? e.message : String(e);
491
- let kill;
492
- try {
493
- kill = await env.killBackground(r.value.shellId);
579
+ const adopted = await adoptBackgroundShell({
580
+ env,
581
+ shellId: r.value.shellId,
582
+ registry,
583
+ description: description ?? command,
584
+ toolUseId: ctx.toolCallId,
585
+ ctxTaskId: ctx.taskId,
586
+ ctxPrincipal: ctx.principal,
587
+ wiring: { sessionId: taskOpts.sessionId, taskOwner: taskOpts.taskOwner, taskScope: taskOpts.taskScope, onNotify },
588
+ });
589
+ if (!adopted.ok) {
590
+ if (adopted.killed) {
591
+ return errorResult(`Error (Bash): background process was started but could not be registered (${adopted.reason}); it has been terminated.`);
494
592
  }
495
- catch {
496
- kill = undefined;
497
- }
498
- if (kill !== undefined && (kill.ok || kill.error.code === "not_found")) {
499
- return errorResult(`Error (Bash): background process was started but could not be registered (${reason}); it has been terminated.`);
500
- }
501
- const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
502
593
  return {
503
- content: `Error (Bash): background process was started but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
594
+ content: `Error (Bash): background process was started but could not be registered (${adopted.reason}), and the cleanup kill failed (${adopted.killMsg}). ` +
504
595
  `The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
505
596
  `If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).`,
506
597
  details: { type: "bash", registerFailed: true, killFailed: true },
507
598
  isError: true,
508
599
  };
509
600
  }
601
+ const { taskId, outputFile } = adopted;
510
602
  const interimNote = outputFile !== undefined
511
603
  ? ` Output file: ${outputFile} (full output is appended there — Read it any time).`
512
604
  : ` Use TaskOutput("${taskId}") to check interim output.`;
@@ -532,62 +624,40 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
532
624
  budgetNote +
533
625
  lifetimeNote);
534
626
  }
535
- const detachChain = taskOpts.detachHub && taskOpts.taskRegistry && hasBackgroundShell(env) && env.backgroundCapabilities.supportsDetach === true
627
+ const detachHub = taskOpts.detachHub;
628
+ const detachRegistry = taskOpts.taskRegistry;
629
+ const detachEnv = hasBackgroundShell(env) ? env : undefined;
630
+ const detachChain = detachHub !== undefined && detachRegistry !== undefined && detachEnv !== undefined && detachEnv.backgroundCapabilities.supportsDetach === true
536
631
  ? {
537
- signal: taskOpts.detachHub.signalFor(ctx.toolCallId),
632
+ signal: detachHub.signalFor(ctx.toolCallId),
538
633
  onDetached: async (shellId, stdoutSoFar, stderrSoFar, cause) => {
539
- const sessionScoped = taskOpts.sessionId !== undefined;
540
- const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
541
- const scope = ctx.principal ?? taskOpts.taskScope;
542
634
  const onNotify = taskOpts.onTaskNotification;
543
- let outputFile;
544
- if (onNotify !== undefined) {
545
- const tf = await env.createTempFile({ prefix: "bash-task-", suffix: ".output" });
546
- if (tf.ok) {
547
- const tfCanon = await env.canonicalPath(tf.value);
548
- outputFile = tfCanon.ok ? tfCanon.value : tf.value;
549
- }
550
- }
551
- let taskId;
552
- try {
553
- taskId = taskOpts.taskRegistry.registerBackgroundBash({
554
- env: env,
555
- shellId: shellId,
556
- description: description ?? command,
557
- toolUseId: ctx.toolCallId,
558
- ...(owner !== undefined ? { owner } : {}),
559
- ...(scope !== undefined ? { scope } : {}),
560
- ...(sessionScoped ? { sessionScoped: true } : {}),
561
- ...(outputFile !== undefined ? { outputFile } : {}),
562
- ...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
563
- });
564
- }
565
- catch (e) {
566
- const reason = e instanceof Error ? e.message : String(e);
567
- let kill;
568
- try {
569
- kill = await env.killBackground(shellId);
570
- }
571
- catch {
572
- kill = undefined;
573
- }
574
- const killed = kill !== undefined && (kill.ok || kill.error.code === "not_found");
635
+ const adopted = await adoptBackgroundShell({
636
+ env: detachEnv,
637
+ shellId: shellId,
638
+ registry: detachRegistry,
639
+ description: description ?? command,
640
+ toolUseId: ctx.toolCallId,
641
+ ctxTaskId: ctx.taskId,
642
+ ctxPrincipal: ctx.principal,
643
+ wiring: { sessionId: taskOpts.sessionId, taskOwner: taskOpts.taskOwner, taskScope: taskOpts.taskScope, onNotify },
644
+ });
645
+ if (!adopted.ok) {
575
646
  const outTail = stdoutSoFar.slice(-1_000);
576
647
  const errTail = stderrSoFar.slice(-1_000);
577
648
  const tails = (outTail ? `\n--- output so far (tail) ---\n${outTail}` : "") +
578
649
  (errTail ? `\n--- stderr so far (tail) ---\n${errTail}` : "");
579
- if (killed) {
650
+ if (adopted.killed) {
580
651
  return {
581
- content: `Error (Bash): command was detached to the background but could not be registered (${reason}); ` +
652
+ content: `Error (Bash): command was detached to the background but could not be registered (${adopted.reason}); ` +
582
653
  `it has been terminated (an unregistered background process would have no task_id and could not be read or stopped).` +
583
654
  tails,
584
655
  details: { type: "bash", detached: true, registerFailed: true },
585
656
  isError: true,
586
657
  };
587
658
  }
588
- const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
589
659
  return {
590
- content: `Error (Bash): command was detached to the background but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
660
+ content: `Error (Bash): command was detached to the background but could not be registered (${adopted.reason}), and the cleanup kill failed (${adopted.killMsg}). ` +
591
661
  `The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
592
662
  `If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).` +
593
663
  tails,
@@ -595,8 +665,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
595
665
  isError: true,
596
666
  };
597
667
  }
668
+ const { taskId, outputFile } = adopted;
598
669
  const tail = stdoutSoFar.slice(-1_000);
599
- const adoptCaps = env.backgroundCapabilities;
670
+ const adoptCaps = detachEnv.backgroundCapabilities;
600
671
  const adoptBudgetNote = typeof adoptCaps.defaultBgTimeoutSec === "number" && typeof adoptCaps.maxBgTimeoutSec === "number"
601
672
  ? ` Time budget: auto-terminates if still running after ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
602
673
  : "";
@@ -615,6 +686,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
615
686
  ? `You will be notified when it completes — do not poll.`
616
687
  : `Poll TaskOutput until its status is no longer "running".`) +
617
688
  adoptBudgetNote +
689
+ backgroundLifetimeNote(adoptCaps) +
618
690
  (tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
619
691
  details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}), ...(cause === "timeout" ? { autoBackgrounded: true } : {}) },
620
692
  };
@@ -641,6 +713,234 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
641
713
  },
642
714
  });
643
715
  }
716
+ const GLOB_METACHAR_RE = /[*?[]/;
717
+ const MAX_GLOB_DIRS = 256;
718
+ const MAX_GLOB_MATCHES = 8192;
719
+ function compileGlobSegment(pattern) {
720
+ if (pattern.includes("\\"))
721
+ return undefined;
722
+ let src = "";
723
+ for (let i = 0; i < pattern.length; i++) {
724
+ const ch = pattern[i];
725
+ if (ch === "*") {
726
+ src += "[^/]*";
727
+ }
728
+ else if (ch === "?") {
729
+ src += "[^/]";
730
+ }
731
+ else if (ch === "[") {
732
+ const cls = parseBracketExpression(pattern, i);
733
+ if (cls === undefined)
734
+ src += "\\[";
735
+ else {
736
+ src += cls.source;
737
+ i = cls.end;
738
+ }
739
+ }
740
+ else {
741
+ src += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
742
+ }
743
+ }
744
+ try {
745
+ return new RegExp(`^${src}$`, "s");
746
+ }
747
+ catch {
748
+ return undefined;
749
+ }
750
+ }
751
+ function parseBracketExpression(pattern, start) {
752
+ let i = start + 1;
753
+ let negate = false;
754
+ if (pattern[i] === "!" || pattern[i] === "^") {
755
+ negate = true;
756
+ i++;
757
+ }
758
+ const contentStart = i;
759
+ let body = "";
760
+ let anyChar = false;
761
+ for (; i < pattern.length; i++) {
762
+ const ch = pattern[i];
763
+ if (ch === "]" && i > contentStart) {
764
+ if (body.length === 0 && !anyChar)
765
+ return undefined;
766
+ return { source: anyChar ? "[^/]" : `[${negate ? "^" : ""}${body}]`, end: i };
767
+ }
768
+ if (ch === "[" && (pattern[i + 1] === ":" || pattern[i + 1] === "." || pattern[i + 1] === "=")) {
769
+ const close = pattern.indexOf(`${pattern[i + 1]}]`, i + 2);
770
+ if (close < 0)
771
+ return undefined;
772
+ anyChar = true;
773
+ i = close + 1;
774
+ continue;
775
+ }
776
+ body += ch === "-" ? "-" : ch.replace(/[\]\\^]/g, "\\$&");
777
+ }
778
+ return undefined;
779
+ }
780
+ function globSegmentMatches(re, pattern, name) {
781
+ if (name.startsWith(".") && !pattern.startsWith("."))
782
+ return false;
783
+ return re.test(name);
784
+ }
785
+ function joinCandidate(dir, name) {
786
+ return `${dir.replace(/[\\/]+$/, "")}/${name}`;
787
+ }
788
+ function pathSegmentsFrom(p, from) {
789
+ const segs = [];
790
+ let scan = from;
791
+ while (scan < p.length) {
792
+ while (scan < p.length && (p[scan] === "/" || p[scan] === "\\"))
793
+ scan++;
794
+ const s = scan;
795
+ while (scan < p.length && p[scan] !== "/" && p[scan] !== "\\")
796
+ scan++;
797
+ if (scan > s)
798
+ segs.push({ text: p.slice(s, scan), start: s });
799
+ }
800
+ return segs;
801
+ }
802
+ function createCanonicalReadBoundary(env, readRoots) {
803
+ const lexicalOnly = isRemoteExecutionEnv(env);
804
+ let rootsOnce;
805
+ const canonicalRoots = async (signal) => {
806
+ rootsOnce ??= (async () => {
807
+ const out = [];
808
+ for (const r of readRoots) {
809
+ const c = await env.canonicalPath(r, signal);
810
+ out.push(c.ok ? c.value : r);
811
+ }
812
+ return out;
813
+ })();
814
+ return await rootsOnce;
815
+ };
816
+ const resolve = async (p, signal) => {
817
+ const parts = p.split(/[\\/]/);
818
+ const tail = [];
819
+ while (parts.length > 0) {
820
+ const canon = await env.canonicalPath(parts.join("/") || "/", signal);
821
+ if (canon.ok) {
822
+ return [canon.value.replace(/[\\/]+$/, ""), ...[...tail].reverse()].join("/");
823
+ }
824
+ tail.push(parts.pop());
825
+ }
826
+ return undefined;
827
+ };
828
+ const literalRootHead = (p, roots) => {
829
+ let best;
830
+ for (const r of [...readRoots, ...roots]) {
831
+ if (!r)
832
+ continue;
833
+ const withSep = r.endsWith("/") || r.endsWith("\\") ? r : `${r}/`;
834
+ if (p !== r && !p.startsWith(withSep))
835
+ continue;
836
+ if (best === undefined || r.length > best.length)
837
+ best = r;
838
+ }
839
+ return best;
840
+ };
841
+ const expandGlobCandidate = async (p, roots, signal) => {
842
+ const head = literalRootHead(p, roots);
843
+ let segs = pathSegmentsFrom(p, head?.length ?? 0);
844
+ let first = segs.findIndex((s) => GLOB_METACHAR_RE.test(s.text));
845
+ if (first < 0 && head !== undefined && GLOB_METACHAR_RE.test(head)) {
846
+ segs = pathSegmentsFrom(p, 0);
847
+ first = segs.findIndex((s) => GLOB_METACHAR_RE.test(s.text));
848
+ }
849
+ if (first < 0)
850
+ return undefined;
851
+ let frontier = [p.slice(0, segs[first].start).replace(/[\\/]+$/, "") || "/"];
852
+ let dirsUsed = 0;
853
+ let matches = 0;
854
+ for (let j = first; j < segs.length; j++) {
855
+ const seg = segs[j].text;
856
+ if (!GLOB_METACHAR_RE.test(seg)) {
857
+ frontier = frontier.map((d) => joinCandidate(d, seg));
858
+ continue;
859
+ }
860
+ const re = compileGlobSegment(seg);
861
+ if (re === undefined) {
862
+ return { unverifiable: `the pattern "${seg}" cannot be expanded to verify what it would read` };
863
+ }
864
+ const isLast = j === segs.length - 1;
865
+ const next = [];
866
+ for (const dir of frontier) {
867
+ if (++dirsUsed > MAX_GLOB_DIRS) {
868
+ return { unverifiable: `"${p}" expands over more than ${MAX_GLOB_DIRS} directories, too many to enumerate to verify` };
869
+ }
870
+ const listed = await env.listDir(dir, signal);
871
+ if (!listed.ok) {
872
+ if (listed.error.code === "not_found" || listed.error.code === "not_directory")
873
+ continue;
874
+ return { unverifiable: `"${dir}" cannot be enumerated to verify what "${seg}" would expand to (${listed.error.code})` };
875
+ }
876
+ if (seg.startsWith(".")) {
877
+ for (const dot of [".", ".."])
878
+ if (re.test(dot))
879
+ next.push(joinCandidate(dir, dot));
880
+ }
881
+ let dirInside = false;
882
+ if (isLast) {
883
+ const canonDir = await resolve(dir, signal);
884
+ if (canonDir !== undefined && !withinAnyRoot(roots, canonDir)) {
885
+ next.push(canonDir);
886
+ continue;
887
+ }
888
+ dirInside = canonDir !== undefined;
889
+ }
890
+ for (const entry of listed.value) {
891
+ if (!globSegmentMatches(re, seg, entry.name))
892
+ continue;
893
+ if (++matches > MAX_GLOB_MATCHES) {
894
+ return { unverifiable: `"${p}" expands to more than ${MAX_GLOB_MATCHES} names, too many to enumerate to verify` };
895
+ }
896
+ if (isLast && dirInside && entry.kind !== "symlink")
897
+ continue;
898
+ next.push(joinCandidate(dir, entry.name));
899
+ }
900
+ }
901
+ frontier = next;
902
+ if (frontier.length === 0)
903
+ return { paths: [] };
904
+ }
905
+ return { paths: frontier };
906
+ };
907
+ return {
908
+ escapesAfterResolution: async (paths, signal) => {
909
+ if (lexicalOnly || (paths.literal.length === 0 && paths.patterns.length === 0))
910
+ return { escaping: [] };
911
+ const roots = await canonicalRoots(signal);
912
+ const escaping = [];
913
+ const judge = async (p) => {
914
+ const real = await resolve(p, signal);
915
+ if (real === undefined)
916
+ return;
917
+ if (!withinAnyRoot(roots, real) && !escaping.includes(real))
918
+ escaping.push(real);
919
+ };
920
+ for (const p of paths.literal)
921
+ await judge(p);
922
+ for (const p of paths.patterns) {
923
+ const expanded = await expandGlobCandidate(p, roots, signal);
924
+ if (expanded !== undefined && "unverifiable" in expanded)
925
+ return { escaping, unverifiable: expanded.unverifiable };
926
+ for (const t of expanded !== undefined && expanded.paths.length > 0 ? expanded.paths : [p])
927
+ await judge(t);
928
+ }
929
+ return { escaping };
930
+ },
931
+ allResolveInside: async (paths, signal) => {
932
+ if (lexicalOnly || paths.length === 0)
933
+ return false;
934
+ const roots = await canonicalRoots(signal);
935
+ for (const p of paths) {
936
+ const real = await resolve(p, signal);
937
+ if (real === undefined || !withinAnyRoot(roots, real))
938
+ return false;
939
+ }
940
+ return true;
941
+ },
942
+ };
943
+ }
644
944
  export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opts) {
645
945
  const timeoutCaps = resolveBashTimeoutCaps(opts);
646
946
  const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
@@ -659,14 +959,16 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
659
959
  }
660
960
  return true;
661
961
  };
962
+ const canonicalBoundary = createCanonicalReadBoundary(env, readRoots);
662
963
  return defineTool({
663
964
  name: "Bash",
664
965
  contract: { contractId: "core.bash_readonly@1", implementationRevision: "1" },
665
966
  description: `Run a SINGLE read-only inspection command (e.g. ${sample}, …) and return its output. Shell ` +
666
967
  "operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
667
968
  "allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
668
- "refused, as is one that cannot be resolved statically (e.g. a `~` path). Still subject to the " +
669
- "deployment's approval policy.",
969
+ "refused, as is one that cannot be resolved statically (e.g. a `~` path). " +
970
+ (isRemoteExecutionEnv(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to. ") +
971
+ "Still subject to the deployment's approval policy.",
670
972
  parameters: Type.Object({
671
973
  command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
672
974
  timeout: Type.Optional(Type.Number({ description: `Timeout in milliseconds (default ${timeoutCaps.defaultMs}, max ${timeoutCaps.maxMs}; requests above the max are capped to it).` })),
@@ -678,8 +980,21 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
678
980
  if (reason)
679
981
  return errorResult(`Error (Bash): ${reason}`);
680
982
  const boundary = classifySimpleCommandReadBoundary(command, { roots: readRoots, cwd: rootCanonical });
681
- if (boundary.reason !== undefined && !(await readsOnlyEngineOverflowSpool(boundary))) {
682
- return errorResult(`Error (Bash): ${boundary.reason}. bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.`, { code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
983
+ if (boundary.reason !== undefined) {
984
+ const rescued = boundary.outOfRootRead === true && (await canonicalBoundary.allResolveInside(boundary.outOfRootPaths ?? [], ctx.signal));
985
+ if (!rescued && !(await readsOnlyEngineOverflowSpool(boundary))) {
986
+ return errorResult(`Error (Bash): ${boundary.reason}. bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.`, { code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
987
+ }
988
+ }
989
+ const resolved = await canonicalBoundary.escapesAfterResolution({ literal: boundary.checkedPaths ?? [], patterns: boundary.undecidedPaths ?? [] }, ctx.signal);
990
+ if (resolved.unverifiable !== undefined) {
991
+ return errorResult(`Error (Bash): ${resolved.unverifiable}, so this command cannot be confirmed to read only inside the allowed directories for this session: ${readRoots.join(", ")}. ` +
992
+ "bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.", { code: "readonly_out_of_root", paths: [] });
993
+ }
994
+ if (resolved.escaping.length > 0) {
995
+ const quoted = resolved.escaping.map((p) => `"${p}"`).join(", ");
996
+ return errorResult(`Error (Bash): a path this command reads resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")} — not auto-allowed. ` +
997
+ "bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.", { code: "readonly_out_of_root", paths: resolved.escaping });
683
998
  }
684
999
  return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
685
1000
  },
@@ -715,8 +1030,9 @@ export function createEnvTaskOutputTool(env) {
715
1030
  return errorResult(`Error (TaskOutput): ${r.error.message}`);
716
1031
  const shape = (text) => clipShellOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text);
717
1032
  const status = r.value.status === "exited" ? `exited(code ${r.value.exitCode})` : r.value.status;
718
- const dropped = r.value.bytesDroppedBeforeCursor ?? 0;
719
- const droppedNote = dropped > 0 ? `[!] ${dropped} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n` : "";
1033
+ const dropAcct = {};
1034
+ accountDroppedBytes(dropAcct, r.value);
1035
+ const droppedNote = spoolDropNote(dropAcct);
720
1036
  const body = `status: ${status}\n${droppedNote}--- new stdout ---\n${shape(r.value.stdout) || "(none)"}\n` +
721
1037
  `--- new stderr ---\n${shape(r.value.stderr) || "(none)"}`;
722
1038
  return delimitUntrusted(`TaskOutput ${id}`, body);
@@ -1,21 +1,11 @@
1
1
  import { errorResult } from "../../core/tools.js";
2
+ import type { DocumentContent, ImageContent, TextContent } from "../../internal/llm.js";
2
3
  import type { ExecutionEnv } from "../../internal/harness-types.js";
3
4
  import { type PdfModelCapabilities } from "./pdf.js";
4
5
  import { type ReadImageDownsamplerOption } from "./fs-shared.js";
5
6
  export { pdfModelCapabilitiesOf, type PdfModelCapabilities } from "./pdf.js";
6
7
  type ReadPdfReturn = string | {
7
- content: Array<{
8
- type: "text";
9
- text: string;
10
- } | {
11
- type: "image";
12
- data: string;
13
- mimeType: string;
14
- } | {
15
- type: "document";
16
- data: string;
17
- mimeType: string;
18
- }>;
8
+ content: Array<TextContent | ImageContent | DocumentContent>;
19
9
  details: unknown;
20
10
  };
21
11
  export declare function readPdfFile(env: ExecutionEnv, path: string, key: string, pages: string | undefined, signal: AbortSignal | undefined, downsamplerOpt: ReadImageDownsamplerOption, cwd: string, preRead?: Uint8Array, caps?: PdfModelCapabilities): Promise<ReadPdfReturn>;
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { clipWithFilePointer } from "../../core/tool-errors.js";
3
- import { sha256, similarNameSuggestion, OVERSIZE_READ_ESCAPE_HINT, PARTIAL_VIEW_READ_ESCAPE_HINT, } from "./safety.js";
3
+ import { normalizeAbsPathLexically, sha256, similarNameSuggestion, OVERSIZE_READ_ESCAPE_HINT, PARTIAL_VIEW_READ_ESCAPE_HINT, } from "./safety.js";
4
4
  import { decodeTextBytes, normalizeFileText } from "./encoding.js";
5
5
  import { shellQuote } from "./search.js";
6
6
  import { isNotebookPath } from "./notebook.js";
@@ -117,7 +117,7 @@ export function createShellOverflowSpoolFence(env) {
117
117
  return cut > 0 ? resolved.slice(0, cut) : undefined;
118
118
  };
119
119
  return async (path) => {
120
- const target = toPosixPathKey(path);
120
+ const target = normalizeAbsPathLexically(toPosixPathKey(path));
121
121
  const cut = target.lastIndexOf("/");
122
122
  if (cut <= 0 || !isShellOverflowFileName(target.slice(cut + 1)))
123
123
  return false;