@sema-agent/core 7.23.4 → 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 +56 -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 +4 -0
- package/dist/core/runner/prepare-task.js +2 -2
- package/dist/core/runner/run-leg.js +2 -13
- 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 +30 -7
- package/dist/tools/fs/bash-readonly-classifier.js +31 -10
- package/dist/tools/fs/fs-bash.d.ts +30 -1
- package/dist/tools/fs/fs-bash.js +123 -54
- package/dist/tools/fs/read-deny.d.ts +14 -0
- package/dist/tools/fs/read-deny.js +5 -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.5.json +133 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sensitivePathDenyList } from "./read-deny.js";
|
|
1
2
|
import { hasWin32AliasedComponent, isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
|
|
2
3
|
import { lexShellText, literalTildeSpelling, quoteMask, substitutionsInHeredocBody, SUBSTITUTION_PLACEHOLDER, wordsOf, } from "./bash-lexer.js";
|
|
3
4
|
import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRunsShell, shellScanDeclineOf, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
|
|
@@ -19,11 +20,16 @@ function hasBareShellOperator(s, quotedOperatorsAreText) {
|
|
|
19
20
|
if (!mask.balanced)
|
|
20
21
|
return SHELL_OPERATORS.test(s);
|
|
21
22
|
for (let i = 0; i < s.length; i++) {
|
|
22
|
-
|
|
23
|
+
const ch = s[i];
|
|
24
|
+
if (!SHELL_OPERATORS.test(ch))
|
|
25
|
+
continue;
|
|
26
|
+
const literal = SHELL_EXPANDS_IN_DOUBLE_QUOTES.test(ch) ? mask.openedBy[i] === "'" : mask.quoted[i];
|
|
27
|
+
if (!literal)
|
|
23
28
|
return true;
|
|
24
29
|
}
|
|
25
30
|
return false;
|
|
26
31
|
}
|
|
32
|
+
const SHELL_EXPANDS_IN_DOUBLE_QUOTES = /[$`]/;
|
|
27
33
|
export function parseLeadingCommandName(command, options) {
|
|
28
34
|
const trimmed = command.trim();
|
|
29
35
|
if (!trimmed)
|
|
@@ -2169,7 +2175,7 @@ function evaluateReadBoundary(foldedSegments, boundary, mintGrantCandidate, reac
|
|
|
2169
2175
|
const denied = boundary.denyMatch?.(finding.path);
|
|
2170
2176
|
if (denied != null) {
|
|
2171
2177
|
return {
|
|
2172
|
-
reason: `a command operand resolves to "${finding.path}", which matches
|
|
2178
|
+
reason: `a command operand resolves to "${finding.path}", which matches ${sensitivePathDenyList(denied)} ${NOT_AUTO_ALLOWED}`,
|
|
2173
2179
|
readDenied: true,
|
|
2174
2180
|
readDeniedPath: finding.path,
|
|
2175
2181
|
readDeniedPattern: denied,
|
|
@@ -2301,7 +2307,7 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
2301
2307
|
}
|
|
2302
2308
|
return evaluateReadBoundary([toks], boundary, true);
|
|
2303
2309
|
}
|
|
2304
|
-
const NOT_GATED = { gated: false, paths: [] };
|
|
2310
|
+
const NOT_GATED = { gated: false, paths: [], checked: [], undecided: [] };
|
|
2305
2311
|
const UNREADABLE_WHITELIST_WORD = /[$`()*?[\]{}]/;
|
|
2306
2312
|
const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
2307
2313
|
export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
|
|
@@ -2359,18 +2365,33 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
|
2359
2365
|
if (readers.length === 0)
|
|
2360
2366
|
return NOT_GATED;
|
|
2361
2367
|
if (baseMoved)
|
|
2362
|
-
return { gated: true, paths: [] };
|
|
2368
|
+
return { gated: true, paths: [], checked: [], undecided: [] };
|
|
2363
2369
|
const verdict = evaluateReadBoundary(readers, containmentOnly, false, readerReach);
|
|
2364
|
-
const
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2370
|
+
const denyPass = boundary.denyMatch === undefined ? undefined : evaluateReadBoundary(readers, boundary, false, readerReach);
|
|
2371
|
+
const denied = denyPass?.readDenied === true && denyPass.readDeniedPath !== undefined && denyPass.readDeniedPattern !== undefined
|
|
2372
|
+
? { path: denyPass.readDeniedPath, pattern: denyPass.readDeniedPattern }
|
|
2373
|
+
: undefined;
|
|
2374
|
+
const resolvedOperands = {
|
|
2375
|
+
checked: [...(verdict.checkedPaths ?? []), ...(verdict.checkedSpellings ?? [])],
|
|
2376
|
+
undecided: [...(verdict.undecidedPaths ?? []), ...(verdict.undecidedSpellings ?? [])],
|
|
2377
|
+
};
|
|
2378
|
+
if (verdict.outOfRootRead === true) {
|
|
2379
|
+
return { gated: true, paths: verdict.outOfRootPaths ?? [], ...resolvedOperands, ...(denied !== undefined ? { denied } : {}) };
|
|
2380
|
+
}
|
|
2381
|
+
if (denied !== undefined)
|
|
2382
|
+
return { gated: false, paths: [], ...resolvedOperands, denied };
|
|
2383
|
+
return resolvedOperands.checked.length === 0 && resolvedOperands.undecided.length === 0 ? NOT_GATED : { gated: false, paths: [], ...resolvedOperands };
|
|
2368
2384
|
};
|
|
2369
2385
|
const direct = judge(command);
|
|
2370
|
-
if (direct.gated || direct.denied
|
|
2386
|
+
if (direct.gated || direct.denied !== undefined)
|
|
2371
2387
|
return direct;
|
|
2372
2388
|
const shape = POLL_LOOP_SHAPE.exec(command.trim());
|
|
2373
|
-
|
|
2389
|
+
if (shape === null)
|
|
2390
|
+
return direct;
|
|
2391
|
+
const loop = judge(shape[3]);
|
|
2392
|
+
return loop.gated || loop.denied !== undefined
|
|
2393
|
+
? loop
|
|
2394
|
+
: { ...loop, checked: [...direct.checked, ...loop.checked], undecided: [...direct.undecided, ...loop.undecided] };
|
|
2374
2395
|
}
|
|
2375
2396
|
export function classifyCompoundReadonly(command, allow, boundary) {
|
|
2376
2397
|
return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
|
|
@@ -66,6 +66,10 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
|
|
|
66
66
|
* ONE rule, TWO TABLES: which {@link ShellScanDecline} a SEAT reads as read evidence. A decline names no word,
|
|
67
67
|
* so it is never the boundary DECLARING anything about a path.
|
|
68
68
|
*
|
|
69
|
+
* BOTH tables are ROW-PER-MEMBER objects behind a compile-time fence ({@link BOUNDARY_SEAT_READS_DECLINE} /
|
|
70
|
+
* {@link CLASSIFY_SEAT_READS_DECLINE}); the sets below are DERIVED from those rows and the ARGUMENT for the
|
|
71
|
+
* rows is this block. "Empty by the argument" is a row saying `false`, not a literal nobody can judge.
|
|
72
|
+
*
|
|
69
73
|
* The READ-BOUNDARY seat is what the default/off doctrine and a bypass mode mount ALONE: there "not mandated"
|
|
70
74
|
* is VOUCHED, so a decline must count as evidence or a read nobody judged runs silently (`<out-of-root> |
|
|
71
75
|
* xargs cat` at zero asks — a closed P0). It keeps both. The CLASSIFY seat composes the same boundary with the
|
|
@@ -87,8 +91,13 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
|
|
|
87
91
|
* DID read naming a path outside the roots still gates the call ({@link classifyOutOfRootReadGate}).
|
|
88
92
|
*/
|
|
89
93
|
export declare const BOUNDARY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
|
|
90
|
-
/** The classify doctrine's table — see above; empty
|
|
94
|
+
/** The classify doctrine's table in the shape its readers take — see above; empty because every row says `false`. */
|
|
91
95
|
export declare const CLASSIFY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
|
|
96
|
+
export declare function fitMiddle(s: string, budget: number): string;
|
|
97
|
+
/** `s` shortened to `budget` by removing its MIDDLE, marked — cut through the repo's surrogate-safe
|
|
98
|
+
* primitive, never a raw `slice` (a raw cut leaves half an astral code point on a card and in a durable
|
|
99
|
+
* row). Under a dozen characters the HEAD is taken. */
|
|
100
|
+
export declare function elideMiddle(s: string, budget: number): string;
|
|
92
101
|
/**
|
|
93
102
|
* The READ-BOUNDARY seat of the full shell: a `ToolSpec.reversibilityProbe` that answers exactly ONE
|
|
94
103
|
* question — does the deployment's read boundary object to where this command reads? — and nothing about
|
|
@@ -292,6 +301,26 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
292
301
|
* Absent ⇒ historic bare tag. */
|
|
293
302
|
reminderMark?: string;
|
|
294
303
|
}): AgentTool;
|
|
304
|
+
type ShellCanonicalRefusal = {
|
|
305
|
+
content: string;
|
|
306
|
+
isError: true;
|
|
307
|
+
details?: unknown;
|
|
308
|
+
};
|
|
309
|
+
/** What a face contributes to the shared refusal cards: the TOOL refusing, and the three prose slots. */
|
|
310
|
+
export interface ShellFaceProse {
|
|
311
|
+
readonly tool: string;
|
|
312
|
+
readonly subject: string;
|
|
313
|
+
readonly verdictPhrase: string;
|
|
314
|
+
readonly note: string;
|
|
315
|
+
}
|
|
316
|
+
export type ShellCanonicalRecheck = (command: string, signal?: AbortSignal) => Promise<ShellCanonicalRefusal | undefined>;
|
|
317
|
+
/**
|
|
318
|
+
* THE execute-time canonical recheck, mounted by every face that spawns a shell command — the full `Bash`
|
|
319
|
+
* tool and `Monitor`, whose approval-time seats judge the WRITTEN spelling while this re-judges where the
|
|
320
|
+
* kernel takes those words. The SAME leg, not a second one; the contract below is the leg's own. */
|
|
321
|
+
export declare function createShellCanonicalRecheck(env: ExecutionEnv, readBoundary: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined) | undefined, face?: ShellFaceProse): ShellCanonicalRecheck;
|
|
322
|
+
/** The `Monitor` face's prose — the full shell's with its own tool name: one leg, one finding, two tools. */
|
|
323
|
+
export declare const MONITOR_ESCAPE_PROSE: ShellFaceProse;
|
|
295
324
|
/**
|
|
296
325
|
* `bash_readonly` (effect:read) — a restricted shell for the verifier read-only boundary: a single
|
|
297
326
|
* allowlisted, bare command with no shell operators. effect:read lets it survive the verifier's
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -7,15 +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 { readDenySeat } from "./read-deny.js";
|
|
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";
|
|
19
20
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
20
21
|
import { thrownText } from "../../core/thrown-value.js";
|
|
21
22
|
const RECURSIVE_CAUSE_MAX_PATHS = 3;
|
|
@@ -35,13 +36,13 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
35
36
|
const backgrounded = isBackgroundShellCall(args);
|
|
36
37
|
const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
|
|
37
38
|
if (backgrounded)
|
|
38
|
-
return { reversible: false, ...boundaryGate(detailed) };
|
|
39
|
+
return { reversible: false, ...boundaryGate(detailed), ...probeAccountOf(command, allowSet, resolved, detailed) };
|
|
39
40
|
if (detailed.reason === undefined) {
|
|
40
41
|
if (boundaryUnread(detailed)) {
|
|
41
|
-
const
|
|
42
|
-
if (cause !== undefined)
|
|
43
|
-
return { reversible: false, ...boundaryGate(detailed),
|
|
44
|
-
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 };
|
|
45
46
|
}
|
|
46
47
|
return { reversible: true };
|
|
47
48
|
}
|
|
@@ -51,9 +52,8 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
51
52
|
const seatOffer = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
|
|
52
53
|
const loopScan = refusedCommandScan(command, allowSet, resolved, loop, CLASSIFY_SEAT_DECLINE_EVIDENCE);
|
|
53
54
|
if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop, loopScan))
|
|
54
|
-
return { reversible: false, mandated: true, ...seatOffer(loop) };
|
|
55
|
-
|
|
56
|
-
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) };
|
|
57
57
|
};
|
|
58
58
|
return (args) => {
|
|
59
59
|
const command = shellCommandOf(args);
|
|
@@ -90,8 +90,8 @@ function shellCommandOf(args) {
|
|
|
90
90
|
function isBackgroundShellCall(args) {
|
|
91
91
|
return args?.run_in_background === true;
|
|
92
92
|
}
|
|
93
|
-
export const BOUNDARY_SEAT_DECLINE_EVIDENCE =
|
|
94
|
-
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);
|
|
95
95
|
function readBoundaryMandate(command, allowSet, resolved, verdict, declineEvidence) {
|
|
96
96
|
if (resolved === undefined)
|
|
97
97
|
return {};
|
|
@@ -99,7 +99,7 @@ function readBoundaryMandate(command, allowSet, resolved, verdict, declineEviden
|
|
|
99
99
|
if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict, scan))
|
|
100
100
|
return { mandated: true };
|
|
101
101
|
const named = classifyOutOfRootReadGate(command, allowSet, resolved);
|
|
102
|
-
if (named.gated || named.denied
|
|
102
|
+
if (named.gated || named.denied !== undefined)
|
|
103
103
|
return { mandated: true };
|
|
104
104
|
return declinedRung(scan);
|
|
105
105
|
}
|
|
@@ -122,7 +122,7 @@ function unjudgedRead(command, allowSet, resolved, verdict, scan) {
|
|
|
122
122
|
if (resolved === undefined)
|
|
123
123
|
return false;
|
|
124
124
|
const named = classifyOutOfRootReadGate(command, allowSet, resolved);
|
|
125
|
-
return named.gated || named.denied
|
|
125
|
+
return named.gated || named.denied !== undefined;
|
|
126
126
|
}
|
|
127
127
|
function recursiveReadCause(detailed) {
|
|
128
128
|
const recursive = detailed.recursiveReadPaths;
|
|
@@ -136,17 +136,65 @@ function recursiveReadCause(detailed) {
|
|
|
136
136
|
...(others.length > 0 ? { further: operandFamily(others) } : {}),
|
|
137
137
|
};
|
|
138
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
|
+
}
|
|
139
189
|
export function bashReadBoundaryProbe(boundary) {
|
|
140
190
|
const allowSet = new Set(bashClassifyDefaultAllow());
|
|
141
191
|
const judge = (command, resolved, offers) => {
|
|
142
192
|
const seatVerdict = (verdict) => {
|
|
143
193
|
if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict, refusedCommandScan(command, allowSet, resolved, verdict, BOUNDARY_SEAT_DECLINE_EVIDENCE))) {
|
|
144
|
-
|
|
145
|
-
return { reversible: false, mandated: true, ...(cause !== undefined ? { cause } : {}) };
|
|
194
|
+
return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, verdict) };
|
|
146
195
|
}
|
|
147
196
|
if (boundaryUndecided(verdict)) {
|
|
148
|
-
|
|
149
|
-
return { reversible: false, ...(cause !== undefined ? { cause } : {}) };
|
|
197
|
+
return { reversible: false, ...probeAccountOf(command, allowSet, resolved, verdict) };
|
|
150
198
|
}
|
|
151
199
|
if (verdict.reason === undefined)
|
|
152
200
|
return { reversible: true };
|
|
@@ -165,7 +213,7 @@ export function bashReadBoundaryProbe(boundary) {
|
|
|
165
213
|
if (mandated !== undefined)
|
|
166
214
|
return { ...mandated, ...offerFrom(detailed), ...offerFrom(loop) };
|
|
167
215
|
if (readBoundaryMandate(command, allowSet, resolved, detailed, BOUNDARY_SEAT_DECLINE_EVIDENCE).mandated === true)
|
|
168
|
-
return { reversible: false, mandated: true };
|
|
216
|
+
return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, detailed) };
|
|
169
217
|
return compoundVerdict ?? loopVerdict ?? { reversible: true };
|
|
170
218
|
};
|
|
171
219
|
return (args) => {
|
|
@@ -763,30 +811,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
763
811
|
const bgRetained = hasBackgroundShell(env) && env.backgroundCapabilities.retainBackgroundProcesses === true;
|
|
764
812
|
const bgSessionScoped = taskOpts.sessionId !== undefined && taskOpts.taskRegistry !== undefined;
|
|
765
813
|
const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
|
|
766
|
-
const
|
|
767
|
-
let canonicalForRoots;
|
|
768
|
-
const canonicalBoundaryFor = (roots) => {
|
|
769
|
-
const key = roots.join("\u0000");
|
|
770
|
-
if (canonicalForRoots?.key !== key)
|
|
771
|
-
canonicalForRoots = { key, boundary: createCanonicalReadBoundary(env, roots) };
|
|
772
|
-
return canonicalForRoots.boundary;
|
|
773
|
-
};
|
|
774
|
-
const canonicalEscape = async (command, signal) => {
|
|
775
|
-
const resolvedBoundary = typeof taskOpts.readBoundary === "function" ? taskOpts.readBoundary() : taskOpts.readBoundary;
|
|
776
|
-
if (resolvedBoundary === undefined)
|
|
777
|
-
return undefined;
|
|
778
|
-
const ask = { deny: resolvedBoundary.denyMatch, containment: resolvedBoundary.face !== "open" };
|
|
779
|
-
if (ask.deny === undefined && !ask.containment)
|
|
780
|
-
return undefined;
|
|
781
|
-
const faces = [
|
|
782
|
-
classifyCompoundReadonlyDetailed(command, recheckReaders, resolvedBoundary),
|
|
783
|
-
classifyBoundedReadonlyPollLoopDetailed(command, recheckReaders, resolvedBoundary),
|
|
784
|
-
];
|
|
785
|
-
const literal = [...new Set(faces.flatMap((v) => [...(v.checkedPaths ?? []), ...(v.checkedSpellings ?? [])]))];
|
|
786
|
-
const asked = new Set(faces.flatMap((v) => v.outOfRootPaths ?? []));
|
|
787
|
-
const patterns = [...new Set(faces.flatMap((v) => [...(v.undecidedPaths ?? []), ...(v.undecidedSpellings ?? [])]))].filter((p) => !asked.has(normalizeAbsPathLexically(p)));
|
|
788
|
-
return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal, patterns }, resolvedBoundary.roots, FULL_SHELL_FACE_ESCAPE_PROSE, ask, signal);
|
|
789
|
-
};
|
|
814
|
+
const canonicalEscape = createShellCanonicalRecheck(env, taskOpts.readBoundary);
|
|
790
815
|
return defineTool({
|
|
791
816
|
name: "Bash",
|
|
792
817
|
...toolFace("bash"),
|
|
@@ -1259,13 +1284,15 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1259
1284
|
if (deny === undefined || denied !== undefined)
|
|
1260
1285
|
return false;
|
|
1261
1286
|
const folded = normalizeAbsPathLexically(p);
|
|
1287
|
+
if (ask.denyAnswered.has(folded))
|
|
1288
|
+
return false;
|
|
1262
1289
|
const pattern = deny(folded);
|
|
1263
1290
|
if (pattern === null)
|
|
1264
1291
|
return false;
|
|
1265
1292
|
denied = { path: folded, pattern };
|
|
1266
1293
|
return true;
|
|
1267
1294
|
};
|
|
1268
|
-
const judge = async (p, canonicalKnown) => {
|
|
1295
|
+
const judge = async (p, canonicalKnown, origin) => {
|
|
1269
1296
|
if (denyHit(p))
|
|
1270
1297
|
return;
|
|
1271
1298
|
const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : await resolve(p, signal);
|
|
@@ -1280,6 +1307,8 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1280
1307
|
}
|
|
1281
1308
|
if (denyHit(real.value))
|
|
1282
1309
|
return;
|
|
1310
|
+
if (ask.containmentAnswered.has(normalizeAbsPathLexically(origin ?? p)))
|
|
1311
|
+
return;
|
|
1283
1312
|
if (ask.containment && !withinAnyRoot(roots, real.value) && !escaping.includes(real.value))
|
|
1284
1313
|
escaping.push(real.value);
|
|
1285
1314
|
};
|
|
@@ -1300,7 +1329,7 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1300
1329
|
for (const t of expanded.paths) {
|
|
1301
1330
|
if (denied !== undefined)
|
|
1302
1331
|
break;
|
|
1303
|
-
await judge(t, expanded.canonical.get(t));
|
|
1332
|
+
await judge(t, expanded.canonical.get(t), p);
|
|
1304
1333
|
}
|
|
1305
1334
|
}
|
|
1306
1335
|
else
|
|
@@ -1331,18 +1360,56 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1331
1360
|
},
|
|
1332
1361
|
};
|
|
1333
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
|
+
}
|
|
1334
1401
|
async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, ask, signal) {
|
|
1335
1402
|
const resolved = await canonical.escapesAfterResolution(candidates, ask, signal);
|
|
1336
1403
|
if (resolved.denied !== undefined) {
|
|
1337
|
-
return readDenyRefusal(resolved.denied.path, resolved.denied.pattern);
|
|
1404
|
+
return readDenyRefusal(resolved.denied.path, resolved.denied.pattern, face.tool);
|
|
1338
1405
|
}
|
|
1339
1406
|
if (resolved.unverifiable !== undefined) {
|
|
1340
|
-
return readContainmentRefusal(`Error (
|
|
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(", ")}. ` +
|
|
1341
1408
|
face.note);
|
|
1342
1409
|
}
|
|
1343
1410
|
if (resolved.escaping.length > 0) {
|
|
1344
1411
|
const quoted = resolved.escaping.map((p) => `"${p}"`).join(", ");
|
|
1345
|
-
return readContainmentRefusal(`Error (
|
|
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}. ` +
|
|
1346
1413
|
face.note, resolved.escaping);
|
|
1347
1414
|
}
|
|
1348
1415
|
return undefined;
|
|
@@ -1352,22 +1419,24 @@ const DENY_REFUSAL_NOTE = "The read boundary judges this command's written spell
|
|
|
1352
1419
|
function readContainmentRefusal(content, paths = []) {
|
|
1353
1420
|
return errorResult(content, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths });
|
|
1354
1421
|
}
|
|
1355
|
-
function readDenyRefusal(target, pattern) {
|
|
1356
|
-
const content = `Error (
|
|
1357
|
-
|
|
1358
|
-
`applies regardless of the containment roots. ${DENY_REFUSAL_NOTE}`;
|
|
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}`;
|
|
1359
1425
|
return errorResult(content, violationDetails({ code: "read_path_denied", message: content, target, pattern }));
|
|
1360
1426
|
}
|
|
1361
1427
|
const READONLY_FACE_ESCAPE_PROSE = {
|
|
1428
|
+
tool: "Bash",
|
|
1362
1429
|
subject: "a path this command reads",
|
|
1363
1430
|
verdictPhrase: ` ${NOT_AUTO_ALLOWED}`,
|
|
1364
1431
|
note: BASH_READONLY_CONFINEMENT_NOTE,
|
|
1365
1432
|
};
|
|
1366
1433
|
const FULL_SHELL_FACE_ESCAPE_PROSE = {
|
|
1434
|
+
tool: "Bash",
|
|
1367
1435
|
subject: "a path this command names",
|
|
1368
1436
|
verdictPhrase: "",
|
|
1369
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.",
|
|
1370
1438
|
};
|
|
1439
|
+
export const MONITOR_ESCAPE_PROSE = { ...FULL_SHELL_FACE_ESCAPE_PROSE, tool: "Monitor" };
|
|
1371
1440
|
export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
1372
1441
|
const timeoutCaps = resolveBashTimeoutCaps(opts);
|
|
1373
1442
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
@@ -1439,7 +1508,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
|
1439
1508
|
return readContainmentRefusal(`Error (Bash): ${boundary.reason}. ${BASH_READONLY_CONFINEMENT_NOTE}`, boundary.outOfRootPaths ?? []);
|
|
1440
1509
|
}
|
|
1441
1510
|
}
|
|
1442
|
-
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 }, ctx.signal);
|
|
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);
|
|
1443
1512
|
if (escape !== undefined)
|
|
1444
1513
|
return escape;
|
|
1445
1514
|
const readOnlyTick = opts?.onToolProgress === undefined ? undefined : (t) => describeSafely(opts.onToolProgress, { toolCallId: ctx.toolCallId, toolName: "Bash", ...t });
|
|
@@ -75,6 +75,20 @@ export interface ReadDenySeat {
|
|
|
75
75
|
* ONCE per mount and reuse the result — the returned closure is then the function OBJECT every face mounted
|
|
76
76
|
* from that matcher shares, which makes "the same judge" checkable rather than argued. */
|
|
77
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.";
|
|
78
92
|
/**
|
|
79
93
|
* The closed set of built-in tiers. A tier is the deployment's selection unit
|
|
80
94
|
* ({@link ReadDenyBuiltinConfig.tiers}); every {@link READ_FACE_BUILTIN_DENY_TABLE} row belongs to
|
|
@@ -4,6 +4,11 @@ export function readDenySeat(matcher) {
|
|
|
4
4
|
return { denyEntries: entries };
|
|
5
5
|
return { denyMatch: (p) => matcher.matchPath(p)?.pattern ?? null, denyEntries: entries };
|
|
6
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.";
|
|
7
12
|
export const READ_DENY_BUILTIN_TIERS = ["credentials", "shell-history", "browser", "wallet", "agent-config"];
|
|
8
13
|
export const READ_FACE_BUILTIN_DENY_TABLE = [
|
|
9
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 } : {}),
|