@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.
- package/CHANGELOG.md +2345 -0
- package/dist/agents/cascade.d.ts +6 -1
- package/dist/agents/cascade.js +76 -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.d.ts +2 -0
- package/dist/agents/repair-loop.js +110 -3
- package/dist/agents/retain-ledger.d.ts +20 -2
- package/dist/agents/retain-ledger.js +38 -8
- package/dist/agents/roster-store.d.ts +4 -0
- package/dist/agents/roster-store.js +20 -1
- package/dist/agents/subagent.js +66 -64
- package/dist/agents/teacher.d.ts +1 -9
- package/dist/agents/teacher.js +23 -24
- package/dist/agents/team.js +7 -4
- package/dist/agents/verify.d.ts +15 -2
- package/dist/agents/verify.js +39 -3
- package/dist/bin/sema-tb.js +2 -2
- package/dist/brain/anthropic.js +24 -21
- package/dist/brain/circuit-breaker.d.ts +1 -0
- package/dist/brain/circuit-breaker.js +26 -9
- package/dist/brain/degrading.js +13 -1
- package/dist/brain/failover.js +2 -0
- package/dist/brain/openai.js +24 -21
- package/dist/brain/repetition.js +10 -1
- package/dist/brain/routing.js +5 -1
- package/dist/brain/status-sink.js +5 -1
- 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 +34 -15
- package/dist/core/auto-compaction.js +33 -32
- 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 +7 -13
- package/dist/core/checkpoint-store.js +39 -15
- package/dist/core/consolidate-scope.js +4 -3
- package/dist/core/context-edit.js +3 -0
- package/dist/core/context-guard.js +3 -0
- package/dist/core/file-snapshot-store.d.ts +8 -0
- package/dist/core/file-snapshot-store.js +4 -3
- package/dist/core/git-worktree-env.d.ts +6 -1
- package/dist/core/git-worktree-env.js +12 -1
- package/dist/core/hooks.d.ts +4 -1
- package/dist/core/hooks.js +35 -7
- package/dist/core/lsp-diagnostics.js +13 -7
- package/dist/core/lsp-protocol.d.ts +1 -1
- package/dist/core/lsp-protocol.js +20 -8
- package/dist/core/lsp-session.d.ts +12 -2
- package/dist/core/lsp-session.js +114 -49
- package/dist/core/lsp.d.ts +13 -1
- package/dist/core/lsp.js +65 -9
- 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/data-plane.js +4 -0
- package/dist/core/memory-engine/engine.d.ts +2 -2
- package/dist/core/memory-engine/engine.js +96 -31
- package/dist/core/memory-engine/file-backend.d.ts +2 -2
- package/dist/core/memory-engine/file-backend.js +38 -39
- package/dist/core/memory-engine/layout.d.ts +8 -0
- package/dist/core/memory-engine/layout.js +71 -4
- package/dist/core/memory-engine/scope-contract.js +9 -3
- package/dist/core/memory-engine/types.d.ts +1 -1
- package/dist/core/memory.js +3 -0
- package/dist/core/permission-rules.d.ts +2 -1
- package/dist/core/permission-rules.js +24 -3
- 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 +3 -8
- package/dist/core/runner/assemble-result.js +3 -5
- 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 +31 -53
- package/dist/core/runner/runtask.d.ts +1 -0
- package/dist/core/runner/runtask.js +86 -178
- 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/runner/usage-accounting.d.ts +6 -0
- package/dist/core/runner/usage-accounting.js +9 -8
- package/dist/core/safe-notify.d.ts +17 -0
- package/dist/core/safe-notify.js +59 -0
- package/dist/core/secret-env.js +5 -1
- package/dist/core/sensitive-path-policy.js +11 -4
- package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
- package/dist/core/store-contracts/session-repo-contract.js +40 -0
- 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 +15 -14
- 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 +72 -19
- 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 +3 -9
- package/dist/core/types.d.ts +16 -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/compaction.d.ts +1 -0
- package/dist/engine/compaction/compaction.js +14 -5
- 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/lsp/frame-decoder.js +6 -3
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +103 -26
- package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
- 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/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/run-spec.js +7 -4
- package/dist/orchestration/run-workflow-tool.js +3 -35
- package/dist/orchestration/workflow-governance.js +43 -9
- package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
- package/dist/orchestration/workflow-script-store.js +34 -4
- package/dist/orchestration/workflow.d.ts +3 -11
- package/dist/orchestration/workflow.js +164 -204
- package/dist/prompt-assembly/artifact-store.d.ts +2 -0
- package/dist/prompt-assembly/artifact-store.js +39 -24
- package/dist/prompt-assembly/assemble.js +38 -7
- package/dist/prompt-assembly/epoch.js +19 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/explain.js +1 -1
- package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
- package/dist/prompt-assembly/tool-catalog.js +16 -1
- package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
- package/dist/prompt-assembly/turn-snapshot.js +4 -1
- 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/fs-atomic.d.ts +3 -0
- package/dist/stores/file/fs-atomic.js +53 -8
- package/dist/stores/file/index.d.ts +1 -0
- package/dist/stores/file/index.js +7 -0
- 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 +4 -6
- package/dist/stores/file/session-store.js +28 -1
- 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 +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +73 -21
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +407 -91
- 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/loop-tick.js +1 -1
- package/dist/tools/monitor.d.ts +1 -0
- package/dist/tools/monitor.js +57 -9
- package/dist/tools/worktree.d.ts +4 -7
- package/dist/tools/worktree.js +235 -143
- package/package.json +9 -2
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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 =
|
|
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).
|
|
669
|
-
"
|
|
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
|
|
682
|
-
|
|
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
|
|
719
|
-
|
|
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;
|