@sema-agent/core 2.6.0 → 2.7.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/core/background-agent-store.d.ts +3 -0
- package/dist/core/runner/prepare-task.js +6 -2
- package/dist/core/task-registry-agent.js +25 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +13 -1
- package/dist/tools/fs/bash-readonly-classifier.js +126 -11
- package/dist/tools/fs/fs-bash.d.ts +2 -1
- package/dist/tools/fs/fs-bash.js +3 -2
- package/package.json +1 -1
|
@@ -37,6 +37,9 @@ export interface BackgroundAgentRecord {
|
|
|
37
37
|
finalOutput?: string;
|
|
38
38
|
finalOutputFull?: string;
|
|
39
39
|
error?: string;
|
|
40
|
+
errorCode?: string;
|
|
41
|
+
errorRetryable?: boolean;
|
|
42
|
+
errorKind?: string;
|
|
40
43
|
resultIsPartial?: boolean;
|
|
41
44
|
recentSteps?: SubagentStep[];
|
|
42
45
|
editedFiles?: SubagentEditedFile[];
|
|
@@ -1225,14 +1225,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1225
1225
|
shellGatedBash = !egressTools.has("Bash") && !irreversibilityTier.has("Bash");
|
|
1226
1226
|
irreversibilityTier.set("Bash", shellGate === "always" ? "always" : "maybe");
|
|
1227
1227
|
irreversibleTools.add("Bash");
|
|
1228
|
+
const shellReadBoundary = () => ({
|
|
1229
|
+
roots: [rootCanonical, ...additionalRootsCanonical],
|
|
1230
|
+
...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
|
|
1231
|
+
});
|
|
1228
1232
|
if (shellGate === "classify")
|
|
1229
|
-
reversibilityProbes.set("Bash", bashReversibilityProbe());
|
|
1233
|
+
reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
|
|
1230
1234
|
if (backgroundTaskToolsActive) {
|
|
1231
1235
|
shellGatedMonitor = !egressTools.has("Monitor") && !irreversibilityTier.has("Monitor");
|
|
1232
1236
|
irreversibilityTier.set("Monitor", shellGate === "always" ? "always" : "maybe");
|
|
1233
1237
|
irreversibleTools.add("Monitor");
|
|
1234
1238
|
if (shellGate === "classify")
|
|
1235
|
-
reversibilityProbes.set("Monitor", bashReversibilityProbe());
|
|
1239
|
+
reversibilityProbes.set("Monitor", bashReversibilityProbe(undefined, shellReadBoundary));
|
|
1236
1240
|
}
|
|
1237
1241
|
}
|
|
1238
1242
|
}
|
|
@@ -723,6 +723,9 @@ export function settleBackgroundAgentLane(core, id, outcome) {
|
|
|
723
723
|
...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
|
|
724
724
|
...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
|
|
725
725
|
...(handle.error !== undefined ? { error: handle.error } : {}),
|
|
726
|
+
...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
|
|
727
|
+
...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
|
|
728
|
+
...(handle.errorKind !== undefined ? { errorKind: handle.errorKind } : {}),
|
|
726
729
|
}, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
|
|
727
730
|
return outcome.status;
|
|
728
731
|
}
|
|
@@ -856,7 +859,22 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
|
|
|
856
859
|
handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
|
|
857
860
|
handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
|
|
858
861
|
handle.updatedAt = Date.now();
|
|
859
|
-
durableAgentWriteLane(handle, { status: "running" }, [
|
|
862
|
+
durableAgentWriteLane(handle, { status: "running" }, [
|
|
863
|
+
"settledAt",
|
|
864
|
+
"stoppedBy",
|
|
865
|
+
"finalOutput",
|
|
866
|
+
"finalOutputFull",
|
|
867
|
+
"error",
|
|
868
|
+
"errorCode",
|
|
869
|
+
"errorRetryable",
|
|
870
|
+
"errorKind",
|
|
871
|
+
"resultIsPartial",
|
|
872
|
+
"completionId",
|
|
873
|
+
"summary",
|
|
874
|
+
"recentSteps",
|
|
875
|
+
"editedFiles",
|
|
876
|
+
"usage",
|
|
877
|
+
]);
|
|
860
878
|
return { ok: true, cycle: handle.reviveCycle };
|
|
861
879
|
}
|
|
862
880
|
export function settleRevivedAgentLane(core, id, cycle, outcome) {
|
|
@@ -1019,8 +1037,11 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
|
|
|
1019
1037
|
},
|
|
1020
1038
|
};
|
|
1021
1039
|
}
|
|
1040
|
+
const kindClause = row.status === "failed" && row.errorKind !== undefined && row.errorRetryable !== undefined
|
|
1041
|
+
? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable})`
|
|
1042
|
+
: "";
|
|
1022
1043
|
const body = `status: ${row.status}
|
|
1023
|
-
${row.error ? `error: ${row.error}
|
|
1044
|
+
${row.error ? `error: ${row.error}${kindClause}
|
|
1024
1045
|
` : ""}${row.finalOutput ? `--- result${row.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
|
|
1025
1046
|
${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}`;
|
|
1026
1047
|
return {
|
|
@@ -1034,6 +1055,8 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
1034
1055
|
...(row.seq !== undefined ? { seq: row.seq } : {}),
|
|
1035
1056
|
...(row.resultIsPartial ? { partial_result: true } : {}),
|
|
1036
1057
|
...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
|
|
1058
|
+
...(row.status === "failed" && row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
|
|
1059
|
+
...(row.status === "failed" && row.errorRetryable !== undefined ? { retryable: row.errorRetryable } : {}),
|
|
1037
1060
|
},
|
|
1038
1061
|
...(row.status === "failed" ? { isError: true } : {}),
|
|
1039
1062
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core
|
|
|
67
67
|
export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
|
|
68
68
|
export type { InvariantKind, FunctionContract, Invariant, InvariantViolation, CheckResult, } from "./core/property-harness.js";
|
|
69
69
|
export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
|
|
70
|
+
export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, type BashReadonlyRootBoundary, type CompoundReadonlyVerdict, } from "./tools/fs/index.js";
|
|
70
71
|
export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
|
|
71
72
|
export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, type ToolResultStore, type ToolResultSlice, } from "./core/tool-result-store.js";
|
|
72
73
|
export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingWalltimeMs, winnerFromOutcome, validatePendingSteer, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type ResourceLedger, type ResourceLimitReason, } from "./core/checkpoint-store.js";
|
package/dist/index.js
CHANGED
|
@@ -58,6 +58,7 @@ export { runExecGate } from "./core/exec-gate.js";
|
|
|
58
58
|
export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core/untrusted-text.js";
|
|
59
59
|
export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
|
|
60
60
|
export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
|
|
61
|
+
export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, } from "./tools/fs/index.js";
|
|
61
62
|
export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
|
|
62
63
|
export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, } from "./core/tool-result-store.js";
|
|
63
64
|
export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingWalltimeMs, winnerFromOutcome, validatePendingSteer, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
|
|
@@ -5,4 +5,16 @@ export declare function parseLeadingCommandName(command: string): {
|
|
|
5
5
|
reject: string;
|
|
6
6
|
};
|
|
7
7
|
export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string>): string | undefined;
|
|
8
|
-
export
|
|
8
|
+
export interface BashReadonlyRootBoundary {
|
|
9
|
+
roots: readonly string[];
|
|
10
|
+
cwd?: string;
|
|
11
|
+
homeDir?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CompoundReadonlyVerdict {
|
|
14
|
+
reason?: string;
|
|
15
|
+
outOfRootRead?: true;
|
|
16
|
+
outOfRootPaths?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function formatOutOfRootReadApprovalOption(directory: string): string;
|
|
19
|
+
export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
|
|
20
|
+
export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isBlockedDevicePath, normalizeAbsPathLexically } from "./safety.js";
|
|
1
|
+
import { isBlockedDevicePath, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
|
|
2
2
|
export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
3
3
|
"ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
|
|
4
4
|
"grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
|
|
@@ -49,12 +49,102 @@ function foldQuoteRemovalToken(tok) {
|
|
|
49
49
|
}
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
|
-
export function
|
|
52
|
+
export function formatOutOfRootReadApprovalOption(directory) {
|
|
53
|
+
const trimmed = directory.replace(/[/\\]+$/, "");
|
|
54
|
+
const cut = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
|
|
55
|
+
const leaf = cut >= 0 ? trimmed.slice(cut + 1) : trimmed;
|
|
56
|
+
return `Yes, allow reading from ${leaf || directory}/ from this project`;
|
|
57
|
+
}
|
|
58
|
+
const NO_PATH_OPERAND_COMMANDS = new Set(["pwd", "echo", "whoami", "uname", "which", "tr", "basename", "dirname"]);
|
|
59
|
+
function isAbsolutePathToken(p) {
|
|
60
|
+
return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
|
61
|
+
}
|
|
62
|
+
function isPathShapedToken(tok) {
|
|
63
|
+
return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
|
|
64
|
+
}
|
|
65
|
+
function normalizeAbsPathLexicalEitherFamily(p) {
|
|
66
|
+
const s = p.replace(/\\/g, "/");
|
|
67
|
+
return /^[A-Za-z]:\//.test(s) ? s.slice(0, 2) + normalizeAbsPathLexically(s.slice(2)) : normalizeAbsPathLexically(s);
|
|
68
|
+
}
|
|
69
|
+
function resolveOperandLexically(base, operand, homeDir) {
|
|
70
|
+
let raw = operand;
|
|
71
|
+
if (raw === "~" || raw.startsWith("~/")) {
|
|
72
|
+
if (homeDir === undefined || !isAbsolutePathToken(homeDir))
|
|
73
|
+
return undefined;
|
|
74
|
+
raw = raw === "~" ? homeDir : `${homeDir.replace(/[/\\]+$/, "")}/${raw.slice(2)}`;
|
|
75
|
+
}
|
|
76
|
+
else if (raw.startsWith("~")) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (isAbsolutePathToken(raw))
|
|
80
|
+
return normalizeAbsPathLexicalEitherFamily(raw);
|
|
81
|
+
if (base === undefined || !isAbsolutePathToken(base))
|
|
82
|
+
return undefined;
|
|
83
|
+
return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
|
|
84
|
+
}
|
|
85
|
+
function collectSegmentBoundaryFindings(toks, boundary) {
|
|
86
|
+
const name = toks[0];
|
|
87
|
+
if (NO_PATH_OPERAND_COMMANDS.has(name))
|
|
88
|
+
return [];
|
|
89
|
+
const args = toks.slice(1);
|
|
90
|
+
const findings = [];
|
|
91
|
+
const candidates = [];
|
|
92
|
+
if (name === "cd") {
|
|
93
|
+
const target = args.find((t) => !t.startsWith("-") || t === "-");
|
|
94
|
+
if (target === undefined) {
|
|
95
|
+
return [{ kind: "unresolvable", reason: '`cd` with no argument targets the home directory, which cannot be checked against the allowed directories — not auto-allowed' }];
|
|
96
|
+
}
|
|
97
|
+
if (target === "-") {
|
|
98
|
+
return [{ kind: "unresolvable", reason: '`cd -` targets the previous working directory, which cannot be resolved statically — not auto-allowed' }];
|
|
99
|
+
}
|
|
100
|
+
candidates.push(target);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const patternSuppliedByFlag = name === "grep" && args.some((t) => t === "-e" || t.startsWith("-e") || t === "-f" || t.startsWith("-f") || t.startsWith("--regexp") || t.startsWith("--file"));
|
|
104
|
+
let sawOperand = false;
|
|
105
|
+
for (let k = 0; k < args.length; k++) {
|
|
106
|
+
const t = args[k];
|
|
107
|
+
if (name === "cut" && (t === "-d" || t === "--delimiter" || t === "--output-delimiter")) {
|
|
108
|
+
k++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (t.startsWith("-") && t !== "-") {
|
|
112
|
+
const eq = t.indexOf("=");
|
|
113
|
+
const value = eq > 0 ? t.slice(eq + 1) : "";
|
|
114
|
+
if (value.length > 0 && isAbsolutePathToken(value))
|
|
115
|
+
candidates.push(value);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (t === "-")
|
|
119
|
+
continue;
|
|
120
|
+
const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
|
|
121
|
+
sawOperand = true;
|
|
122
|
+
if (isGrepPatternSlot)
|
|
123
|
+
continue;
|
|
124
|
+
if (isPathShapedToken(t))
|
|
125
|
+
candidates.push(t);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], candidate, boundary.homeDir);
|
|
130
|
+
if (resolved === undefined) {
|
|
131
|
+
findings.push({
|
|
132
|
+
kind: "unresolvable",
|
|
133
|
+
reason: `"${name}" names the path "${candidate}", which cannot be resolved statically — not auto-allowed`,
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!withinAnyRoot(boundary.roots, resolved))
|
|
138
|
+
findings.push({ kind: "outside", command: name, path: resolved });
|
|
139
|
+
}
|
|
140
|
+
return findings;
|
|
141
|
+
}
|
|
142
|
+
export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
53
143
|
const trimmed = command.trim();
|
|
54
144
|
if (!trimmed)
|
|
55
|
-
return "empty command";
|
|
145
|
+
return { reason: "empty command" };
|
|
56
146
|
if (SHELL_SEGMENT_HARD_REJECT.test(trimmed)) {
|
|
57
|
-
return "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed";
|
|
147
|
+
return { reason: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
|
|
58
148
|
}
|
|
59
149
|
const source = trimmed.endsWith(";") ? trimmed.slice(0, -1) : trimmed;
|
|
60
150
|
const segments = [];
|
|
@@ -70,7 +160,7 @@ export function classifyCompoundReadonly(command, allow) {
|
|
|
70
160
|
i++;
|
|
71
161
|
}
|
|
72
162
|
else
|
|
73
|
-
return "`&` backgrounding is not allowed — a backgrounded process outlives the command";
|
|
163
|
+
return { reason: "`&` backgrounding is not allowed — a backgrounded process outlives the command" };
|
|
74
164
|
}
|
|
75
165
|
else if (c === "|") {
|
|
76
166
|
if (source[i + 1] === "|") {
|
|
@@ -97,7 +187,7 @@ export function classifyCompoundReadonly(command, allow) {
|
|
|
97
187
|
for (const segment of segments) {
|
|
98
188
|
const reason = coarseReadonlyCheck(segment, allow);
|
|
99
189
|
if (reason !== undefined)
|
|
100
|
-
return reason;
|
|
190
|
+
return { reason };
|
|
101
191
|
}
|
|
102
192
|
const HEAD_AUTO_ALLOW_MAX = 1_000_000;
|
|
103
193
|
const headBoundIsSmall = (name, toks) => {
|
|
@@ -128,11 +218,13 @@ export function classifyCompoundReadonly(command, allow) {
|
|
|
128
218
|
return byteBound;
|
|
129
219
|
};
|
|
130
220
|
const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity };
|
|
221
|
+
const foldedSegments = [];
|
|
131
222
|
for (let si = 0; si < segments.length; si++) {
|
|
132
223
|
const toks = segments[si].trim().split(/\s+/).filter((t) => t.length > 0)
|
|
133
224
|
.map(foldQuoteRemovalToken);
|
|
134
225
|
if (toks.length === 0)
|
|
135
226
|
continue;
|
|
227
|
+
foldedSegments.push(toks);
|
|
136
228
|
const name = toks[0];
|
|
137
229
|
const nonOption = toks.slice(1).filter((t) => !t.startsWith("-"));
|
|
138
230
|
if (!(pipeFed[si] ?? false)) {
|
|
@@ -165,21 +257,44 @@ export function classifyCompoundReadonly(command, allow) {
|
|
|
165
257
|
}
|
|
166
258
|
}
|
|
167
259
|
if (floor !== undefined && hasStdinDash) {
|
|
168
|
-
return `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout — not auto-allowed
|
|
260
|
+
return { reason: `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout — not auto-allowed` };
|
|
169
261
|
}
|
|
170
262
|
if (floor !== undefined && nonOption.length < floor) {
|
|
171
|
-
return `"${name}" with no file argument reads stdin and would block until the tool timeout — not auto-allowed
|
|
263
|
+
return { reason: `"${name}" with no file argument reads stdin and would block until the tool timeout — not auto-allowed` };
|
|
172
264
|
}
|
|
173
265
|
}
|
|
174
266
|
if (name === "tail" && toks.slice(1).some((t) => t === "--follow" || t.startsWith("--follow=") || /^[-+][^\s]*[fF]/.test(t))) {
|
|
175
|
-
return "`tail` in follow mode never terminates — not auto-allowed";
|
|
267
|
+
return { reason: "`tail` in follow mode never terminates — not auto-allowed" };
|
|
176
268
|
}
|
|
177
269
|
const GENERATOR_DEVICES = new Set(["/dev/zero", "/dev/random", "/dev/urandom", "/dev/full"]);
|
|
178
270
|
const deviceArgs = toks.slice(1).filter((t) => !t.startsWith("-")).map(normalizeAbsPathLexically).filter(isBlockedDevicePath);
|
|
179
271
|
const rescuedByHead = headBoundIsSmall(name, toks) && deviceArgs.every((d) => GENERATOR_DEVICES.has(d));
|
|
180
272
|
if (!rescuedByHead && deviceArgs.length > 0) {
|
|
181
|
-
return "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";
|
|
273
|
+
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" };
|
|
182
274
|
}
|
|
183
275
|
}
|
|
184
|
-
|
|
276
|
+
if (boundary !== undefined) {
|
|
277
|
+
const outside = [];
|
|
278
|
+
for (const toks of foldedSegments) {
|
|
279
|
+
for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
|
|
280
|
+
if (finding.kind === "unresolvable")
|
|
281
|
+
return { reason: finding.reason };
|
|
282
|
+
if (!outside.some((o) => o.path === finding.path))
|
|
283
|
+
outside.push(finding);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (outside.length > 0) {
|
|
287
|
+
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
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
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {};
|
|
297
|
+
}
|
|
298
|
+
export function classifyCompoundReadonly(command, allow, boundary) {
|
|
299
|
+
return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
|
|
185
300
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
2
|
import { type TaskRegistry } from "../../core/task-registry.js";
|
|
3
3
|
import { type CwdRef } from "./fs-shared.js";
|
|
4
|
-
|
|
4
|
+
import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
5
|
+
export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => {
|
|
5
6
|
reversible: boolean;
|
|
6
7
|
};
|
|
7
8
|
export interface ExecClampOption {
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -9,13 +9,14 @@ import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
|
9
9
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
10
10
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
|
|
11
11
|
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly } from "./bash-readonly-classifier.js";
|
|
12
|
-
export function bashReversibilityProbe(allow) {
|
|
12
|
+
export function bashReversibilityProbe(allow, boundary) {
|
|
13
13
|
const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
14
14
|
return (args) => {
|
|
15
15
|
const command = args?.command;
|
|
16
16
|
if (typeof command !== "string")
|
|
17
17
|
return { reversible: false };
|
|
18
|
-
|
|
18
|
+
const resolved = typeof boundary === "function" ? boundary() : boundary;
|
|
19
|
+
return { reversible: classifyCompoundReadonly(command, allowSet, resolved) === undefined };
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
const EXIT1_INTERPRETATION = {
|