@sema-agent/core 2.7.0 → 2.9.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/dist/agents/send-message-tool.d.ts +2 -0
- package/dist/agents/send-message-tool.js +38 -30
- package/dist/agents/subagent.js +15 -3
- package/dist/brain/circuit-breaker.js +18 -8
- package/dist/brain/retry.d.ts +1 -0
- package/dist/brain/retry.js +29 -7
- package/dist/brain/stream-engine.d.ts +1 -0
- package/dist/brain/stream-engine.js +74 -12
- package/dist/core/auto-compaction.js +9 -1
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +20 -0
- package/dist/core/mcp.js +8 -5
- package/dist/core/runner/prepare-task.js +25 -8
- package/dist/core/runner/runtask.js +20 -7
- package/dist/core/runner/tool-disclosure.d.ts +8 -3
- package/dist/core/runner/tool-disclosure.js +22 -8
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +257 -28
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -1
- package/dist/core/store-contracts/file-snapshot-store-contract.js +11 -3
- package/dist/core/task-registry-agent.d.ts +2 -1
- package/dist/core/task-registry-agent.js +47 -54
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +1 -1
- package/dist/core/tools.d.ts +6 -2
- package/dist/core/tools.js +3 -2
- package/dist/core/types.d.ts +5 -1
- package/dist/engine/compaction/compaction.js +71 -20
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +1 -0
- package/dist/orchestration/run-workflow-tool.js +4 -1
- package/dist/orchestration/workflow.d.ts +1 -1
- package/dist/orchestration/workflow.js +2 -2
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
- package/dist/tools/fs/bash-readonly-classifier.js +113 -33
- package/dist/tools/fs/fs-bash.d.ts +2 -1
- package/dist/tools/fs/fs-bash.js +26 -6
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +44 -2
- package/dist/tools/fs/index.js +1 -0
- package/package.json +1 -1
|
@@ -82,13 +82,81 @@ function resolveOperandLexically(base, operand, homeDir) {
|
|
|
82
82
|
return undefined;
|
|
83
83
|
return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
|
|
84
84
|
}
|
|
85
|
-
function
|
|
86
|
-
const
|
|
85
|
+
function tokenizeSegment(segment) {
|
|
86
|
+
const raw = segment
|
|
87
|
+
.trim()
|
|
88
|
+
.split(/\s+/)
|
|
89
|
+
.filter((t) => t.length > 0);
|
|
90
|
+
return { folded: raw.map(foldQuoteRemovalToken), raw };
|
|
91
|
+
}
|
|
92
|
+
function hasUnquotedExpansionMetachar(rawToken) {
|
|
93
|
+
let open;
|
|
94
|
+
for (const ch of rawToken) {
|
|
95
|
+
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
96
|
+
open = ch;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (open === ch) {
|
|
100
|
+
open = undefined;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (open === undefined && (ch === "{" || ch === "}" || ch === "$" || ch === "`"))
|
|
104
|
+
return true;
|
|
105
|
+
if (open === '"' && (ch === "$" || ch === "`"))
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
function isGrepPatternFlagToken(tok) {
|
|
111
|
+
if (tok.startsWith("--"))
|
|
112
|
+
return tok.startsWith("--regexp") || tok.startsWith("--file");
|
|
113
|
+
return /^-[A-Za-z]*[ef]/.test(tok);
|
|
114
|
+
}
|
|
115
|
+
function grepClusterValueOwner(tok) {
|
|
116
|
+
if (!/^-[A-Za-z]/.test(tok) || tok.startsWith("--"))
|
|
117
|
+
return undefined;
|
|
118
|
+
for (const ch of tok.slice(1)) {
|
|
119
|
+
if (ch === "e" || ch === "f")
|
|
120
|
+
return ch;
|
|
121
|
+
if (!/[A-Za-z]/.test(ch))
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
function attachedOptionPayloads(tok) {
|
|
127
|
+
const out = [];
|
|
128
|
+
if (tok.startsWith("--")) {
|
|
129
|
+
const eq = tok.indexOf("=");
|
|
130
|
+
if (eq > 0 && eq + 1 < tok.length)
|
|
131
|
+
out.push(tok.slice(eq + 1));
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
for (let k = 1; k <= tok.length; k++) {
|
|
135
|
+
if (!/^[A-Za-z]*$/.test(tok.slice(1, k)))
|
|
136
|
+
break;
|
|
137
|
+
const payload = tok.slice(k);
|
|
138
|
+
if (payload.length > 0)
|
|
139
|
+
out.push(payload);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
144
|
+
const name = tokens.folded[0];
|
|
87
145
|
if (NO_PATH_OPERAND_COMMANDS.has(name))
|
|
88
146
|
return [];
|
|
89
|
-
const args =
|
|
147
|
+
const args = tokens.folded.slice(1);
|
|
90
148
|
const findings = [];
|
|
91
149
|
const candidates = [];
|
|
150
|
+
for (const rawArg of tokens.raw.slice(1)) {
|
|
151
|
+
if (!hasUnquotedExpansionMetachar(rawArg))
|
|
152
|
+
continue;
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
kind: "unresolvable",
|
|
156
|
+
reason: `"${name}" is given the argument "${rawArg}", which the shell expands (brace/variable/command expansion) before the command runs — the path it would actually read cannot be resolved statically, so it is not auto-allowed`,
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
92
160
|
if (name === "cd") {
|
|
93
161
|
const target = args.find((t) => !t.startsWith("-") || t === "-");
|
|
94
162
|
if (target === undefined) {
|
|
@@ -100,7 +168,7 @@ function collectSegmentBoundaryFindings(toks, boundary) {
|
|
|
100
168
|
candidates.push(target);
|
|
101
169
|
}
|
|
102
170
|
else {
|
|
103
|
-
const patternSuppliedByFlag = name === "grep" && args.some(
|
|
171
|
+
const patternSuppliedByFlag = name === "grep" && args.some(isGrepPatternFlagToken);
|
|
104
172
|
let sawOperand = false;
|
|
105
173
|
for (let k = 0; k < args.length; k++) {
|
|
106
174
|
const t = args[k];
|
|
@@ -108,17 +176,21 @@ function collectSegmentBoundaryFindings(toks, boundary) {
|
|
|
108
176
|
k++;
|
|
109
177
|
continue;
|
|
110
178
|
}
|
|
179
|
+
if (name === "cut" && (/^-d./.test(t) || /^--(output-)?delimiter=/.test(t)))
|
|
180
|
+
continue;
|
|
181
|
+
if (name === "grep" && (grepClusterValueOwner(t) === "e" || /^--regexp=/.test(t)))
|
|
182
|
+
continue;
|
|
111
183
|
if (t.startsWith("-") && t !== "-") {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
184
|
+
for (const payload of attachedOptionPayloads(t)) {
|
|
185
|
+
if (isAbsolutePathToken(payload) || isPathShapedToken(payload))
|
|
186
|
+
candidates.push(payload);
|
|
187
|
+
}
|
|
116
188
|
continue;
|
|
117
189
|
}
|
|
118
|
-
if (t === "-")
|
|
119
|
-
continue;
|
|
120
190
|
const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
|
|
121
191
|
sawOperand = true;
|
|
192
|
+
if (t === "-")
|
|
193
|
+
continue;
|
|
122
194
|
if (isGrepPatternSlot)
|
|
123
195
|
continue;
|
|
124
196
|
if (isPathShapedToken(t))
|
|
@@ -220,17 +292,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
220
292
|
const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity };
|
|
221
293
|
const foldedSegments = [];
|
|
222
294
|
for (let si = 0; si < segments.length; si++) {
|
|
223
|
-
const
|
|
224
|
-
|
|
295
|
+
const segmentTokens = tokenizeSegment(segments[si]);
|
|
296
|
+
const toks = segmentTokens.folded;
|
|
225
297
|
if (toks.length === 0)
|
|
226
298
|
continue;
|
|
227
|
-
foldedSegments.push(
|
|
299
|
+
foldedSegments.push(segmentTokens);
|
|
228
300
|
const name = toks[0];
|
|
229
301
|
const nonOption = toks.slice(1).filter((t) => !t.startsWith("-"));
|
|
230
302
|
if (!(pipeFed[si] ?? false)) {
|
|
231
303
|
const floor = STDIN_FILE_FLOOR[name];
|
|
232
304
|
const restArgs = toks.slice(1);
|
|
233
|
-
const grepPatternSuppliedByFlag = name === "grep" && restArgs.some(
|
|
305
|
+
const grepPatternSuppliedByFlag = name === "grep" && restArgs.some(isGrepPatternFlagToken);
|
|
234
306
|
let sawNonFlagOperand = false;
|
|
235
307
|
let hasStdinDash = false;
|
|
236
308
|
if (name !== "tr") {
|
|
@@ -273,27 +345,35 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
273
345
|
return { reason: "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) — not auto-allowed" };
|
|
274
346
|
}
|
|
275
347
|
}
|
|
276
|
-
if (boundary !== undefined)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
|
|
289
|
-
return {
|
|
290
|
-
reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} — not auto-allowed`,
|
|
291
|
-
outOfRootRead: true,
|
|
292
|
-
outOfRootPaths: outside.map((o) => o.path),
|
|
293
|
-
};
|
|
348
|
+
if (boundary !== undefined)
|
|
349
|
+
return evaluateReadBoundary(foldedSegments, boundary);
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
function evaluateReadBoundary(foldedSegments, boundary) {
|
|
353
|
+
const outside = [];
|
|
354
|
+
for (const toks of foldedSegments) {
|
|
355
|
+
for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
|
|
356
|
+
if (finding.kind === "unresolvable")
|
|
357
|
+
return { reason: finding.reason };
|
|
358
|
+
if (!outside.some((o) => o.path === finding.path))
|
|
359
|
+
outside.push(finding);
|
|
294
360
|
}
|
|
295
361
|
}
|
|
296
|
-
|
|
362
|
+
if (outside.length === 0)
|
|
363
|
+
return {};
|
|
364
|
+
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
365
|
+
const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
|
|
366
|
+
return {
|
|
367
|
+
reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} — not auto-allowed`,
|
|
368
|
+
outOfRootRead: true,
|
|
369
|
+
outOfRootPaths: outside.map((o) => o.path),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
373
|
+
const toks = tokenizeSegment(command);
|
|
374
|
+
if (toks.folded.length === 0)
|
|
375
|
+
return {};
|
|
376
|
+
return evaluateReadBoundary([toks], boundary);
|
|
297
377
|
}
|
|
298
378
|
export function classifyCompoundReadonly(command, allow, boundary) {
|
|
299
379
|
return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
|
|
@@ -31,9 +31,10 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
31
31
|
bashDefaultTimeoutMs?: number;
|
|
32
32
|
bashMaxTimeoutMs?: number;
|
|
33
33
|
}): AgentTool;
|
|
34
|
-
export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption,
|
|
34
|
+
export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption, opts?: {
|
|
35
35
|
bashDefaultTimeoutMs?: number;
|
|
36
36
|
bashMaxTimeoutMs?: number;
|
|
37
|
+
additionalRoots?: readonly string[];
|
|
37
38
|
}): AgentTool;
|
|
38
39
|
export declare function createEnvTaskOutputTool(env: ExecutionEnv): AgentTool;
|
|
39
40
|
export declare function createEnvTaskStopTool(env: ExecutionEnv, registry?: TaskRegistry): AgentTool;
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -7,8 +7,8 @@ import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
|
7
7
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
8
8
|
import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
9
9
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
10
|
-
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
|
|
11
|
-
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly } from "./bash-readonly-classifier.js";
|
|
10
|
+
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
|
|
11
|
+
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
|
|
12
12
|
export function bashReversibilityProbe(allow, boundary) {
|
|
13
13
|
const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
14
14
|
return (args) => {
|
|
@@ -627,16 +627,32 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
627
627
|
},
|
|
628
628
|
});
|
|
629
629
|
}
|
|
630
|
-
export function createBashReadonlyTool(env, rootCanonical, allow, execClamp,
|
|
631
|
-
const timeoutCaps = resolveBashTimeoutCaps(
|
|
630
|
+
export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opts) {
|
|
631
|
+
const timeoutCaps = resolveBashTimeoutCaps(opts);
|
|
632
632
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
633
633
|
const sample = [...allow].slice(0, 6).join(", ");
|
|
634
|
+
const readRoots = [rootCanonical, ...(opts?.additionalRoots ?? [])];
|
|
635
|
+
const overflowSpoolFence = createShellOverflowSpoolFence(env);
|
|
636
|
+
const readsOnlyEngineOverflowSpool = async (verdict) => {
|
|
637
|
+
if (verdict.outOfRootRead !== true)
|
|
638
|
+
return false;
|
|
639
|
+
const paths = verdict.outOfRootPaths ?? [];
|
|
640
|
+
if (paths.length === 0)
|
|
641
|
+
return false;
|
|
642
|
+
for (const p of paths) {
|
|
643
|
+
if (!(await overflowSpoolFence(p)))
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
return true;
|
|
647
|
+
};
|
|
634
648
|
return defineTool({
|
|
635
649
|
name: "Bash",
|
|
636
650
|
contract: { contractId: "core.bash_readonly@1", implementationRevision: "1" },
|
|
637
651
|
description: `Run a SINGLE read-only inspection command (e.g. ${sample}, …) and return its output. Shell ` +
|
|
638
652
|
"operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
|
|
639
|
-
"allowlisted commands run.
|
|
653
|
+
"allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
|
|
654
|
+
"refused, as is one that cannot be resolved statically (e.g. a `~` path). Still subject to the " +
|
|
655
|
+
"deployment's approval policy.",
|
|
640
656
|
parameters: Type.Object({
|
|
641
657
|
command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
|
|
642
658
|
timeout: Type.Optional(Type.Number({ description: `Timeout in milliseconds (default ${timeoutCaps.defaultMs}, max ${timeoutCaps.maxMs}; requests above the max are capped to it).` })),
|
|
@@ -647,7 +663,11 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, tim
|
|
|
647
663
|
const reason = coarseReadonlyCheck(command, allow);
|
|
648
664
|
if (reason)
|
|
649
665
|
return errorResult(`Error (Bash): ${reason}`);
|
|
650
|
-
|
|
666
|
+
const boundary = classifySimpleCommandReadBoundary(command, { roots: readRoots, cwd: rootCanonical });
|
|
667
|
+
if (boundary.reason !== undefined && !(await readsOnlyEngineOverflowSpool(boundary))) {
|
|
668
|
+
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 ?? [] });
|
|
669
|
+
}
|
|
670
|
+
return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
|
|
651
671
|
},
|
|
652
672
|
});
|
|
653
673
|
}
|
|
@@ -47,6 +47,7 @@ export declare const FILE_PATH_PARAMS: {
|
|
|
47
47
|
};
|
|
48
48
|
export declare function clipShellOutput(s: string): string;
|
|
49
49
|
export declare function writeShellOverflowFile(env: ExecutionEnv, stdout: string, stderr: string): Promise<string | undefined>;
|
|
50
|
+
export declare function createShellOverflowSpoolFence(env: ExecutionEnv): (path: string) => Promise<boolean>;
|
|
50
51
|
export declare function shellRecoveryHint(path: string, readOnly: boolean | undefined): string;
|
|
51
52
|
export declare const FILE_STATE_TRAILER = " (file state is current in your context \u2014 no need to Read it back)";
|
|
52
53
|
export declare const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
|
|
@@ -52,7 +52,10 @@ export const BASH_MAX_TIMEOUT_SEC = 600;
|
|
|
52
52
|
export const BASH_DEFAULT_TIMEOUT_MS = BASH_DEFAULT_TIMEOUT_SEC * 1000;
|
|
53
53
|
export const BASH_MAX_TIMEOUT_MS = BASH_MAX_TIMEOUT_SEC * 1000;
|
|
54
54
|
function validTimeoutMs(n) {
|
|
55
|
-
|
|
55
|
+
if (n === undefined || !Number.isFinite(n))
|
|
56
|
+
return undefined;
|
|
57
|
+
const floored = Math.floor(n);
|
|
58
|
+
return floored >= 1 ? floored : undefined;
|
|
56
59
|
}
|
|
57
60
|
export function resolveBashTimeoutCaps(opts) {
|
|
58
61
|
const defaultMs = validTimeoutMs(opts?.bashDefaultTimeoutMs) ??
|
|
@@ -80,7 +83,7 @@ export function clipShellOutput(s) {
|
|
|
80
83
|
return clipWithFilePointer(s, bashMaxOutputChars());
|
|
81
84
|
}
|
|
82
85
|
export async function writeShellOverflowFile(env, stdout, stderr) {
|
|
83
|
-
const tf = await env.createTempFile({ prefix:
|
|
86
|
+
const tf = await env.createTempFile({ prefix: SHELL_OVERFLOW_FILE_PREFIX, suffix: SHELL_OVERFLOW_FILE_SUFFIX });
|
|
84
87
|
if (!tf.ok)
|
|
85
88
|
return undefined;
|
|
86
89
|
const body = stderr.length > 0 ? `${stdout}${stdout.length > 0 && !stdout.endsWith("\n") ? "\n" : ""}--- stderr ---\n${stderr}` : stdout;
|
|
@@ -90,6 +93,45 @@ export async function writeShellOverflowFile(env, stdout, stderr) {
|
|
|
90
93
|
const canon = await env.canonicalPath(tf.value);
|
|
91
94
|
return canon.ok ? canon.value : tf.value;
|
|
92
95
|
}
|
|
96
|
+
const SHELL_OVERFLOW_FILE_PREFIX = "bash-output-";
|
|
97
|
+
const SHELL_OVERFLOW_FILE_SUFFIX = ".log";
|
|
98
|
+
function toPosixPathKey(p) {
|
|
99
|
+
return p.replace(/\\/g, "/").replace(/(.)\/+$/, "$1");
|
|
100
|
+
}
|
|
101
|
+
function isShellOverflowFileName(base) {
|
|
102
|
+
if (!base.startsWith(SHELL_OVERFLOW_FILE_PREFIX) || !base.endsWith(SHELL_OVERFLOW_FILE_SUFFIX))
|
|
103
|
+
return false;
|
|
104
|
+
const middle = base.slice(SHELL_OVERFLOW_FILE_PREFIX.length, base.length - SHELL_OVERFLOW_FILE_SUFFIX.length);
|
|
105
|
+
return /^[0-9A-Za-z][0-9A-Za-z._-]*$/.test(middle);
|
|
106
|
+
}
|
|
107
|
+
export function createShellOverflowSpoolFence(env) {
|
|
108
|
+
let tempArea;
|
|
109
|
+
const learnTempArea = async () => {
|
|
110
|
+
const probe = await env.createTempDir();
|
|
111
|
+
if (!probe.ok)
|
|
112
|
+
return undefined;
|
|
113
|
+
const canon = await env.canonicalPath(probe.value);
|
|
114
|
+
const resolved = toPosixPathKey(canon.ok ? canon.value : probe.value);
|
|
115
|
+
await env.remove(probe.value, { recursive: true, force: true });
|
|
116
|
+
const cut = resolved.lastIndexOf("/");
|
|
117
|
+
return cut > 0 ? resolved.slice(0, cut) : undefined;
|
|
118
|
+
};
|
|
119
|
+
return async (path) => {
|
|
120
|
+
const target = toPosixPathKey(path);
|
|
121
|
+
const cut = target.lastIndexOf("/");
|
|
122
|
+
if (cut <= 0 || !isShellOverflowFileName(target.slice(cut + 1)))
|
|
123
|
+
return false;
|
|
124
|
+
tempArea ??= learnTempArea();
|
|
125
|
+
const area = await tempArea;
|
|
126
|
+
if (area === undefined)
|
|
127
|
+
return false;
|
|
128
|
+
const parent = target.slice(0, cut);
|
|
129
|
+
if (parent === area)
|
|
130
|
+
return true;
|
|
131
|
+
const up = parent.lastIndexOf("/");
|
|
132
|
+
return up > 0 && parent.slice(0, up) === area;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
93
135
|
export function shellRecoveryHint(path, readOnly) {
|
|
94
136
|
const quoted = shellQuote(path);
|
|
95
137
|
const example = readOnly ? `tail -c 50000 ${quoted}` : `sed -n 'START,ENDp' ${quoted}`;
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -38,6 +38,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
38
38
|
? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), opts.execClamp, {
|
|
39
39
|
...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
|
|
40
40
|
...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
|
|
41
|
+
...(additionalRoots !== undefined ? { additionalRoots } : {}),
|
|
41
42
|
})
|
|
42
43
|
: createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
|
|
43
44
|
taskRegistry: opts.taskRegistry,
|