@sema-agent/core 7.24.3 → 7.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -1
- package/dist/agents/peer-session-drain.d.ts +16 -1
- package/dist/agents/peer-session-drain.js +16 -1
- package/dist/agents/send-message-tool.d.ts +10 -2
- package/dist/agents/send-message-tool.js +18 -7
- package/dist/agents/subagent.js +13 -10
- package/dist/core/background-agent-store.d.ts +3 -3
- package/dist/core/checkpoint-store.d.ts +1 -0
- package/dist/core/delegation-frames.d.ts +5 -0
- package/dist/core/gate-fold.js +85 -0
- package/dist/core/gate-lanes.js +23 -1
- package/dist/core/gate-outcome.d.ts +8 -2
- package/dist/core/gate-outcome.js +3 -1
- package/dist/core/hooks.d.ts +29 -0
- package/dist/core/hooks.js +2 -0
- package/dist/core/mailbox-store.d.ts +5 -0
- package/dist/core/mailbox-store.js +1 -1
- package/dist/core/parent-spec-seat.d.ts +7 -1
- package/dist/core/parent-spec-seat.js +2 -0
- package/dist/core/permission-rule-consent.js +29 -47
- package/dist/core/permission-rule-provider.js +4 -3
- package/dist/core/permission-rule-store.d.ts +143 -7
- package/dist/core/permission-rule-store.js +165 -15
- package/dist/core/permission-rules.js +14 -10
- package/dist/core/rule-denial-sentence.d.ts +75 -0
- package/dist/core/rule-denial-sentence.js +16 -0
- package/dist/core/runner/contracts.d.ts +36 -2
- package/dist/core/runner/gate-exit.d.ts +14 -0
- package/dist/core/runner/gate-exit.js +4 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +4 -4
- package/dist/core/runner/prepare-delegation-surface.js +2 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
- package/dist/core/runner/prepare-gate-stations.js +7 -4
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
- package/dist/core/runner/prepare-hands-readface.js +25 -10
- package/dist/core/runner/prepare-inherited-gate.d.ts +23 -0
- package/dist/core/runner/prepare-inherited-gate.js +11 -6
- package/dist/core/runner/prepare-policy-chain.d.ts +15 -1
- package/dist/core/runner/prepare-policy-chain.js +42 -22
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -2
- package/dist/core/runner/prepare-suspend-saga.js +4 -3
- package/dist/core/runner/prepare-task.js +10 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -4
- package/dist/core/runner/prepare-wiring-manifest.js +8 -4
- package/dist/core/runner/resume-apply.js +2 -0
- package/dist/core/runner/run-harness-handlers.js +1 -0
- package/dist/core/runner/run-identity-wiring.d.ts +2 -2
- package/dist/core/runner/run-leg.js +28 -1
- package/dist/core/runner/run-notification-lane.d.ts +2 -2
- package/dist/core/runner/run-notification-lane.js +2 -1
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/session-rule-policy.d.ts +73 -2
- package/dist/core/runner/session-rule-policy.js +121 -21
- package/dist/core/session-policy-store.d.ts +27 -3
- package/dist/core/session-policy-store.js +12 -4
- package/dist/core/store-contracts/background-agent-store-contract.js +10 -0
- package/dist/core/store-contracts/peer-directory-contract.d.ts +23 -0
- package/dist/core/store-contracts/peer-directory-contract.js +82 -0
- package/dist/core/task-event.d.ts +6 -0
- package/dist/core/task-notification.d.ts +79 -4
- package/dist/core/task-notification.js +90 -1
- package/dist/core/task-registry-agent.js +1 -2
- package/dist/core/task-registry-monitor.js +2 -1
- package/dist/core/task-registry-shared.d.ts +53 -5
- package/dist/core/task-registry-shared.js +13 -2
- package/dist/core/task-registry.js +8 -5
- package/dist/core/task-spec.d.ts +12 -5
- package/dist/core/tool-catalog-entries.js +1 -0
- package/dist/core/tool-spec.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +2 -2
- package/dist/engine/loop/agent-loop.js +1 -1
- package/dist/engine/loop/types.d.ts +4 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -3
- package/dist/orchestration/workflow.js +12 -3
- package/dist/prompts/coordinator.d.ts +2 -1
- package/dist/stores/file/adoption/adopt.js +23 -1
- package/dist/stores/file/permission-rule-store.js +3 -1
- package/dist/stores/file/session-policy-store.d.ts +31 -0
- package/dist/stores/file/session-policy-store.js +87 -7
- package/dist/tools/fs/bash-program-position.d.ts +4 -26
- package/dist/tools/fs/bash-program-position.js +0 -11
- package/dist/tools/fs/bash-readonly-classifier.d.ts +20 -309
- package/dist/tools/fs/bash-readonly-classifier.js +28 -766
- package/dist/tools/fs/fs-bash.d.ts +43 -207
- package/dist/tools/fs/fs-bash.js +20 -341
- package/dist/tools/fs/index.d.ts +12 -13
- package/dist/tools/fs/index.js +3 -4
- package/dist/tools/fs/read-deny.d.ts +25 -0
- package/dist/tools/fs/read-deny.js +19 -0
- package/dist/tools/fs/safety.js +2 -6
- package/dist/tools/fs/shell-read-check.d.ts +99 -0
- package/dist/tools/fs/shell-read-check.js +229 -0
- package/dist/tools/fs/shell-read-establish.d.ts +70 -115
- package/dist/tools/fs/shell-read-establish.js +216 -102
- package/dist/tools/fs/shell-read-judge.d.ts +231 -0
- package/dist/tools/fs/shell-read-judge.js +170 -0
- package/dist/tools/fs/shell-read-plan.d.ts +3 -0
- package/dist/tools/fs/shell-read-plan.js +82 -19
- package/dist/tools/monitor.d.ts +5 -5
- package/dist/tools/monitor.js +3 -5
- package/dist/tools/task-list.d.ts +4 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -3
- package/wire-changes/core@7.24.4.json +54 -0
- package/wire-changes/core@7.25.0.json +671 -0
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -7,82 +7,14 @@ 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";
|
|
11
10
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
12
|
-
import { imageMagicMatches,
|
|
11
|
+
import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
13
12
|
import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
14
13
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
15
|
-
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile,
|
|
16
|
-
import {
|
|
17
|
-
import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.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";
|
|
14
|
+
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, } from "./fs-shared.js";
|
|
15
|
+
import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed } from "./bash-readonly-classifier.js";
|
|
20
16
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
21
17
|
import { thrownText } from "../../core/thrown-value.js";
|
|
22
|
-
const RECURSIVE_CAUSE_MAX_PATHS = 3;
|
|
23
|
-
const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
|
|
24
|
-
function operandFamily(paths) {
|
|
25
|
-
const enc = (s) => s.replace(/%/g, "%25").replace(/;/g, "%3B").replace(/:/g, "%3A");
|
|
26
|
-
return {
|
|
27
|
-
shown: paths.slice(0, RECURSIVE_CAUSE_MAX_PATHS).map((p) => enc(inlineUntrusted(p, PROBE_CAUSE_PATH_MAX))),
|
|
28
|
-
total: paths.length,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
export function bashReversibilityProbe(allow, boundary) {
|
|
32
|
-
const allowSet = new Set(allow ?? bashClassifyDefaultAllow());
|
|
33
|
-
const judge = (args, command, resolved, offers) => {
|
|
34
|
-
const boundaryGate = (verdict) => readBoundaryMandate(command, allowSet, resolved, verdict, CLASSIFY_SEAT_DECLINE_EVIDENCE);
|
|
35
|
-
const outOfRootGate = () => readBoundaryMandate(command, allowSet, resolved, {}, CLASSIFY_SEAT_DECLINE_EVIDENCE);
|
|
36
|
-
const backgrounded = isBackgroundShellCall(args);
|
|
37
|
-
const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
|
|
38
|
-
if (backgrounded)
|
|
39
|
-
return { reversible: false, ...boundaryGate(detailed), ...probeAccountOf(command, allowSet, resolved, detailed) };
|
|
40
|
-
if (detailed.reason === undefined) {
|
|
41
|
-
if (boundaryUnread(detailed)) {
|
|
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 };
|
|
46
|
-
}
|
|
47
|
-
return { reversible: true };
|
|
48
|
-
}
|
|
49
|
-
const loop = classifyBoundedReadonlyPollLoopDetailed(command, allowSet, resolved);
|
|
50
|
-
if (loop.reason === undefined)
|
|
51
|
-
return { reversible: true };
|
|
52
|
-
const seatOffer = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
|
|
53
|
-
const loopScan = refusedCommandScan(command, allowSet, resolved, loop, CLASSIFY_SEAT_DECLINE_EVIDENCE);
|
|
54
|
-
if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop, loopScan))
|
|
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
|
-
};
|
|
58
|
-
return (args) => {
|
|
59
|
-
const command = shellCommandOf(args);
|
|
60
|
-
if (command === undefined)
|
|
61
|
-
return { reversible: false };
|
|
62
|
-
const resolved = typeof boundary === "function" ? boundary() : boundary;
|
|
63
|
-
if (resolved === undefined)
|
|
64
|
-
return judge(args, command, undefined, true);
|
|
65
|
-
return grantClearsSeat((c, b, o) => judge(args, c, b, o), command, resolved);
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
function boundaryUnread(verdict) {
|
|
69
|
-
return boundaryUndecided(verdict) || boundaryUnjudged(verdict);
|
|
70
|
-
}
|
|
71
|
-
function readRootCandidateOf(verdict) {
|
|
72
|
-
const dir = verdict.outOfRootGrantCandidate;
|
|
73
|
-
if (dir === undefined)
|
|
74
|
-
return {};
|
|
75
|
-
return { readRootCandidate: { dir, clearsThisAsk: true, ...(verdict.outOfRootGrantCoversExactPath === true ? { covers: "exact" } : {}) } };
|
|
76
|
-
}
|
|
77
|
-
function boundaryUndecided(verdict) {
|
|
78
|
-
return verdict.undecidedPaths !== undefined && verdict.undecidedPaths.length > 0;
|
|
79
|
-
}
|
|
80
|
-
function boundaryDeclared(verdict) {
|
|
81
|
-
return denyJudgeSpoke(verdict) || verdict.outOfRootRead === true;
|
|
82
|
-
}
|
|
83
|
-
function denyJudgeSpoke(verdict) {
|
|
84
|
-
return verdict.readDenied === true || (verdict.recursiveReadPaths !== undefined && verdict.recursiveReadPaths.length > 0);
|
|
85
|
-
}
|
|
86
18
|
function shellCommandOf(args) {
|
|
87
19
|
const command = args?.command;
|
|
88
20
|
return typeof command === "string" ? command : undefined;
|
|
@@ -90,152 +22,23 @@ function shellCommandOf(args) {
|
|
|
90
22
|
function isBackgroundShellCall(args) {
|
|
91
23
|
return args?.run_in_background === true;
|
|
92
24
|
}
|
|
93
|
-
export
|
|
94
|
-
|
|
95
|
-
function readBoundaryMandate(command, allowSet, resolved, verdict, declineEvidence) {
|
|
96
|
-
if (resolved === undefined)
|
|
97
|
-
return {};
|
|
98
|
-
const scan = refusedCommandScan(command, allowSet, resolved, verdict, declineEvidence);
|
|
99
|
-
if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict, scan))
|
|
100
|
-
return { mandated: true };
|
|
101
|
-
const named = classifyOutOfRootReadGate(command, allowSet, resolved);
|
|
102
|
-
if (named.gated || named.denied !== undefined)
|
|
103
|
-
return { mandated: true };
|
|
104
|
-
return declinedRung(scan);
|
|
105
|
-
}
|
|
106
|
-
function declinedRung(scan) {
|
|
107
|
-
return scan.declined ? { boundaryDeclined: true } : {};
|
|
108
|
-
}
|
|
109
|
-
function refusedCommandScan(command, allowSet, resolved, verdict, declineEvidence) {
|
|
110
|
-
return verdict.refusedWhole === true ? refusedCommandMayRead(command, allowSet, resolved, declineEvidence) : { mayRead: false, declined: false };
|
|
111
|
-
}
|
|
112
|
-
function boundaryUnjudged(verdict) {
|
|
113
|
-
return verdict.refusedWhole === true || (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0);
|
|
114
|
-
}
|
|
115
|
-
function unjudgedRead(command, allowSet, resolved, verdict, scan) {
|
|
116
|
-
if (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0)
|
|
117
|
-
return true;
|
|
118
|
-
if (verdict.refusedWhole !== true)
|
|
119
|
-
return false;
|
|
120
|
-
if (scan.mayRead)
|
|
121
|
-
return true;
|
|
122
|
-
if (resolved === undefined)
|
|
123
|
-
return false;
|
|
124
|
-
const named = classifyOutOfRootReadGate(command, allowSet, resolved);
|
|
125
|
-
return named.gated || named.denied !== undefined;
|
|
126
|
-
}
|
|
127
|
-
function recursiveReadCause(detailed) {
|
|
128
|
-
const recursive = detailed.recursiveReadPaths;
|
|
129
|
-
if (recursive === undefined || recursive.length === 0)
|
|
130
|
-
return undefined;
|
|
131
|
-
const recursiveSet = new Set(recursive);
|
|
132
|
-
const others = (detailed.undecidedPaths ?? []).filter((p) => !recursiveSet.has(p));
|
|
133
|
-
return {
|
|
134
|
-
code: RECURSIVE_READ_CAUSE_CODE,
|
|
135
|
-
roots: operandFamily(recursive),
|
|
136
|
-
...(others.length > 0 ? { further: operandFamily(others) } : {}),
|
|
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
|
-
}
|
|
189
|
-
export function bashReadBoundaryProbe(boundary) {
|
|
190
|
-
const allowSet = new Set(bashClassifyDefaultAllow());
|
|
191
|
-
const judge = (command, resolved, offers) => {
|
|
192
|
-
const seatVerdict = (verdict) => {
|
|
193
|
-
if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict, refusedCommandScan(command, allowSet, resolved, verdict, BOUNDARY_SEAT_DECLINE_EVIDENCE))) {
|
|
194
|
-
return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, verdict) };
|
|
195
|
-
}
|
|
196
|
-
if (boundaryUndecided(verdict)) {
|
|
197
|
-
return { reversible: false, ...probeAccountOf(command, allowSet, resolved, verdict) };
|
|
198
|
-
}
|
|
199
|
-
if (verdict.reason === undefined)
|
|
200
|
-
return { reversible: true };
|
|
201
|
-
return undefined;
|
|
202
|
-
};
|
|
203
|
-
const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
|
|
204
|
-
if (detailed.reason === undefined)
|
|
205
|
-
return seatVerdict(detailed) ?? { reversible: true };
|
|
206
|
-
const loop = classifyBoundedReadonlyPollLoopDetailed(command, allowSet, resolved);
|
|
207
|
-
if (loop.reason === undefined)
|
|
208
|
-
return seatVerdict(loop) ?? { reversible: true };
|
|
209
|
-
const compoundVerdict = seatVerdict(detailed);
|
|
210
|
-
const loopVerdict = seatVerdict(loop);
|
|
211
|
-
const mandated = [compoundVerdict, loopVerdict].find((verdict) => verdict?.mandated === true);
|
|
212
|
-
const offerFrom = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
|
|
213
|
-
if (mandated !== undefined)
|
|
214
|
-
return { ...mandated, ...offerFrom(detailed), ...offerFrom(loop) };
|
|
215
|
-
if (readBoundaryMandate(command, allowSet, resolved, detailed, BOUNDARY_SEAT_DECLINE_EVIDENCE).mandated === true)
|
|
216
|
-
return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, detailed) };
|
|
217
|
-
return compoundVerdict ?? loopVerdict ?? { reversible: true };
|
|
218
|
-
};
|
|
25
|
+
export function bashReversibilityProbe(allow, boundary) {
|
|
26
|
+
const allowSet = new Set(allow ?? bashClassifyDefaultAllow());
|
|
219
27
|
return (args) => {
|
|
220
28
|
const command = shellCommandOf(args);
|
|
221
29
|
if (command === undefined)
|
|
222
30
|
return { reversible: false };
|
|
223
|
-
|
|
224
|
-
|
|
31
|
+
if (isBackgroundShellCall(args))
|
|
32
|
+
return { reversible: false };
|
|
33
|
+
const home = typeof boundary === "function" ? boundary() : boundary;
|
|
34
|
+
const fold = home?.homeDir !== undefined ? { homeDir: home.homeDir } : undefined;
|
|
35
|
+
if (classifyCompoundReadonlyDetailed(command, allowSet, fold).reason === undefined)
|
|
36
|
+
return { reversible: true };
|
|
37
|
+
if (classifyBoundedReadonlyPollLoopDetailed(command, allowSet, fold).reason === undefined)
|
|
225
38
|
return { reversible: true };
|
|
226
|
-
return
|
|
39
|
+
return { reversible: false };
|
|
227
40
|
};
|
|
228
41
|
}
|
|
229
|
-
function grantClearsSeat(judge, command, resolved) {
|
|
230
|
-
const verdict = judge(command, resolved, true);
|
|
231
|
-
const dir = verdict.readRootCandidate?.dir;
|
|
232
|
-
if (dir === undefined)
|
|
233
|
-
return verdict;
|
|
234
|
-
if (judge(command, { ...resolved, roots: [...resolved.roots, dir] }, false).reversible)
|
|
235
|
-
return verdict;
|
|
236
|
-
const { readRootCandidate: _withdrawn, ...withoutOffer } = verdict;
|
|
237
|
-
return withoutOffer;
|
|
238
|
-
}
|
|
239
42
|
export { FULL_SHELL_CONTRACT_ID } from "../../core/tool-catalog-entries.js";
|
|
240
43
|
const EXIT1_INTERPRETATION = {
|
|
241
44
|
grep: "No matches found",
|
|
@@ -811,7 +614,6 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
811
614
|
const bgRetained = hasBackgroundShell(env) && env.backgroundCapabilities.retainBackgroundProcesses === true;
|
|
812
615
|
const bgSessionScoped = taskOpts.sessionId !== undefined && taskOpts.taskRegistry !== undefined;
|
|
813
616
|
const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
|
|
814
|
-
const canonicalEscape = createShellCanonicalRecheck(env, taskOpts.readBoundary);
|
|
815
617
|
return defineTool({
|
|
816
618
|
name: "Bash",
|
|
817
619
|
...toolFace("bash"),
|
|
@@ -847,9 +649,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
847
649
|
const timeoutRefusal = bashTimeoutArgRefusal(timeout, timeoutCaps);
|
|
848
650
|
if (timeoutRefusal !== undefined)
|
|
849
651
|
return errorResult(timeoutRefusal, { type: "bash_invalid_timeout", code: "bash_invalid_timeout" });
|
|
850
|
-
const
|
|
851
|
-
if (
|
|
852
|
-
return
|
|
652
|
+
const refused = await taskOpts.shellReadAdmission?.("Bash", ctx.toolCallId, command, ctx.signal);
|
|
653
|
+
if (refused !== undefined)
|
|
654
|
+
return refused;
|
|
853
655
|
if (run_in_background) {
|
|
854
656
|
if (!hasBackgroundShell(env)) {
|
|
855
657
|
return errorResult("Error (Bash): this environment does not support background processes (run_in_background).");
|
|
@@ -1037,110 +839,10 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
1037
839
|
},
|
|
1038
840
|
});
|
|
1039
841
|
}
|
|
1040
|
-
import { createCanonicalReadBoundary, NOTHING_ANSWERED } from "./shell-read-establish.js";
|
|
1041
|
-
export function createShellCanonicalRecheck(env, readBoundary, face) {
|
|
1042
|
-
const recheckReaders = new Set(bashClassifyDefaultAllow());
|
|
1043
|
-
let canonicalForRoots;
|
|
1044
|
-
const canonicalBoundaryFor = (roots) => {
|
|
1045
|
-
const key = roots.join("\u0000");
|
|
1046
|
-
if (canonicalForRoots?.key !== key)
|
|
1047
|
-
canonicalForRoots = { key, boundary: createCanonicalReadBoundary(env, roots) };
|
|
1048
|
-
return canonicalForRoots.boundary;
|
|
1049
|
-
};
|
|
1050
|
-
return async (command, signal) => {
|
|
1051
|
-
const resolvedBoundary = typeof readBoundary === "function" ? readBoundary() : readBoundary;
|
|
1052
|
-
if (resolvedBoundary === undefined)
|
|
1053
|
-
return undefined;
|
|
1054
|
-
const containmentAsked = resolvedBoundary.face !== "open";
|
|
1055
|
-
if (resolvedBoundary.denyMatch === undefined && !containmentAsked)
|
|
1056
|
-
return undefined;
|
|
1057
|
-
const faces = [
|
|
1058
|
-
classifyCompoundReadonlyDetailed(command, recheckReaders, resolvedBoundary),
|
|
1059
|
-
classifyBoundedReadonlyPollLoopDetailed(command, recheckReaders, resolvedBoundary),
|
|
1060
|
-
];
|
|
1061
|
-
const named = classifyOutOfRootReadGate(command, recheckReaders, resolvedBoundary);
|
|
1062
|
-
const literalAll = [
|
|
1063
|
-
...new Set([
|
|
1064
|
-
...faces.flatMap((v) => [...(v.checkedPaths ?? []), ...(v.checkedSpellings ?? []), ...(v.outOfRootPaths ?? [])]),
|
|
1065
|
-
...named.checked,
|
|
1066
|
-
...named.paths,
|
|
1067
|
-
]),
|
|
1068
|
-
];
|
|
1069
|
-
const folded = (ps) => new Set(ps.map((p) => normalizeAbsPathLexically(p)));
|
|
1070
|
-
const answered = {
|
|
1071
|
-
containment: folded([...faces.flatMap((v) => v.outOfRootPaths ?? []), ...named.paths]),
|
|
1072
|
-
deny: folded([...faces.flatMap((v) => (v.readDeniedPath !== undefined ? [v.readDeniedPath] : [])), ...(named.denied !== undefined ? [named.denied.path] : [])]),
|
|
1073
|
-
};
|
|
1074
|
-
const patterns = [...new Set([...faces.flatMap((v) => [...(v.undecidedPaths ?? []), ...(v.undecidedSpellings ?? [])]), ...named.undecided])];
|
|
1075
|
-
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);
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, ask, signal) {
|
|
1079
|
-
const resolved = await canonical.escapesAfterResolution(candidates, ask, signal);
|
|
1080
|
-
if (resolved.denied !== undefined) {
|
|
1081
|
-
return readDenyRefusal(resolved.denied.path, resolved.denied.pattern, face.tool);
|
|
1082
|
-
}
|
|
1083
|
-
if (resolved.unverifiable !== undefined) {
|
|
1084
|
-
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(", ")}. ` +
|
|
1085
|
-
face.note);
|
|
1086
|
-
}
|
|
1087
|
-
if (resolved.escaping.length > 0) {
|
|
1088
|
-
const quoted = resolved.escaping.map((p) => `"${p}"`).join(", ");
|
|
1089
|
-
return readContainmentRefusal(`Error (${face.tool}): ${face.subject} resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")}${face.verdictPhrase}. ` +
|
|
1090
|
-
face.note, resolved.escaping);
|
|
1091
|
-
}
|
|
1092
|
-
return undefined;
|
|
1093
|
-
}
|
|
1094
|
-
const DENY_REFUSAL_NOTE = "The read boundary judges this command's written spelling and, where a filesystem can be consulted, the " +
|
|
1095
|
-
"paths it would actually read; a deny-listed read is refused here rather than escalated.";
|
|
1096
|
-
function readContainmentRefusal(content, paths = []) {
|
|
1097
|
-
return errorResult(content, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths });
|
|
1098
|
-
}
|
|
1099
|
-
function readDenyRefusal(target, pattern, tool = "Bash") {
|
|
1100
|
-
const content = `Error (${tool}): reading "${target}" is refused: the target matches ${sensitivePathDenyList(pattern)}. ` +
|
|
1101
|
-
`${SENSITIVE_PATH_DENY_SCOPE} ${DENY_REFUSAL_NOTE}`;
|
|
1102
|
-
return errorResult(content, violationDetails({ code: "read_path_denied", message: content, target, pattern }));
|
|
1103
|
-
}
|
|
1104
|
-
const READONLY_FACE_ESCAPE_PROSE = {
|
|
1105
|
-
tool: "Bash",
|
|
1106
|
-
subject: "a path this command reads",
|
|
1107
|
-
verdictPhrase: ` ${NOT_AUTO_ALLOWED}`,
|
|
1108
|
-
note: BASH_READONLY_CONFINEMENT_NOTE,
|
|
1109
|
-
};
|
|
1110
|
-
const FULL_SHELL_FACE_ESCAPE_PROSE = {
|
|
1111
|
-
tool: "Bash",
|
|
1112
|
-
subject: "a path this command names",
|
|
1113
|
-
verdictPhrase: "",
|
|
1114
|
-
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.",
|
|
1115
|
-
};
|
|
1116
|
-
export const MONITOR_ESCAPE_PROSE = { ...FULL_SHELL_FACE_ESCAPE_PROSE, tool: "Monitor" };
|
|
1117
842
|
export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
1118
843
|
const timeoutCaps = resolveBashTimeoutCaps(opts);
|
|
1119
844
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
1120
845
|
const sample = [...allow].slice(0, 6).join(", ");
|
|
1121
|
-
const readRoots = [rootCanonical, ...(opts?.additionalRoots ?? [])];
|
|
1122
|
-
const denySeat = readDenySeat(opts?.readDeny);
|
|
1123
|
-
const readBoundary = { roots: readRoots, cwd: rootCanonical, ...denySeat };
|
|
1124
|
-
const overflowSpoolFence = createShellOverflowSpoolFence(env);
|
|
1125
|
-
const readsOnlyEngineOverflowSpool = async (verdict) => {
|
|
1126
|
-
if (verdict.outOfRootRead !== true)
|
|
1127
|
-
return false;
|
|
1128
|
-
if ((verdict.unresolvedOperands ?? []).length > 0)
|
|
1129
|
-
return false;
|
|
1130
|
-
const paths = verdict.outOfRootPaths ?? [];
|
|
1131
|
-
if (paths.length === 0)
|
|
1132
|
-
return false;
|
|
1133
|
-
for (const p of [...paths, ...(verdict.outOfRootSpellings ?? [])]) {
|
|
1134
|
-
const real = await canonicalBoundary.canonicalOf(p);
|
|
1135
|
-
for (const view of [real, p])
|
|
1136
|
-
if (view !== undefined && denySeat.denyMatch?.(normalizeAbsPathLexically(view)) != null)
|
|
1137
|
-
return false;
|
|
1138
|
-
if (!(await overflowSpoolFence(real ?? p)))
|
|
1139
|
-
return false;
|
|
1140
|
-
}
|
|
1141
|
-
return true;
|
|
1142
|
-
};
|
|
1143
|
-
const canonicalBoundary = createCanonicalReadBoundary(env, readRoots);
|
|
1144
846
|
return defineTool({
|
|
1145
847
|
name: "Bash",
|
|
1146
848
|
...toolFace("bash-readonly"),
|
|
@@ -1148,7 +850,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
|
1148
850
|
"operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
|
|
1149
851
|
"allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
|
|
1150
852
|
"refused, as is one that cannot be resolved statically (e.g. a `~` path). " +
|
|
1151
|
-
(boundaryIsTheSandbox(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to. ") +
|
|
853
|
+
(boundaryIsTheSandbox(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to or a recursive read traverses. ") +
|
|
1152
854
|
"Still subject to the deployment's approval policy.",
|
|
1153
855
|
parameters: Type.Object({
|
|
1154
856
|
command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
|
|
@@ -1162,32 +864,9 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
|
1162
864
|
const reason = coarseReadonlyCheck(command, allow);
|
|
1163
865
|
if (reason)
|
|
1164
866
|
return errorResult(`Error (Bash): ${reason}`);
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
return readDenyRefusal(boundary.readDeniedPath, boundary.readDeniedPattern);
|
|
1169
|
-
const traversal = boundary.recursiveReadPaths ?? [];
|
|
1170
|
-
if (traversal.length > 0) {
|
|
1171
|
-
const shown = traversal.slice(0, RECURSIVE_CAUSE_MAX_PATHS);
|
|
1172
|
-
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 — ` +
|
|
1173
|
-
`${traversal.length} path(s) taken as traversal roots: ${shown.join(", ")}${traversal.length > shown.length ? `, and ${traversal.length - shown.length} more` : ""}. ` +
|
|
1174
|
-
`Use the Grep tool to search a directory tree — it applies the same deny list to every file it reads. ` +
|
|
1175
|
-
`${BASH_READONLY_CONFINEMENT_NOTE}`);
|
|
1176
|
-
}
|
|
1177
|
-
if (boundary.reason !== undefined) {
|
|
1178
|
-
const rescueCandidates = [...(boundary.outOfRootPaths ?? []), ...(boundary.outOfRootSpellings ?? [])];
|
|
1179
|
-
const rescued = boundary.outOfRootRead === true &&
|
|
1180
|
-
(boundary.unresolvedOperands ?? []).length === 0 &&
|
|
1181
|
-
(await canonicalBoundary.allResolveInside(rescueCandidates, ctx.signal));
|
|
1182
|
-
if (rescued)
|
|
1183
|
-
releasedByGrant = rescueCandidates;
|
|
1184
|
-
if (!rescued && !(await readsOnlyEngineOverflowSpool(boundary))) {
|
|
1185
|
-
return readContainmentRefusal(`Error (Bash): ${boundary.reason}. ${BASH_READONLY_CONFINEMENT_NOTE}`, boundary.outOfRootPaths ?? []);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
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);
|
|
1189
|
-
if (escape !== undefined)
|
|
1190
|
-
return escape;
|
|
867
|
+
const refused = await opts?.shellReadAdmission?.("Bash", ctx.toolCallId, command, ctx.signal);
|
|
868
|
+
if (refused !== undefined)
|
|
869
|
+
return refused;
|
|
1191
870
|
const readOnlyTick = opts?.onToolProgress === undefined ? undefined : (t) => describeSafely(opts.onToolProgress, { toolCallId: ctx.toolCallId, toolName: "Bash", ...t });
|
|
1192
871
|
return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, true, undefined, readOnlyTick);
|
|
1193
872
|
},
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export * from "./fs-search-tools.js";
|
|
|
11
11
|
export * from "./bash-readonly-classifier.js";
|
|
12
12
|
export * from "./fs-bash.js";
|
|
13
13
|
export * from "./read-deny.js";
|
|
14
|
-
import {
|
|
14
|
+
import type { ShellReadAdmission } from "./shell-read-check.js";
|
|
15
15
|
export { BASH_CLASSIFY_EXTRA_ALLOW, bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
16
16
|
import { type ReadDenyEntry } from "./read-deny.js";
|
|
17
17
|
import { type ContainmentFace } from "./containment-face.js";
|
|
@@ -42,19 +42,18 @@ export interface HandsToolkitOptions {
|
|
|
42
42
|
readOnly?: boolean;
|
|
43
43
|
/** Override the `bash_readonly` command allowlist (default {@link BASH_READONLY_DEFAULT_ALLOW}). */
|
|
44
44
|
bashReadonlyAllow?: readonly string[];
|
|
45
|
-
/** The
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
45
|
+
/** The gate's shell read station's pre-spawn ADMISSION (`ShellReadAdmission`, shell-read-check.ts), handed to
|
|
46
|
+
* BOTH shell faces this band mounts: what a shell tool runs is what the station judged before approval, and
|
|
47
|
+
* the admission re-establishes the same candidates right before the spawn. The station itself — ONE parse of
|
|
48
|
+
* the command, its candidates established on the filesystem, judged by the read deny seat and the read
|
|
49
|
+
* roots — is the gate's (`ToolGateInput.shellReadCheck`, built by the Runner's hands phase for every shell
|
|
50
|
+
* face and every doctrine); this band judges nothing on its own.
|
|
51
51
|
*
|
|
52
|
-
* ABSENT ⇒
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
shellReadBoundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined);
|
|
52
|
+
* ABSENT ⇒ no station judged this mount (a library-direct toolkit with no gate): the full shell runs
|
|
53
|
+
* unconfined as it always did, and `bash_readonly` confines NOTHING — its containment IS the station, so a
|
|
54
|
+
* consumer mounting the read-only band outside the Runner must run `runToolGate` with a station of its own
|
|
55
|
+
* (`createShellReadStation`). */
|
|
56
|
+
shellReadAdmission?: ShellReadAdmission;
|
|
58
57
|
/** Commit-message attribution trailer for the `bash` git protocol. Default NONE — attribution is a
|
|
59
58
|
* deployment identity asset, so the branded deployment sets it; pass `"Name <email>"` to advertise
|
|
60
59
|
* one, `false` (≡ unset) for no trailer section. */
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export * from "./fs-search-tools.js";
|
|
|
11
11
|
export * from "./bash-readonly-classifier.js";
|
|
12
12
|
export * from "./fs-bash.js";
|
|
13
13
|
export * from "./read-deny.js";
|
|
14
|
-
import { BASH_READONLY_DEFAULT_ALLOW
|
|
14
|
+
import { BASH_READONLY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
|
|
15
15
|
export { BASH_CLASSIFY_EXTRA_ALLOW, bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
16
16
|
import { compileReadDeny } from "./read-deny.js";
|
|
17
17
|
import { deploymentFaceClampNotice, resolveContainmentFace } from "./containment-face.js";
|
|
@@ -71,8 +71,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
71
71
|
...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
|
|
72
72
|
...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
|
|
73
73
|
...(opts.onNotice !== undefined ? { onNotice: opts.onNotice } : {}),
|
|
74
|
-
...(
|
|
75
|
-
readDeny,
|
|
74
|
+
...(opts.shellReadAdmission !== undefined ? { shellReadAdmission: opts.shellReadAdmission } : {}),
|
|
76
75
|
...(opts.onToolProgress !== undefined ? { onToolProgress: opts.onToolProgress } : {}),
|
|
77
76
|
})
|
|
78
77
|
: createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
|
|
@@ -87,7 +86,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
87
86
|
...(opts.queuedHumanInput !== undefined ? { queuedHumanInput: opts.queuedHumanInput } : {}),
|
|
88
87
|
...(opts.oneShot !== undefined ? { oneShot: opts.oneShot } : {}),
|
|
89
88
|
...(additionalRoots !== undefined ? { additionalRoots } : {}),
|
|
90
|
-
...(opts.
|
|
89
|
+
...(opts.shellReadAdmission !== undefined ? { shellReadAdmission: opts.shellReadAdmission } : {}),
|
|
91
90
|
...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
|
|
92
91
|
...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
|
|
93
92
|
...(opts.onNotice !== undefined ? { onNotice: opts.onNotice } : {}),
|
|
@@ -89,6 +89,31 @@ export declare function readDenySeat(matcher: ReadDenyMatcher | undefined): Read
|
|
|
89
89
|
export declare function sensitivePathDenyList(pattern: string | readonly string[]): string;
|
|
90
90
|
/** The deny table's REACH: a deny row holds INSIDE the containment roots, so no root grant answers it. */
|
|
91
91
|
export declare const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots.";
|
|
92
|
+
/**
|
|
93
|
+
* THE deny refusal sentence — five segments, one mint, every read face: the ACTION, the ABSOLUTE target as the
|
|
94
|
+
* caller spelled it (with its canonical target in parentheses when the two differ), the DISPOSITION (a deny row is
|
|
95
|
+
* refused, never escalated), the table BY NAME with the PATTERN verbatim ({@link sensitivePathDenyList}), and
|
|
96
|
+
* one sentence of why ({@link SENSITIVE_PATH_DENY_SCOPE}). `matchedView` names the view the pattern actually hit
|
|
97
|
+
* when that is the requested SPELLING rather than the target — under a symlink or case alias the two strings
|
|
98
|
+
* differ, only one matches, and naming the other sends the reader chasing a path the pattern does not match.
|
|
99
|
+
* A face prefixes its own `Error (<tool>): ` and may append its own closing clause; the finding itself is
|
|
100
|
+
* never re-spelled.
|
|
101
|
+
*/
|
|
102
|
+
export declare function readDenySentence(finding: {
|
|
103
|
+
readonly action?: string;
|
|
104
|
+
readonly target: string;
|
|
105
|
+
readonly canonical?: string;
|
|
106
|
+
readonly pattern: string;
|
|
107
|
+
readonly matchedView?: string;
|
|
108
|
+
}): string;
|
|
109
|
+
/** The per-part budget of the deny sentence — the gate's own card-account cap, not a second number. */
|
|
110
|
+
export declare const READ_DENY_SENTENCE_PART_MAX = 200;
|
|
111
|
+
/** `s` verbatim when it fits `budget`, else elided in its MIDDLE ({@link elideMiddle}). */
|
|
112
|
+
export declare function fitMiddle(s: string, budget: number): string;
|
|
113
|
+
/** `s` shortened to `budget` by removing its MIDDLE, marked — cut through the repo's surrogate-safe
|
|
114
|
+
* primitive, never a raw `slice` (a raw cut leaves half an astral code point on a card and in a durable
|
|
115
|
+
* row). Under a dozen characters the HEAD is taken. */
|
|
116
|
+
export declare function elideMiddle(s: string, budget: number): string;
|
|
92
117
|
/**
|
|
93
118
|
* The closed set of built-in tiers. A tier is the deployment's selection unit
|
|
94
119
|
* ({@link ReadDenyBuiltinConfig.tiers}); every {@link READ_FACE_BUILTIN_DENY_TABLE} row belongs to
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sliceHeadSafe, sliceTailSafe } from "../../core/surrogate-safe-slice.js";
|
|
2
|
+
import { PROBE_REASON_MAX } from "../../core/untrusted-text.js";
|
|
1
3
|
export function readDenySeat(matcher) {
|
|
2
4
|
const entries = (matcher?.entries ?? []).map((e) => e.pattern);
|
|
3
5
|
if (matcher === undefined || entries.length === 0)
|
|
@@ -9,6 +11,23 @@ export function sensitivePathDenyList(pattern) {
|
|
|
9
11
|
return `the sensitive-path read deny list (pattern${rows.length === 1 ? "" : "s"} ${rows.map((p) => `"${p}"`).join(", ")})`;
|
|
10
12
|
}
|
|
11
13
|
export const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots.";
|
|
14
|
+
export function readDenySentence(finding) {
|
|
15
|
+
const canonicalNote = finding.canonical !== undefined && finding.canonical !== finding.target ? `(canonical target: "${fitMiddle(finding.canonical, READ_DENY_SENTENCE_PART_MAX)}") ` : "";
|
|
16
|
+
const lexicalHit = finding.matchedView !== undefined && finding.matchedView !== (finding.canonical ?? finding.target);
|
|
17
|
+
const judged = lexicalHit ? `the requested path's spelling ("${fitMiddle(finding.matchedView, READ_DENY_SENTENCE_PART_MAX)}") matches` : "the target matches";
|
|
18
|
+
return `${finding.action ?? "reading"} "${fitMiddle(finding.target, READ_DENY_SENTENCE_PART_MAX)}" ${canonicalNote}is refused: ${judged} ${sensitivePathDenyList(fitMiddle(finding.pattern, READ_DENY_SENTENCE_PART_MAX))}. ${SENSITIVE_PATH_DENY_SCOPE}`;
|
|
19
|
+
}
|
|
20
|
+
export const READ_DENY_SENTENCE_PART_MAX = PROBE_REASON_MAX;
|
|
21
|
+
export function fitMiddle(s, budget) {
|
|
22
|
+
return s.length <= budget ? s : elideMiddle(s, budget);
|
|
23
|
+
}
|
|
24
|
+
export function elideMiddle(s, budget) {
|
|
25
|
+
if (budget < 12)
|
|
26
|
+
return sliceHeadSafe(s, Math.max(0, budget));
|
|
27
|
+
const keep = budget - 1;
|
|
28
|
+
const head = Math.ceil(keep / 2);
|
|
29
|
+
return `${sliceHeadSafe(s, head)}\u2026${sliceTailSafe(s, keep - head)}`;
|
|
30
|
+
}
|
|
12
31
|
export const READ_DENY_BUILTIN_TIERS = ["credentials", "shell-history", "browser", "wallet", "agent-config"];
|
|
13
32
|
export const READ_FACE_BUILTIN_DENY_TABLE = [
|
|
14
33
|
{ pattern: ".ssh", tier: "credentials" },
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { readDenySentence } from "./read-deny.js";
|
|
3
3
|
import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
|
|
4
4
|
import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
|
|
5
5
|
import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
|
|
@@ -461,15 +461,11 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
|
|
|
461
461
|
aliasResolved: canon.aliasResolved === true,
|
|
462
462
|
});
|
|
463
463
|
if (hit !== null) {
|
|
464
|
-
const lexicalHit = hit.matchedView !== undefined && hit.matchedView !== key;
|
|
465
|
-
const judged = lexicalHit
|
|
466
|
-
? `the requested path's spelling ("${hit.matchedView}") matches`
|
|
467
|
-
: `the target matches`;
|
|
468
464
|
return {
|
|
469
465
|
ok: false,
|
|
470
466
|
violation: {
|
|
471
467
|
code: "read_path_denied",
|
|
472
|
-
message:
|
|
468
|
+
message: readDenySentence({ target: path, canonical: key, pattern: hit.pattern, ...(hit.matchedView !== undefined ? { matchedView: hit.matchedView } : {}) }),
|
|
473
469
|
target: key,
|
|
474
470
|
pattern: hit.pattern,
|
|
475
471
|
},
|