@sema-agent/core 3.0.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.
- package/CHANGELOG.md +2345 -0
- package/dist/agents/cascade.d.ts +2 -1
- package/dist/agents/cascade.js +31 -32
- package/dist/agents/cumulative-stats.d.ts +29 -0
- package/dist/agents/cumulative-stats.js +29 -0
- package/dist/agents/observer.d.ts +4 -0
- package/dist/agents/observer.js +11 -10
- package/dist/agents/repair-loop.js +27 -19
- package/dist/agents/retain-ledger.d.ts +2 -0
- package/dist/agents/retain-ledger.js +6 -5
- package/dist/agents/roster-store.js +3 -5
- package/dist/agents/subagent.js +66 -64
- package/dist/agents/teacher.d.ts +1 -9
- package/dist/agents/teacher.js +21 -24
- package/dist/agents/verify.js +15 -14
- package/dist/brain/anthropic.js +24 -21
- package/dist/brain/openai.js +24 -21
- package/dist/brain/stream-engine.js +5 -2
- package/dist/brain/stream-shared.d.ts +1 -0
- package/dist/brain/stream-shared.js +14 -0
- package/dist/brain/terminal-cause.d.ts +4 -0
- package/dist/brain/terminal-cause.js +12 -0
- package/dist/brain/tool-call-id.d.ts +1 -0
- package/dist/brain/tool-call-id.js +3 -0
- package/dist/brain/tool-call-repair.js +2 -1
- package/dist/core/auto-compaction.js +16 -25
- package/dist/core/background-agent-store.d.ts +22 -22
- package/dist/core/background-agent-store.js +19 -23
- package/dist/core/checkpoint-store.d.ts +6 -13
- package/dist/core/checkpoint-store.js +11 -5
- package/dist/core/file-snapshot-store.d.ts +8 -0
- package/dist/core/file-snapshot-store.js +4 -3
- package/dist/core/hooks.d.ts +3 -1
- package/dist/core/hooks.js +8 -18
- package/dist/core/lsp.js +4 -3
- package/dist/core/mailbox-store.d.ts +7 -10
- package/dist/core/mcp.d.ts +7 -10
- package/dist/core/mcp.js +7 -3
- package/dist/core/memory-engine/engine.d.ts +0 -1
- package/dist/core/memory-engine/engine.js +24 -34
- package/dist/core/memory-engine/file-backend.d.ts +1 -2
- package/dist/core/memory-engine/file-backend.js +25 -36
- package/dist/core/memory-engine/layout.d.ts +8 -0
- package/dist/core/memory-engine/layout.js +60 -4
- package/dist/core/memory-engine/scope-contract.js +9 -3
- package/dist/core/memory-engine/types.d.ts +1 -1
- package/dist/core/runner/active-skill-scope.d.ts +0 -1
- package/dist/core/runner/active-skill-scope.js +1 -15
- package/dist/core/runner/assemble-result.d.ts +2 -7
- package/dist/core/runner/assemble-result.js +3 -4
- package/dist/core/runner/compaction-call-options.d.ts +7 -0
- package/dist/core/runner/compaction-call-options.js +30 -0
- package/dist/core/runner/prepare-task.d.ts +6 -23
- package/dist/core/runner/prepare-task.js +20 -53
- package/dist/core/runner/runtask.d.ts +1 -0
- package/dist/core/runner/runtask.js +65 -156
- package/dist/core/runner/session-rule-policy.js +4 -1
- package/dist/core/runner/teardown-bounded.d.ts +7 -0
- package/dist/core/runner/teardown-bounded.js +36 -0
- package/dist/core/runner/tool-disclosure.d.ts +2 -5
- package/dist/core/runner/turn-attachments.d.ts +5 -15
- package/dist/core/runner/turn-attachments.js +1 -1
- package/dist/core/safe-notify.d.ts +17 -0
- package/dist/core/safe-notify.js +59 -0
- package/dist/core/sensitive-path-policy.js +2 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
- package/dist/core/stub-env.d.ts +2 -6
- package/dist/core/surrogate-safe-slice.d.ts +4 -0
- package/dist/core/surrogate-safe-slice.js +18 -0
- package/dist/core/task-registry-monitor.js +8 -13
- package/dist/core/task-registry-shared.d.ts +22 -0
- package/dist/core/task-registry-shared.js +16 -0
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +40 -57
- package/dist/core/tool-errors.js +5 -4
- package/dist/core/tool-name-aliases.js +2 -1
- package/dist/core/tool-policy.js +10 -5
- package/dist/core/tool-result-store.d.ts +1 -0
- package/dist/core/tool-result-store.js +17 -1
- package/dist/core/trace.d.ts +2 -9
- package/dist/core/types.d.ts +15 -12
- package/dist/core/untrusted-egress.js +7 -5
- package/dist/core/workflow-journal-store.d.ts +9 -20
- package/dist/core/workflow-run-store.d.ts +11 -10
- package/dist/core/workflow-run-store.js +22 -16
- package/dist/engine/compaction/utils.js +4 -2
- package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +13 -33
- package/dist/engine/harness/types.d.ts +29 -67
- package/dist/engine/llm/types.d.ts +1 -1
- package/dist/engine/loop/agent-loop.js +11 -4
- package/dist/engine/loop/types.d.ts +11 -9
- package/dist/engine/session/memory-repo.d.ts +2 -6
- package/dist/engine/session/session.d.ts +5 -30
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/internal/harness-types.d.ts +3 -1
- package/dist/orchestration/run-spec.js +7 -4
- package/dist/orchestration/run-workflow-tool.js +3 -35
- package/dist/orchestration/workflow.d.ts +3 -11
- package/dist/orchestration/workflow.js +164 -204
- package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
- package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
- package/dist/stores/file/background-agent-store.d.ts +3 -12
- package/dist/stores/file/background-agent-store.js +3 -41
- package/dist/stores/file/checkpoint-store.js +6 -6
- package/dist/stores/file/mailbox-store.d.ts +2 -6
- package/dist/stores/file/session-policy-store.js +15 -1
- package/dist/stores/file/session-store.d.ts +2 -6
- package/dist/stores/file/workflow-journal-store.d.ts +3 -11
- package/dist/stores/file/workflow-run-store.d.ts +3 -7
- package/dist/stores/file/workflow-run-store.js +4 -17
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
- package/dist/tools/fs/bash-readonly-classifier.js +60 -25
- package/dist/tools/fs/fs-bash.js +324 -96
- package/dist/tools/fs/fs-pdf.d.ts +2 -12
- package/dist/tools/fs/fs-shared.js +2 -2
- package/dist/tools/fs/notebook.d.ts +2 -5
- package/dist/tools/fs/safety.d.ts +3 -0
- package/dist/tools/fs/safety.js +43 -14
- package/dist/tools/fs/search.js +5 -5
- package/dist/tools/monitor.d.ts +1 -0
- package/dist/tools/monitor.js +7 -5
- package/dist/tools/worktree.d.ts +2 -7
- package/dist/tools/worktree.js +3 -1
- package/package.json +6 -2
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
@@ -157,7 +158,52 @@ function unquotePayload(payload) {
|
|
|
157
158
|
return quoted ? (quoted[2] ?? "") : s;
|
|
158
159
|
}
|
|
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
|
+
}
|
|
160
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
|
+
}
|
|
161
207
|
const segments = command
|
|
162
208
|
.split(/&&|\|\||;|\||\r?\n/)
|
|
163
209
|
.map((s) => s.trim())
|
|
@@ -412,6 +458,53 @@ ${coAuthorLines}
|
|
|
412
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.
|
|
413
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.`;
|
|
414
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
|
+
}
|
|
415
508
|
export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = { current: rootCanonical }, taskOpts = {}) {
|
|
416
509
|
const timeoutCaps = resolveBashTimeoutCaps(taskOpts);
|
|
417
510
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
@@ -479,57 +572,33 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
479
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.`
|
|
480
573
|
: ` Time budget: auto-terminates if still running after ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).`
|
|
481
574
|
: "";
|
|
482
|
-
const lifetimeNote = bgCaps
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (taskOpts.taskRegistry) {
|
|
486
|
-
const sessionScoped = taskOpts.sessionId !== undefined;
|
|
487
|
-
const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
|
|
488
|
-
const scope = ctx.principal ?? taskOpts.taskScope;
|
|
575
|
+
const lifetimeNote = backgroundLifetimeNote(bgCaps);
|
|
576
|
+
const registry = taskOpts.taskRegistry;
|
|
577
|
+
if (registry !== undefined) {
|
|
489
578
|
const onNotify = taskOpts.onTaskNotification;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
description: description ?? command,
|
|
504
|
-
toolUseId: ctx.toolCallId,
|
|
505
|
-
...(owner !== undefined ? { owner } : {}),
|
|
506
|
-
...(scope !== undefined ? { scope } : {}),
|
|
507
|
-
...(sessionScoped ? { sessionScoped: true } : {}),
|
|
508
|
-
...(outputFile !== undefined ? { outputFile } : {}),
|
|
509
|
-
...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
catch (e) {
|
|
513
|
-
const reason = e instanceof Error ? e.message : String(e);
|
|
514
|
-
let kill;
|
|
515
|
-
try {
|
|
516
|
-
kill = await env.killBackground(r.value.shellId);
|
|
517
|
-
}
|
|
518
|
-
catch {
|
|
519
|
-
kill = undefined;
|
|
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.`);
|
|
520
592
|
}
|
|
521
|
-
if (kill !== undefined && (kill.ok || kill.error.code === "not_found")) {
|
|
522
|
-
return errorResult(`Error (Bash): background process was started but could not be registered (${reason}); it has been terminated.`);
|
|
523
|
-
}
|
|
524
|
-
const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
|
|
525
593
|
return {
|
|
526
|
-
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}). ` +
|
|
527
595
|
`The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
|
|
528
596
|
`If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).`,
|
|
529
597
|
details: { type: "bash", registerFailed: true, killFailed: true },
|
|
530
598
|
isError: true,
|
|
531
599
|
};
|
|
532
600
|
}
|
|
601
|
+
const { taskId, outputFile } = adopted;
|
|
533
602
|
const interimNote = outputFile !== undefined
|
|
534
603
|
? ` Output file: ${outputFile} (full output is appended there — Read it any time).`
|
|
535
604
|
: ` Use TaskOutput("${taskId}") to check interim output.`;
|
|
@@ -555,62 +624,40 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
555
624
|
budgetNote +
|
|
556
625
|
lifetimeNote);
|
|
557
626
|
}
|
|
558
|
-
const
|
|
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
|
|
559
631
|
? {
|
|
560
|
-
signal:
|
|
632
|
+
signal: detachHub.signalFor(ctx.toolCallId),
|
|
561
633
|
onDetached: async (shellId, stdoutSoFar, stderrSoFar, cause) => {
|
|
562
|
-
const sessionScoped = taskOpts.sessionId !== undefined;
|
|
563
|
-
const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
|
|
564
|
-
const scope = ctx.principal ?? taskOpts.taskScope;
|
|
565
634
|
const onNotify = taskOpts.onTaskNotification;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
env: env,
|
|
578
|
-
shellId: shellId,
|
|
579
|
-
description: description ?? command,
|
|
580
|
-
toolUseId: ctx.toolCallId,
|
|
581
|
-
...(owner !== undefined ? { owner } : {}),
|
|
582
|
-
...(scope !== undefined ? { scope } : {}),
|
|
583
|
-
...(sessionScoped ? { sessionScoped: true } : {}),
|
|
584
|
-
...(outputFile !== undefined ? { outputFile } : {}),
|
|
585
|
-
...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
catch (e) {
|
|
589
|
-
const reason = e instanceof Error ? e.message : String(e);
|
|
590
|
-
let kill;
|
|
591
|
-
try {
|
|
592
|
-
kill = await env.killBackground(shellId);
|
|
593
|
-
}
|
|
594
|
-
catch {
|
|
595
|
-
kill = undefined;
|
|
596
|
-
}
|
|
597
|
-
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) {
|
|
598
646
|
const outTail = stdoutSoFar.slice(-1_000);
|
|
599
647
|
const errTail = stderrSoFar.slice(-1_000);
|
|
600
648
|
const tails = (outTail ? `\n--- output so far (tail) ---\n${outTail}` : "") +
|
|
601
649
|
(errTail ? `\n--- stderr so far (tail) ---\n${errTail}` : "");
|
|
602
|
-
if (killed) {
|
|
650
|
+
if (adopted.killed) {
|
|
603
651
|
return {
|
|
604
|
-
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}); ` +
|
|
605
653
|
`it has been terminated (an unregistered background process would have no task_id and could not be read or stopped).` +
|
|
606
654
|
tails,
|
|
607
655
|
details: { type: "bash", detached: true, registerFailed: true },
|
|
608
656
|
isError: true,
|
|
609
657
|
};
|
|
610
658
|
}
|
|
611
|
-
const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
|
|
612
659
|
return {
|
|
613
|
-
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}). ` +
|
|
614
661
|
`The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
|
|
615
662
|
`If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).` +
|
|
616
663
|
tails,
|
|
@@ -618,8 +665,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
618
665
|
isError: true,
|
|
619
666
|
};
|
|
620
667
|
}
|
|
668
|
+
const { taskId, outputFile } = adopted;
|
|
621
669
|
const tail = stdoutSoFar.slice(-1_000);
|
|
622
|
-
const adoptCaps =
|
|
670
|
+
const adoptCaps = detachEnv.backgroundCapabilities;
|
|
623
671
|
const adoptBudgetNote = typeof adoptCaps.defaultBgTimeoutSec === "number" && typeof adoptCaps.maxBgTimeoutSec === "number"
|
|
624
672
|
? ` Time budget: auto-terminates if still running after ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
|
|
625
673
|
: "";
|
|
@@ -638,6 +686,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
638
686
|
? `You will be notified when it completes — do not poll.`
|
|
639
687
|
: `Poll TaskOutput until its status is no longer "running".`) +
|
|
640
688
|
adoptBudgetNote +
|
|
689
|
+
backgroundLifetimeNote(adoptCaps) +
|
|
641
690
|
(tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
|
|
642
691
|
details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}), ...(cause === "timeout" ? { autoBackgrounded: true } : {}) },
|
|
643
692
|
};
|
|
@@ -664,6 +713,92 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
664
713
|
},
|
|
665
714
|
});
|
|
666
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
|
+
}
|
|
667
802
|
function createCanonicalReadBoundary(env, readRoots) {
|
|
668
803
|
const lexicalOnly = isRemoteExecutionEnv(env);
|
|
669
804
|
let rootsOnce;
|
|
@@ -690,20 +825,108 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
690
825
|
}
|
|
691
826
|
return undefined;
|
|
692
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
|
+
};
|
|
693
907
|
return {
|
|
694
908
|
escapesAfterResolution: async (paths, signal) => {
|
|
695
|
-
if (lexicalOnly || paths.length === 0)
|
|
696
|
-
return [];
|
|
909
|
+
if (lexicalOnly || (paths.literal.length === 0 && paths.patterns.length === 0))
|
|
910
|
+
return { escaping: [] };
|
|
697
911
|
const roots = await canonicalRoots(signal);
|
|
698
912
|
const escaping = [];
|
|
699
|
-
|
|
913
|
+
const judge = async (p) => {
|
|
700
914
|
const real = await resolve(p, signal);
|
|
701
915
|
if (real === undefined)
|
|
702
|
-
|
|
916
|
+
return;
|
|
703
917
|
if (!withinAnyRoot(roots, real) && !escaping.includes(real))
|
|
704
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);
|
|
705
928
|
}
|
|
706
|
-
return escaping;
|
|
929
|
+
return { escaping };
|
|
707
930
|
},
|
|
708
931
|
allResolveInside: async (paths, signal) => {
|
|
709
932
|
if (lexicalOnly || paths.length === 0)
|
|
@@ -744,7 +967,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
|
|
|
744
967
|
"operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
|
|
745
968
|
"allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
|
|
746
969
|
"refused, as is one that cannot be resolved statically (e.g. a `~` path). " +
|
|
747
|
-
(isRemoteExecutionEnv(env) ? "" : "A path that leads outside via a symlink counts as outside. ") +
|
|
970
|
+
(isRemoteExecutionEnv(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to. ") +
|
|
748
971
|
"Still subject to the deployment's approval policy.",
|
|
749
972
|
parameters: Type.Object({
|
|
750
973
|
command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
|
|
@@ -763,11 +986,15 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
|
|
|
763
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 ?? [] });
|
|
764
987
|
}
|
|
765
988
|
}
|
|
766
|
-
const
|
|
767
|
-
if (
|
|
768
|
-
|
|
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(", ");
|
|
769
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. ` +
|
|
770
|
-
"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: escaping });
|
|
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 });
|
|
771
998
|
}
|
|
772
999
|
return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
|
|
773
1000
|
},
|
|
@@ -803,8 +1030,9 @@ export function createEnvTaskOutputTool(env) {
|
|
|
803
1030
|
return errorResult(`Error (TaskOutput): ${r.error.message}`);
|
|
804
1031
|
const shape = (text) => clipShellOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text);
|
|
805
1032
|
const status = r.value.status === "exited" ? `exited(code ${r.value.exitCode})` : r.value.status;
|
|
806
|
-
const
|
|
807
|
-
|
|
1033
|
+
const dropAcct = {};
|
|
1034
|
+
accountDroppedBytes(dropAcct, r.value);
|
|
1035
|
+
const droppedNote = spoolDropNote(dropAcct);
|
|
808
1036
|
const body = `status: ${status}\n${droppedNote}--- new stdout ---\n${shape(r.value.stdout) || "(none)"}\n` +
|
|
809
1037
|
`--- new stderr ---\n${shape(r.value.stderr) || "(none)"}`;
|
|
810
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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ImageContent } from "../../internal/llm.js";
|
|
1
2
|
export declare function isNotebookPath(p: string): boolean;
|
|
2
3
|
export declare const NOTEBOOK_CELL_OUTPUT_POINTER_CHARS = 10000;
|
|
3
4
|
export declare const NOTEBOOK_OUTPUT_TEXT_CLIP = 10000;
|
|
@@ -31,11 +32,7 @@ export interface NotebookRender {
|
|
|
31
32
|
blocks: Array<{
|
|
32
33
|
type: "text";
|
|
33
34
|
text: string;
|
|
34
|
-
} |
|
|
35
|
-
type: "image";
|
|
36
|
-
data: string;
|
|
37
|
-
mimeType: string;
|
|
38
|
-
}>;
|
|
35
|
+
} | ImageContent>;
|
|
39
36
|
textChars: number;
|
|
40
37
|
imageChars: number;
|
|
41
38
|
}
|
|
@@ -22,8 +22,11 @@ export interface FsViolation {
|
|
|
22
22
|
roots?: readonly string[];
|
|
23
23
|
message: string;
|
|
24
24
|
}
|
|
25
|
+
export declare function isAbsolutePathForm(p: string): boolean;
|
|
26
|
+
export declare function isWinFormPath(p: string): boolean;
|
|
25
27
|
export declare function isBlockedDevicePath(key: string): boolean;
|
|
26
28
|
export declare function normalizeAbsPathLexically(p: string): string;
|
|
29
|
+
export declare function foldRedundantPathSegments(p: string): string;
|
|
27
30
|
export declare function hasBinaryExtension(path: string): boolean;
|
|
28
31
|
export declare function imageMimeForRead(path: string): string | undefined;
|
|
29
32
|
export declare function imageMagicMatches(bytes: Uint8Array, mimeType: string): boolean;
|