@sema-agent/core 7.23.3 → 7.23.5
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 +70 -0
- package/dist/agents/list-agents-tool.d.ts +5 -1
- package/dist/agents/list-agents-tool.js +14 -2
- package/dist/brain/anthropic.js +3 -2
- package/dist/brain/circuit-breaker.js +3 -3
- package/dist/brain/degrading.js +3 -5
- package/dist/brain/errors.d.ts +35 -18
- package/dist/brain/errors.js +49 -31
- package/dist/brain/failover.js +2 -3
- package/dist/brain/open-responses.js +6 -5
- package/dist/brain/openai.js +6 -5
- package/dist/brain/route-adjudicator.d.ts +26 -4
- package/dist/brain/route-adjudicator.js +26 -0
- package/dist/brain/stream-engine.js +8 -2
- package/dist/core/checkpoint-store.d.ts +4 -6
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/permission-rule-model.js +8 -5
- package/dist/core/runner/prepare-delegation-surface.d.ts +3 -0
- package/dist/core/runner/prepare-delegation-surface.js +1 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -1
- package/dist/core/runner/prepare-hands-readface.js +7 -7
- package/dist/core/runner/prepare-task.js +2 -2
- package/dist/core/runner/run-leg.js +2 -13
- package/dist/core/task-spec.d.ts +2 -1
- package/dist/core/thrown-value.d.ts +30 -0
- package/dist/core/thrown-value.js +23 -1
- package/dist/core/tool-catalog-entries.js +1 -1
- package/dist/core/tool-model-gate.js +3 -15
- package/dist/core/tools.js +7 -7
- package/dist/core/untrusted-text.d.ts +6 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/core/workflow-run-store-contract.js +13 -0
- package/dist/core/workflow-run-store.d.ts +15 -0
- package/dist/core/workflow-run-store.js +1 -0
- package/dist/engine/loop/agent-loop.js +10 -24
- package/dist/tools/fs/bash-program-position.d.ts +23 -0
- package/dist/tools/fs/bash-program-position.js +11 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +64 -42
- package/dist/tools/fs/bash-readonly-classifier.js +33 -10
- package/dist/tools/fs/fs-bash.d.ts +41 -3
- package/dist/tools/fs/fs-bash.js +152 -58
- package/dist/tools/fs/index.d.ts +2 -1
- package/dist/tools/fs/index.js +1 -0
- package/dist/tools/fs/read-deny.d.ts +28 -0
- package/dist/tools/fs/read-deny.js +11 -0
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/safety.js +2 -1
- package/dist/tools/monitor.d.ts +5 -0
- package/dist/tools/monitor.js +7 -0
- package/package.json +1 -1
- package/wire-changes/core@7.23.4.json +58 -0
- package/wire-changes/core@7.23.5.json +133 -0
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -7,14 +7,16 @@ import { RollingTailBuffer } from "../../core/exec-output-tail.js";
|
|
|
7
7
|
import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, 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";
|
|
8
8
|
import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
9
9
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
10
|
+
import { sliceHeadSafe, sliceTailSafe } from "../../core/surrogate-safe-slice.js";
|
|
10
11
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
11
12
|
import { imageMagicMatches, normalizeAbsPathLexically, violationDetails, withinAnyRoot } from "./safety.js";
|
|
12
13
|
import { boundaryIsTheSandbox, canonicalPathIsAuthoritative, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
13
14
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
14
15
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
15
|
-
import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
|
|
16
|
+
import { PROBE_CAUSE_PATH_MAX, PROBE_REASON_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
|
|
16
17
|
import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
|
|
17
|
-
import { NOT_AUTO_ALLOWED,
|
|
18
|
+
import { BOUNDARY_SEAT_READS_DECLINE, CLASSIFY_SEAT_READS_DECLINE, NOT_AUTO_ALLOWED, seatDeclineEvidence, } from "./bash-program-position.js";
|
|
19
|
+
import { readDenySeat, sensitivePathDenyList, SENSITIVE_PATH_DENY_SCOPE } from "./read-deny.js";
|
|
18
20
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
19
21
|
import { thrownText } from "../../core/thrown-value.js";
|
|
20
22
|
const RECURSIVE_CAUSE_MAX_PATHS = 3;
|
|
@@ -34,13 +36,13 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
34
36
|
const backgrounded = isBackgroundShellCall(args);
|
|
35
37
|
const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
|
|
36
38
|
if (backgrounded)
|
|
37
|
-
return { reversible: false, ...boundaryGate(detailed) };
|
|
39
|
+
return { reversible: false, ...boundaryGate(detailed), ...probeAccountOf(command, allowSet, resolved, detailed) };
|
|
38
40
|
if (detailed.reason === undefined) {
|
|
39
41
|
if (boundaryUnread(detailed)) {
|
|
40
|
-
const
|
|
41
|
-
if (cause !== undefined)
|
|
42
|
-
return { reversible: false, ...boundaryGate(detailed),
|
|
43
|
-
return { reversible: false, ...outOfRootGate() };
|
|
42
|
+
const account = probeAccountOf(command, allowSet, resolved, detailed);
|
|
43
|
+
if (account.cause !== undefined)
|
|
44
|
+
return { reversible: false, ...boundaryGate(detailed), ...account };
|
|
45
|
+
return { reversible: false, ...outOfRootGate(), ...account };
|
|
44
46
|
}
|
|
45
47
|
return { reversible: true };
|
|
46
48
|
}
|
|
@@ -50,9 +52,8 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
50
52
|
const seatOffer = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
|
|
51
53
|
const loopScan = refusedCommandScan(command, allowSet, resolved, loop, CLASSIFY_SEAT_DECLINE_EVIDENCE);
|
|
52
54
|
if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop, loopScan))
|
|
53
|
-
return { reversible: false, mandated: true, ...seatOffer(loop) };
|
|
54
|
-
|
|
55
|
-
return { reversible: false, ...boundaryGate(detailed), ...seatOffer(detailed), ...(refusedCause !== undefined ? { cause: refusedCause } : {}) };
|
|
55
|
+
return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, loop), ...seatOffer(loop) };
|
|
56
|
+
return { reversible: false, ...boundaryGate(detailed), ...seatOffer(detailed), ...probeAccountOf(command, allowSet, resolved, detailed) };
|
|
56
57
|
};
|
|
57
58
|
return (args) => {
|
|
58
59
|
const command = shellCommandOf(args);
|
|
@@ -89,8 +90,8 @@ function shellCommandOf(args) {
|
|
|
89
90
|
function isBackgroundShellCall(args) {
|
|
90
91
|
return args?.run_in_background === true;
|
|
91
92
|
}
|
|
92
|
-
export const BOUNDARY_SEAT_DECLINE_EVIDENCE =
|
|
93
|
-
export const CLASSIFY_SEAT_DECLINE_EVIDENCE =
|
|
93
|
+
export const BOUNDARY_SEAT_DECLINE_EVIDENCE = seatDeclineEvidence(BOUNDARY_SEAT_READS_DECLINE);
|
|
94
|
+
export const CLASSIFY_SEAT_DECLINE_EVIDENCE = seatDeclineEvidence(CLASSIFY_SEAT_READS_DECLINE);
|
|
94
95
|
function readBoundaryMandate(command, allowSet, resolved, verdict, declineEvidence) {
|
|
95
96
|
if (resolved === undefined)
|
|
96
97
|
return {};
|
|
@@ -98,7 +99,7 @@ function readBoundaryMandate(command, allowSet, resolved, verdict, declineEviden
|
|
|
98
99
|
if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict, scan))
|
|
99
100
|
return { mandated: true };
|
|
100
101
|
const named = classifyOutOfRootReadGate(command, allowSet, resolved);
|
|
101
|
-
if (named.gated || named.denied
|
|
102
|
+
if (named.gated || named.denied !== undefined)
|
|
102
103
|
return { mandated: true };
|
|
103
104
|
return declinedRung(scan);
|
|
104
105
|
}
|
|
@@ -121,7 +122,7 @@ function unjudgedRead(command, allowSet, resolved, verdict, scan) {
|
|
|
121
122
|
if (resolved === undefined)
|
|
122
123
|
return false;
|
|
123
124
|
const named = classifyOutOfRootReadGate(command, allowSet, resolved);
|
|
124
|
-
return named.gated || named.denied
|
|
125
|
+
return named.gated || named.denied !== undefined;
|
|
125
126
|
}
|
|
126
127
|
function recursiveReadCause(detailed) {
|
|
127
128
|
const recursive = detailed.recursiveReadPaths;
|
|
@@ -135,17 +136,65 @@ function recursiveReadCause(detailed) {
|
|
|
135
136
|
...(others.length > 0 ? { further: operandFamily(others) } : {}),
|
|
136
137
|
};
|
|
137
138
|
}
|
|
139
|
+
function denyHitReason(finding) {
|
|
140
|
+
if (finding === undefined)
|
|
141
|
+
return {};
|
|
142
|
+
const { path, pattern } = finding;
|
|
143
|
+
const composed = (target, pat) => `reading "${target}" matches ${sensitivePathDenyList(pat)}.`;
|
|
144
|
+
const room = PROBE_REASON_MAX - composed("", "").length;
|
|
145
|
+
const patternRoom = Math.min(pattern.length, Math.max(Math.floor(room / 2), room - path.length));
|
|
146
|
+
return { reason: composed(fitMiddle(path, room - patternRoom), fitMiddle(pattern, patternRoom)) };
|
|
147
|
+
}
|
|
148
|
+
export function fitMiddle(s, budget) {
|
|
149
|
+
return s.length <= budget ? s : elideMiddle(s, budget);
|
|
150
|
+
}
|
|
151
|
+
export function elideMiddle(s, budget) {
|
|
152
|
+
if (budget < 12)
|
|
153
|
+
return sliceHeadSafe(s, Math.max(0, budget));
|
|
154
|
+
const keep = budget - 1;
|
|
155
|
+
const head = Math.ceil(keep / 2);
|
|
156
|
+
return `${sliceHeadSafe(s, head)}\u2026${sliceTailSafe(s, keep - head)}`;
|
|
157
|
+
}
|
|
158
|
+
function denyFindingOf(command, allowSet, resolved, verdict) {
|
|
159
|
+
if (verdict.readDenied === true && verdict.readDeniedPath !== undefined && verdict.readDeniedPattern !== undefined) {
|
|
160
|
+
return { path: verdict.readDeniedPath, pattern: verdict.readDeniedPattern };
|
|
161
|
+
}
|
|
162
|
+
return resolved === undefined ? undefined : classifyOutOfRootReadGate(command, allowSet, resolved).denied;
|
|
163
|
+
}
|
|
164
|
+
function recursiveDenyReason(resolved, verdict) {
|
|
165
|
+
const roots = verdict.recursiveReadPaths;
|
|
166
|
+
const entries = resolved?.denyEntries;
|
|
167
|
+
if (roots === undefined || roots.length === 0 || entries === undefined || entries.length === 0)
|
|
168
|
+
return {};
|
|
169
|
+
const composed = (root, rows) => `a recursive read form traverses "${root}", which this walk does not enumerate, past ${sensitivePathDenyList(rows)}.`;
|
|
170
|
+
const root = roots[0] ?? "";
|
|
171
|
+
const label = (rows) => rows.length < entries.length ? [...rows, `+${entries.length - rows.length} more`] : rows;
|
|
172
|
+
let rows = entries.slice(0, MAX_DENY_ROWS_NAMED);
|
|
173
|
+
while (rows.length > 1 && PROBE_REASON_MAX - composed("", label(rows)).length < MIN_TRAVERSAL_ROOT_ROOM)
|
|
174
|
+
rows = rows.slice(0, -1);
|
|
175
|
+
let shown = label(rows);
|
|
176
|
+
if (PROBE_REASON_MAX - composed("", shown).length < MIN_TRAVERSAL_ROOT_ROOM) {
|
|
177
|
+
const over = MIN_TRAVERSAL_ROOT_ROOM - (PROBE_REASON_MAX - composed("", shown).length);
|
|
178
|
+
shown = label([fitMiddle(rows[0] ?? "", Math.max((rows[0] ?? "").length - over, 8))]);
|
|
179
|
+
}
|
|
180
|
+
return { reason: composed(fitMiddle(root, Math.max(PROBE_REASON_MAX - composed("", shown).length, 0)), shown) };
|
|
181
|
+
}
|
|
182
|
+
const MAX_DENY_ROWS_NAMED = 3;
|
|
183
|
+
const MIN_TRAVERSAL_ROOT_ROOM = 24;
|
|
184
|
+
function probeAccountOf(command, allowSet, resolved, verdict) {
|
|
185
|
+
const cause = recursiveReadCause(verdict);
|
|
186
|
+
const hit = denyHitReason(denyFindingOf(command, allowSet, resolved, verdict));
|
|
187
|
+
return { ...(hit.reason !== undefined ? hit : recursiveDenyReason(resolved, verdict)), ...(cause !== undefined ? { cause } : {}) };
|
|
188
|
+
}
|
|
138
189
|
export function bashReadBoundaryProbe(boundary) {
|
|
139
190
|
const allowSet = new Set(bashClassifyDefaultAllow());
|
|
140
191
|
const judge = (command, resolved, offers) => {
|
|
141
192
|
const seatVerdict = (verdict) => {
|
|
142
193
|
if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict, refusedCommandScan(command, allowSet, resolved, verdict, BOUNDARY_SEAT_DECLINE_EVIDENCE))) {
|
|
143
|
-
|
|
144
|
-
return { reversible: false, mandated: true, ...(cause !== undefined ? { cause } : {}) };
|
|
194
|
+
return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, verdict) };
|
|
145
195
|
}
|
|
146
196
|
if (boundaryUndecided(verdict)) {
|
|
147
|
-
|
|
148
|
-
return { reversible: false, ...(cause !== undefined ? { cause } : {}) };
|
|
197
|
+
return { reversible: false, ...probeAccountOf(command, allowSet, resolved, verdict) };
|
|
149
198
|
}
|
|
150
199
|
if (verdict.reason === undefined)
|
|
151
200
|
return { reversible: true };
|
|
@@ -164,7 +213,7 @@ export function bashReadBoundaryProbe(boundary) {
|
|
|
164
213
|
if (mandated !== undefined)
|
|
165
214
|
return { ...mandated, ...offerFrom(detailed), ...offerFrom(loop) };
|
|
166
215
|
if (readBoundaryMandate(command, allowSet, resolved, detailed, BOUNDARY_SEAT_DECLINE_EVIDENCE).mandated === true)
|
|
167
|
-
return { reversible: false, mandated: true };
|
|
216
|
+
return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, detailed) };
|
|
168
217
|
return compoundVerdict ?? loopVerdict ?? { reversible: true };
|
|
169
218
|
};
|
|
170
219
|
return (args) => {
|
|
@@ -762,30 +811,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
762
811
|
const bgRetained = hasBackgroundShell(env) && env.backgroundCapabilities.retainBackgroundProcesses === true;
|
|
763
812
|
const bgSessionScoped = taskOpts.sessionId !== undefined && taskOpts.taskRegistry !== undefined;
|
|
764
813
|
const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
|
|
765
|
-
const
|
|
766
|
-
let canonicalForRoots;
|
|
767
|
-
const canonicalBoundaryFor = (roots) => {
|
|
768
|
-
const key = roots.join("\u0000");
|
|
769
|
-
if (canonicalForRoots?.key !== key)
|
|
770
|
-
canonicalForRoots = { key, boundary: createCanonicalReadBoundary(env, roots) };
|
|
771
|
-
return canonicalForRoots.boundary;
|
|
772
|
-
};
|
|
773
|
-
const canonicalEscape = async (command, signal) => {
|
|
774
|
-
const resolvedBoundary = typeof taskOpts.readBoundary === "function" ? taskOpts.readBoundary() : taskOpts.readBoundary;
|
|
775
|
-
if (resolvedBoundary === undefined)
|
|
776
|
-
return undefined;
|
|
777
|
-
const ask = { deny: resolvedBoundary.denyMatch, containment: resolvedBoundary.face !== "open" };
|
|
778
|
-
if (ask.deny === undefined && !ask.containment)
|
|
779
|
-
return undefined;
|
|
780
|
-
const faces = [
|
|
781
|
-
classifyCompoundReadonlyDetailed(command, recheckReaders, resolvedBoundary),
|
|
782
|
-
classifyBoundedReadonlyPollLoopDetailed(command, recheckReaders, resolvedBoundary),
|
|
783
|
-
];
|
|
784
|
-
const literal = [...new Set(faces.flatMap((v) => [...(v.checkedPaths ?? []), ...(v.checkedSpellings ?? [])]))];
|
|
785
|
-
const asked = new Set(faces.flatMap((v) => v.outOfRootPaths ?? []));
|
|
786
|
-
const patterns = [...new Set(faces.flatMap((v) => [...(v.undecidedPaths ?? []), ...(v.undecidedSpellings ?? [])]))].filter((p) => !asked.has(normalizeAbsPathLexically(p)));
|
|
787
|
-
return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal, patterns }, resolvedBoundary.roots, FULL_SHELL_FACE_ESCAPE_PROSE, ask, signal);
|
|
788
|
-
};
|
|
814
|
+
const canonicalEscape = createShellCanonicalRecheck(env, taskOpts.readBoundary);
|
|
789
815
|
return defineTool({
|
|
790
816
|
name: "Bash",
|
|
791
817
|
...toolFace("bash"),
|
|
@@ -1258,13 +1284,15 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1258
1284
|
if (deny === undefined || denied !== undefined)
|
|
1259
1285
|
return false;
|
|
1260
1286
|
const folded = normalizeAbsPathLexically(p);
|
|
1287
|
+
if (ask.denyAnswered.has(folded))
|
|
1288
|
+
return false;
|
|
1261
1289
|
const pattern = deny(folded);
|
|
1262
1290
|
if (pattern === null)
|
|
1263
1291
|
return false;
|
|
1264
1292
|
denied = { path: folded, pattern };
|
|
1265
1293
|
return true;
|
|
1266
1294
|
};
|
|
1267
|
-
const judge = async (p, canonicalKnown) => {
|
|
1295
|
+
const judge = async (p, canonicalKnown, origin) => {
|
|
1268
1296
|
if (denyHit(p))
|
|
1269
1297
|
return;
|
|
1270
1298
|
const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : await resolve(p, signal);
|
|
@@ -1279,6 +1307,8 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1279
1307
|
}
|
|
1280
1308
|
if (denyHit(real.value))
|
|
1281
1309
|
return;
|
|
1310
|
+
if (ask.containmentAnswered.has(normalizeAbsPathLexically(origin ?? p)))
|
|
1311
|
+
return;
|
|
1282
1312
|
if (ask.containment && !withinAnyRoot(roots, real.value) && !escaping.includes(real.value))
|
|
1283
1313
|
escaping.push(real.value);
|
|
1284
1314
|
};
|
|
@@ -1299,7 +1329,7 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1299
1329
|
for (const t of expanded.paths) {
|
|
1300
1330
|
if (denied !== undefined)
|
|
1301
1331
|
break;
|
|
1302
|
-
await judge(t, expanded.canonical.get(t));
|
|
1332
|
+
await judge(t, expanded.canonical.get(t), p);
|
|
1303
1333
|
}
|
|
1304
1334
|
}
|
|
1305
1335
|
else
|
|
@@ -1330,42 +1360,90 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1330
1360
|
},
|
|
1331
1361
|
};
|
|
1332
1362
|
}
|
|
1363
|
+
const NOTHING_ANSWERED = new Set();
|
|
1364
|
+
export function createShellCanonicalRecheck(env, readBoundary, face) {
|
|
1365
|
+
const recheckReaders = new Set(bashClassifyDefaultAllow());
|
|
1366
|
+
let canonicalForRoots;
|
|
1367
|
+
const canonicalBoundaryFor = (roots) => {
|
|
1368
|
+
const key = roots.join("\u0000");
|
|
1369
|
+
if (canonicalForRoots?.key !== key)
|
|
1370
|
+
canonicalForRoots = { key, boundary: createCanonicalReadBoundary(env, roots) };
|
|
1371
|
+
return canonicalForRoots.boundary;
|
|
1372
|
+
};
|
|
1373
|
+
return async (command, signal) => {
|
|
1374
|
+
const resolvedBoundary = typeof readBoundary === "function" ? readBoundary() : readBoundary;
|
|
1375
|
+
if (resolvedBoundary === undefined)
|
|
1376
|
+
return undefined;
|
|
1377
|
+
const containmentAsked = resolvedBoundary.face !== "open";
|
|
1378
|
+
if (resolvedBoundary.denyMatch === undefined && !containmentAsked)
|
|
1379
|
+
return undefined;
|
|
1380
|
+
const faces = [
|
|
1381
|
+
classifyCompoundReadonlyDetailed(command, recheckReaders, resolvedBoundary),
|
|
1382
|
+
classifyBoundedReadonlyPollLoopDetailed(command, recheckReaders, resolvedBoundary),
|
|
1383
|
+
];
|
|
1384
|
+
const named = classifyOutOfRootReadGate(command, recheckReaders, resolvedBoundary);
|
|
1385
|
+
const literalAll = [
|
|
1386
|
+
...new Set([
|
|
1387
|
+
...faces.flatMap((v) => [...(v.checkedPaths ?? []), ...(v.checkedSpellings ?? []), ...(v.outOfRootPaths ?? [])]),
|
|
1388
|
+
...named.checked,
|
|
1389
|
+
...named.paths,
|
|
1390
|
+
]),
|
|
1391
|
+
];
|
|
1392
|
+
const folded = (ps) => new Set(ps.map((p) => normalizeAbsPathLexically(p)));
|
|
1393
|
+
const answered = {
|
|
1394
|
+
containment: folded([...faces.flatMap((v) => v.outOfRootPaths ?? []), ...named.paths]),
|
|
1395
|
+
deny: folded([...faces.flatMap((v) => (v.readDeniedPath !== undefined ? [v.readDeniedPath] : [])), ...(named.denied !== undefined ? [named.denied.path] : [])]),
|
|
1396
|
+
};
|
|
1397
|
+
const patterns = [...new Set([...faces.flatMap((v) => [...(v.undecidedPaths ?? []), ...(v.undecidedSpellings ?? [])]), ...named.undecided])];
|
|
1398
|
+
return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal: literalAll, patterns }, resolvedBoundary.roots, face ?? FULL_SHELL_FACE_ESCAPE_PROSE, { deny: resolvedBoundary.denyMatch, containment: containmentAsked, denyAnswered: answered.deny, containmentAnswered: answered.containment }, signal);
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1333
1401
|
async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, ask, signal) {
|
|
1334
1402
|
const resolved = await canonical.escapesAfterResolution(candidates, ask, signal);
|
|
1335
1403
|
if (resolved.denied !== undefined) {
|
|
1336
|
-
|
|
1337
|
-
`read deny list (pattern "${resolved.denied.pattern}"). This list guards credential-class paths and ` +
|
|
1338
|
-
`applies regardless of the containment roots. ${DENY_REFUSAL_NOTE}`;
|
|
1339
|
-
return errorResult(content, violationDetails({ code: "read_path_denied", message: content, target: resolved.denied.path, pattern: resolved.denied.pattern }));
|
|
1404
|
+
return readDenyRefusal(resolved.denied.path, resolved.denied.pattern, face.tool);
|
|
1340
1405
|
}
|
|
1341
1406
|
if (resolved.unverifiable !== undefined) {
|
|
1342
|
-
return
|
|
1343
|
-
face.note
|
|
1407
|
+
return readContainmentRefusal(`Error (${face.tool}): ${resolved.unverifiable}, so this command cannot be confirmed to read only inside the allowed directories for this session: ${readRoots.join(", ")}. ` +
|
|
1408
|
+
face.note);
|
|
1344
1409
|
}
|
|
1345
1410
|
if (resolved.escaping.length > 0) {
|
|
1346
1411
|
const quoted = resolved.escaping.map((p) => `"${p}"`).join(", ");
|
|
1347
|
-
return
|
|
1348
|
-
face.note,
|
|
1412
|
+
return readContainmentRefusal(`Error (${face.tool}): ${face.subject} resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")}${face.verdictPhrase}. ` +
|
|
1413
|
+
face.note, resolved.escaping);
|
|
1349
1414
|
}
|
|
1350
1415
|
return undefined;
|
|
1351
1416
|
}
|
|
1352
|
-
const DENY_REFUSAL_NOTE = "The read boundary
|
|
1353
|
-
"would actually read
|
|
1417
|
+
const DENY_REFUSAL_NOTE = "The read boundary judges this command's written spelling and, where a filesystem can be consulted, the " +
|
|
1418
|
+
"paths it would actually read; a deny-listed read is refused here rather than escalated.";
|
|
1419
|
+
function readContainmentRefusal(content, paths = []) {
|
|
1420
|
+
return errorResult(content, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths });
|
|
1421
|
+
}
|
|
1422
|
+
function readDenyRefusal(target, pattern, tool = "Bash") {
|
|
1423
|
+
const content = `Error (${tool}): reading "${target}" is refused: the target matches ${sensitivePathDenyList(pattern)}. ` +
|
|
1424
|
+
`${SENSITIVE_PATH_DENY_SCOPE} ${DENY_REFUSAL_NOTE}`;
|
|
1425
|
+
return errorResult(content, violationDetails({ code: "read_path_denied", message: content, target, pattern }));
|
|
1426
|
+
}
|
|
1354
1427
|
const READONLY_FACE_ESCAPE_PROSE = {
|
|
1428
|
+
tool: "Bash",
|
|
1355
1429
|
subject: "a path this command reads",
|
|
1356
1430
|
verdictPhrase: ` ${NOT_AUTO_ALLOWED}`,
|
|
1357
1431
|
note: BASH_READONLY_CONFINEMENT_NOTE,
|
|
1358
1432
|
};
|
|
1359
1433
|
const FULL_SHELL_FACE_ESCAPE_PROSE = {
|
|
1434
|
+
tool: "Bash",
|
|
1360
1435
|
subject: "a path this command names",
|
|
1361
1436
|
verdictPhrase: "",
|
|
1362
1437
|
note: "The read boundary judged this command's written spelling before the call was approved, and execution is past the approval step — a target reached only through symlink resolution is refused here rather than escalated.",
|
|
1363
1438
|
};
|
|
1439
|
+
export const MONITOR_ESCAPE_PROSE = { ...FULL_SHELL_FACE_ESCAPE_PROSE, tool: "Monitor" };
|
|
1364
1440
|
export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
1365
1441
|
const timeoutCaps = resolveBashTimeoutCaps(opts);
|
|
1366
1442
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
1367
1443
|
const sample = [...allow].slice(0, 6).join(", ");
|
|
1368
1444
|
const readRoots = [rootCanonical, ...(opts?.additionalRoots ?? [])];
|
|
1445
|
+
const denySeat = readDenySeat(opts?.readDeny);
|
|
1446
|
+
const readBoundary = { roots: readRoots, cwd: rootCanonical, ...denySeat };
|
|
1369
1447
|
const overflowSpoolFence = createShellOverflowSpoolFence(env);
|
|
1370
1448
|
const readsOnlyEngineOverflowSpool = async (verdict) => {
|
|
1371
1449
|
if (verdict.outOfRootRead !== true)
|
|
@@ -1377,6 +1455,9 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
|
1377
1455
|
return false;
|
|
1378
1456
|
for (const p of [...paths, ...(verdict.outOfRootSpellings ?? [])]) {
|
|
1379
1457
|
const real = await canonicalBoundary.canonicalOf(p);
|
|
1458
|
+
for (const view of [real, p])
|
|
1459
|
+
if (view !== undefined && denySeat.denyMatch?.(normalizeAbsPathLexically(view)) != null)
|
|
1460
|
+
return false;
|
|
1380
1461
|
if (!(await overflowSpoolFence(real ?? p)))
|
|
1381
1462
|
return false;
|
|
1382
1463
|
}
|
|
@@ -1404,17 +1485,30 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
|
1404
1485
|
const reason = coarseReadonlyCheck(command, allow);
|
|
1405
1486
|
if (reason)
|
|
1406
1487
|
return errorResult(`Error (Bash): ${reason}`);
|
|
1407
|
-
const boundary = classifySimpleCommandReadBoundary(command,
|
|
1488
|
+
const boundary = classifySimpleCommandReadBoundary(command, readBoundary);
|
|
1489
|
+
let releasedByGrant = [];
|
|
1490
|
+
if (boundary.readDeniedPath !== undefined && boundary.readDeniedPattern !== undefined)
|
|
1491
|
+
return readDenyRefusal(boundary.readDeniedPath, boundary.readDeniedPattern);
|
|
1492
|
+
const traversal = boundary.recursiveReadPaths ?? [];
|
|
1493
|
+
if (traversal.length > 0) {
|
|
1494
|
+
const shown = traversal.slice(0, RECURSIVE_CAUSE_MAX_PATHS);
|
|
1495
|
+
return readContainmentRefusal(`Error (Bash): this lexical check cannot bound what a recursive read form in this command traverses, and a deny list is in force — ` +
|
|
1496
|
+
`${traversal.length} path(s) taken as traversal roots: ${shown.join(", ")}${traversal.length > shown.length ? `, and ${traversal.length - shown.length} more` : ""}. ` +
|
|
1497
|
+
`Use the Grep tool to search a directory tree — it applies the same deny list to every file it reads. ` +
|
|
1498
|
+
`${BASH_READONLY_CONFINEMENT_NOTE}`);
|
|
1499
|
+
}
|
|
1408
1500
|
if (boundary.reason !== undefined) {
|
|
1409
1501
|
const rescueCandidates = [...(boundary.outOfRootPaths ?? []), ...(boundary.outOfRootSpellings ?? [])];
|
|
1410
1502
|
const rescued = boundary.outOfRootRead === true &&
|
|
1411
1503
|
(boundary.unresolvedOperands ?? []).length === 0 &&
|
|
1412
1504
|
(await canonicalBoundary.allResolveInside(rescueCandidates, ctx.signal));
|
|
1505
|
+
if (rescued)
|
|
1506
|
+
releasedByGrant = rescueCandidates;
|
|
1413
1507
|
if (!rescued && !(await readsOnlyEngineOverflowSpool(boundary))) {
|
|
1414
|
-
return
|
|
1508
|
+
return readContainmentRefusal(`Error (Bash): ${boundary.reason}. ${BASH_READONLY_CONFINEMENT_NOTE}`, boundary.outOfRootPaths ?? []);
|
|
1415
1509
|
}
|
|
1416
1510
|
}
|
|
1417
|
-
const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? [])], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, { deny:
|
|
1511
|
+
const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? []), ...releasedByGrant], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, { deny: denySeat.denyMatch, containment: true, denyAnswered: NOTHING_ANSWERED, containmentAnswered: NOTHING_ANSWERED }, ctx.signal);
|
|
1418
1512
|
if (escape !== undefined)
|
|
1419
1513
|
return escape;
|
|
1420
1514
|
const readOnlyTick = opts?.onToolProgress === undefined ? undefined : (t) => describeSafely(opts.onToolProgress, { toolCallId: ctx.toolCallId, toolName: "Bash", ...t });
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -46,7 +46,8 @@ export interface HandsToolkitOptions {
|
|
|
46
46
|
* (see `createBashTool`'s `readBoundary`). The caller passes the SAME seat it gives the gate's shell
|
|
47
47
|
* probes, so the boundary that judges a command's written spelling before approval and the leg that
|
|
48
48
|
* re-judges its symlink-resolved targets before execution are one declaration. Ignored in `readOnly`
|
|
49
|
-
* mode — `bash_readonly` builds its own from the roots it is confined to
|
|
49
|
+
* mode — `bash_readonly` builds its own: containment from the roots it is confined to, DENY from the same
|
|
50
|
+
* compiled table this band hands the structured read faces.
|
|
50
51
|
*
|
|
51
52
|
* ABSENT ⇒ the leg does not run and costs no I/O: with no boundary declared there is no "outside" for
|
|
52
53
|
* a read to be outside of, which is the same reading the probe seats take of an absent boundary. It
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -72,6 +72,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
72
72
|
...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
|
|
73
73
|
...(opts.onNotice !== undefined ? { onNotice: opts.onNotice } : {}),
|
|
74
74
|
...(readFaceRoots !== undefined ? { additionalRoots: readFaceRoots } : {}),
|
|
75
|
+
readDeny,
|
|
75
76
|
...(opts.onToolProgress !== undefined ? { onToolProgress: opts.onToolProgress } : {}),
|
|
76
77
|
})
|
|
77
78
|
: createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
|
|
@@ -61,6 +61,34 @@ export interface ReadDenyMatcher {
|
|
|
61
61
|
* matching the deny list exist under the scope and were excluded). */
|
|
62
62
|
readonly rgProbeGlobs: readonly ReadDenyRgGlob[];
|
|
63
63
|
}
|
|
64
|
+
/** The DENY HALF of a shell read boundary, in the shape `BashReadonlyRootBoundary` declares it (structural:
|
|
65
|
+
* this module is the leaf every face depends on). `denyMatch` judges one LEXICALLY RESOLVED operand,
|
|
66
|
+
* returning the matched pattern or null; PRESENT-IFF there is something to judge. */
|
|
67
|
+
export interface ReadDenySeat {
|
|
68
|
+
denyMatch?: (resolvedPath: string) => string | null;
|
|
69
|
+
denyEntries: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
/** The ONE adapter from a compiled matcher to that seat, so every face mounts it the SAME way and the "wire
|
|
72
|
+
* the judge only when it has something to judge" rule is spelled once. That rule is load-bearing: the
|
|
73
|
+
* seat's PRESENCE is itself read as a fact (the recursive-read demotion mints on "a deny judge is wired" =
|
|
74
|
+
* the check a traversal would outflank is in place), so a judge over an EMPTY set must not be wired. Call
|
|
75
|
+
* ONCE per mount and reuse the result — the returned closure is then the function OBJECT every face mounted
|
|
76
|
+
* from that matcher shares, which makes "the same judge" checkable rather than argued. */
|
|
77
|
+
export declare function readDenySeat(matcher: ReadDenyMatcher | undefined): ReadDenySeat;
|
|
78
|
+
/** The ONE naming of the deny table and the pattern that matched. Every face composes its own SUBJECT and
|
|
79
|
+
* VERB in front of it and its own DISPOSITION behind it (the verb is not shared: one face names the
|
|
80
|
+
* target, another the REQUESTED SPELLING under a symlink alias). What must not fork is the naming — a
|
|
81
|
+
* reader comparing two surfaces about one finding must not be reading two findings. A LIST of patterns is
|
|
82
|
+
* the same naming in the plural, for the one finding that names no single row: a traversal that goes PAST
|
|
83
|
+
* the judge names the table IN FORCE, never a match it did not make. Four composers: the
|
|
84
|
+
* structured read faces' refusal (`safety.ts`), the shell faces' refusal card (`readDenyRefusal`), the
|
|
85
|
+
* lexical walk's verdict sentence (`bash-readonly-classifier.ts`), and the full shell's MANDATED ask card,
|
|
86
|
+
* which is approvable and so states the finding without claiming a refusal. NOT composers, each stating
|
|
87
|
+
* its own fact: the compaction lane's withheld-attachment HTML attribute, and the traversal notes'
|
|
88
|
+
* withheld-COUNT lines, which name the table and a pattern LIST. */
|
|
89
|
+
export declare function sensitivePathDenyList(pattern: string | readonly string[]): string;
|
|
90
|
+
/** The deny table's REACH: a deny row holds INSIDE the containment roots, so no root grant answers it. */
|
|
91
|
+
export declare const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots.";
|
|
64
92
|
/**
|
|
65
93
|
* The closed set of built-in tiers. A tier is the deployment's selection unit
|
|
66
94
|
* ({@link ReadDenyBuiltinConfig.tiers}); every {@link READ_FACE_BUILTIN_DENY_TABLE} row belongs to
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export function readDenySeat(matcher) {
|
|
2
|
+
const entries = (matcher?.entries ?? []).map((e) => e.pattern);
|
|
3
|
+
if (matcher === undefined || entries.length === 0)
|
|
4
|
+
return { denyEntries: entries };
|
|
5
|
+
return { denyMatch: (p) => matcher.matchPath(p)?.pattern ?? null, denyEntries: entries };
|
|
6
|
+
}
|
|
7
|
+
export function sensitivePathDenyList(pattern) {
|
|
8
|
+
const rows = typeof pattern === "string" ? [pattern] : pattern;
|
|
9
|
+
return `the sensitive-path read deny list (pattern${rows.length === 1 ? "" : "s"} ${rows.map((p) => `"${p}"`).join(", ")})`;
|
|
10
|
+
}
|
|
11
|
+
export const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots.";
|
|
1
12
|
export const READ_DENY_BUILTIN_TIERS = ["credentials", "shell-history", "browser", "wallet", "agent-config"];
|
|
2
13
|
export const READ_FACE_BUILTIN_DENY_TABLE = [
|
|
3
14
|
{ pattern: ".ssh", tier: "credentials" },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
|
|
1
2
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
3
|
import { protectivePathTargetOf, type ToolCallLike } from "../../core/tool-registry.js";
|
|
3
4
|
/**
|
|
@@ -320,7 +321,6 @@ export declare const WIN32_NAMESPACE_REFUSAL_TEXT: {
|
|
|
320
321
|
};
|
|
321
322
|
/** The fence over the text table: `never` while every refusal word has a row. */
|
|
322
323
|
export type NamespaceRefusalTextCoversEveryWord = AssertAllKeysHandled<Exclude<Win32NamespaceRefusal, keyof typeof WIN32_NAMESPACE_REFUSAL_TEXT>>;
|
|
323
|
-
type AssertAllKeysHandled<T extends never> = T;
|
|
324
324
|
/**
|
|
325
325
|
* The Win32 namespace screen — run on every model-spelled target BEFORE the family join, at every site
|
|
326
326
|
* (the fs throat, the effective-target reader, the rule side, the glob splitter), so one spelling is
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { sensitivePathDenyList, SENSITIVE_PATH_DENY_SCOPE } from "./read-deny.js";
|
|
2
3
|
import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
|
|
3
4
|
import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
|
|
4
5
|
import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
|
|
@@ -468,7 +469,7 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
|
|
|
468
469
|
ok: false,
|
|
469
470
|
violation: {
|
|
470
471
|
code: "read_path_denied",
|
|
471
|
-
message: `reading "${path}" ${key !== path ? `(canonical target: "${key}") ` : ""}is refused: ${judged}
|
|
472
|
+
message: `reading "${path}" ${key !== path ? `(canonical target: "${key}") ` : ""}is refused: ${judged} ${sensitivePathDenyList(hit.pattern)}. ${SENSITIVE_PATH_DENY_SCOPE}`,
|
|
472
473
|
target: key,
|
|
473
474
|
pattern: hit.pattern,
|
|
474
475
|
},
|
package/dist/tools/monitor.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
|
|
|
42
42
|
import { type MonitorTimers, type TaskRegistry } from "../core/task-registry.js";
|
|
43
43
|
import type { TaskNotificationPayload } from "../core/task-notification.js";
|
|
44
44
|
import type { ToolResultStore } from "../core/tool-result-store.js";
|
|
45
|
+
import type { BashReadonlyRootBoundary } from "./fs/bash-readonly-classifier.js";
|
|
45
46
|
export interface MonitorToolOptions {
|
|
46
47
|
registry: TaskRegistry;
|
|
47
48
|
/** Fallbacks when the runtime ctx carries no identity (direct mounts) — same posture as createBashTool. */
|
|
@@ -68,6 +69,10 @@ export interface MonitorToolOptions {
|
|
|
68
69
|
* task-registry-monitor.ts's spill helper). Absent ⇒ legal degrade, byte-identical rolling (no spill,
|
|
69
70
|
* same loss shape as before this design). */
|
|
70
71
|
toolResultStore?: ToolResultStore;
|
|
72
|
+
/** The deployment's shell READ BOUNDARY — the SAME option `createBashTool` takes: this face spawns a
|
|
73
|
+
* shell command too, so the seat that judges the WRITTEN spelling needs the leg that re-judges where the
|
|
74
|
+
* kernel takes it. BUILT here, so a library mount is not fail-open on one face and closed on the other. */
|
|
75
|
+
readBoundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined);
|
|
71
76
|
/** RB-465-b (HRD-PTL-3 残件): spec-level retain (`TaskSpec.retainBackgroundProcesses`) — the SECOND
|
|
72
77
|
* retain lane, OR-composed with the env capability at the teardown sweep. Without this bit the mount
|
|
73
78
|
* could only see the env lane, so a deployment retaining per spec still got the "session ends it"
|
package/dist/tools/monitor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { defineTool, errorResult } from "../core/tools.js";
|
|
|
3
3
|
import { hasBackgroundShell } from "../core/background-shell.js";
|
|
4
4
|
import { MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, } from "../core/task-registry.js";
|
|
5
5
|
import { toolFace } from "../core/tool-catalog-entries.js";
|
|
6
|
+
import { createShellCanonicalRecheck, MONITOR_ESCAPE_PROSE } from "./fs/fs-bash.js";
|
|
6
7
|
import { thrownText } from "../core/thrown-value.js";
|
|
7
8
|
const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its stdout as a stream of events.\n" +
|
|
8
9
|
"\n" +
|
|
@@ -42,6 +43,7 @@ function envBackgroundWallSec(caps, requestedSec) {
|
|
|
42
43
|
return Math.min(base, cap);
|
|
43
44
|
}
|
|
44
45
|
export function createMonitorTool(env, opts) {
|
|
46
|
+
const canonicalRecheck = opts.readBoundary === undefined ? undefined : createShellCanonicalRecheck(env, opts.readBoundary, MONITOR_ESCAPE_PROSE);
|
|
45
47
|
return defineTool({
|
|
46
48
|
name: "Monitor",
|
|
47
49
|
...toolFace("monitor"),
|
|
@@ -68,6 +70,11 @@ export function createMonitorTool(env, opts) {
|
|
|
68
70
|
const isPersistent = persistent === true;
|
|
69
71
|
const timeoutMs = Math.min(MONITOR_MAX_TIMEOUT_MS, Math.max(1, Math.floor(timeout_ms ?? MONITOR_DEFAULT_TIMEOUT_MS)));
|
|
70
72
|
const spawnTimeoutSec = isPersistent ? undefined : Math.ceil(timeoutMs / 1000) + 5;
|
|
73
|
+
const escape = await canonicalRecheck?.(command, ctx.signal);
|
|
74
|
+
if (escape !== undefined)
|
|
75
|
+
return escape;
|
|
76
|
+
if (ctx.signal?.aborted === true)
|
|
77
|
+
return errorResult("Error (Monitor): the call was cancelled before the watch started.");
|
|
71
78
|
const spawn = await env.spawnBackground(command, {
|
|
72
79
|
...(opts.cwdRef !== undefined ? { cwd: opts.cwdRef.current } : {}),
|
|
73
80
|
...(spawnTimeoutSec !== undefined ? { timeout: spawnTimeoutSec } : {}),
|
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.23.4",
|
|
4
|
+
"from": "141b1687631b53cf2998a56c7541bb4aab5ca9af",
|
|
5
|
+
"to": "v7.23.4",
|
|
6
|
+
"added": [],
|
|
7
|
+
"removed": [],
|
|
8
|
+
"changed": [],
|
|
9
|
+
"narrowed": [
|
|
10
|
+
{
|
|
11
|
+
"id": "F1",
|
|
12
|
+
"source": "behaviorFlips.summary",
|
|
13
|
+
"text": "NARROWING, the `bash_readonly` read-only shell face (`handsReadOnly`), deployments with a deny judge wired (#937 = sema-internal #67, P1; stock since design/199 — measured identical on 7.22.0 and 7.23.3): ONE deny judge for every read face. The v1 exception that kept `denyMatch` off the read-only shell face is retired: `readDenySeat()` (`src/tools/fs/read-deny.ts`) is the single adapter from the compiled deny set to a boundary's deny half, mounted by the structured read faces and now by `bash_readonly` at both of its judging moments — the lexical verdict and the execute-time recheck (`src/tools/fs/fs-bash.ts`, `{ deny: denySeat.denyMatch, containment: true }`). One mint for the deny refusal (`readDenyRefusal`) and one for the containment refusal (`readContainmentRefusal`). A containment GRANT (root rescue, overflow-spool recovery) answers containment only, never deny (codex r1/r2). Rules −4. WHO NEEDS IT: a deployment that mounts the read-only shell (server autonomy read-only tier) AND configures `readDenyPatterns` / `READ_DENY_BUILTIN_TIERS` — until now `cat .env` ran there at zero cards with the bytes returned. WHO IS AFFECTED: (a) on that face, a deny-listed read is REFUSED (`read_path_denied`, naming the pattern) — named spelling, glob, symlink target, and a recursive walk that would enter a deny row; (b) a recursive verb (`grep -r <needle> .`) on such a deployment is refused as a whole with the Grep tool named as the way out (KL-937b); (c) a bracket-named glob spelling may over-fire fail-closed (KL-937a). Deployments with no deny judge: byte-identical on this face; the full Bash face: unchanged (contrast column pinned)."
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"closedSetMembers": [],
|
|
17
|
+
"closedSetsUnreadable": [],
|
|
18
|
+
"typeKeys": [
|
|
19
|
+
{
|
|
20
|
+
"type": "CompoundReadonlyVerdict",
|
|
21
|
+
"at": [
|
|
22
|
+
"src/tools/fs/bash-readonly-classifier.ts:644"
|
|
23
|
+
],
|
|
24
|
+
"added": [
|
|
25
|
+
"readDeniedPath",
|
|
26
|
+
"readDeniedPattern"
|
|
27
|
+
],
|
|
28
|
+
"removed": [],
|
|
29
|
+
"breaking": false,
|
|
30
|
+
"criteriaCovered": false
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"declarationMoves": [],
|
|
34
|
+
"mintMoves": [],
|
|
35
|
+
"criteriaObserves": [
|
|
36
|
+
{
|
|
37
|
+
"kind": "taskEventField",
|
|
38
|
+
"name": "tool_end.isError",
|
|
39
|
+
"assertions": [
|
|
40
|
+
"G1.a",
|
|
41
|
+
"G1.b"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"consumersHint": {
|
|
46
|
+
"cli": [
|
|
47
|
+
"CompoundReadonlyVerdict"
|
|
48
|
+
],
|
|
49
|
+
"server": [
|
|
50
|
+
"CompoundReadonlyVerdict"
|
|
51
|
+
],
|
|
52
|
+
"sdk": [],
|
|
53
|
+
"note": "scanned cli@4e3b6980 server@866e273b sdk@0a6aeb7 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
|
|
54
|
+
},
|
|
55
|
+
"breakingCount": 0,
|
|
56
|
+
"breakingItems": [],
|
|
57
|
+
"toResolvedAt": "2a5d3b7f6c6d633510d8866e95d66e7b2b85446a"
|
|
58
|
+
}
|