@sema-agent/core 7.24.4 → 7.25.1
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 +100 -0
- 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/arg-summary.d.ts +0 -7
- package/dist/core/arg-summary.js +2 -1
- 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/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 +14 -4
- 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 +26 -0
- 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 +3 -4
- package/dist/core/runner/session-rule-policy.js +11 -3
- 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-policy.js +30 -23
- 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/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-lexer.d.ts +13 -0
- package/dist/tools/fs/bash-lexer.js +61 -0
- package/dist/tools/fs/bash-program-position.d.ts +36 -29
- package/dist/tools/fs/bash-program-position.js +33 -13
- package/dist/tools/fs/bash-readonly-classifier.d.ts +26 -313
- package/dist/tools/fs/bash-readonly-classifier.js +81 -782
- 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 +88 -27
- 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.25.0.json +671 -0
- package/wire-changes/core@7.25.1.json +64 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRunsShell, shellScanDeclineOf, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
|
|
1
|
+
import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf } from "./safety.js";
|
|
2
|
+
import { lexShellText, literalTildeSpelling, ansiCQuoteClose, dollarOpensQuote, quoteMask, wordsOf, } from "./bash-lexer.js";
|
|
3
|
+
import { COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRefusalSentence, ASSIGNMENT_PREFIX, readProgramPosition, writtenAssignmentPrefix, } from "./bash-program-position.js";
|
|
5
4
|
export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
6
5
|
"ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
|
|
7
6
|
"grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
|
|
@@ -154,55 +153,12 @@ function stripExemptRedirections(command) {
|
|
|
154
153
|
flush();
|
|
155
154
|
return out;
|
|
156
155
|
}
|
|
157
|
-
const POLL_LOOP_CAUSE_MAX_PATHS = 3;
|
|
158
156
|
export function formatOutOfRootReadApprovalOption(directory) {
|
|
159
157
|
const trimmed = directory.replace(/[/\\]+$/, "");
|
|
160
158
|
const cut = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
|
|
161
159
|
const leaf = cut >= 0 ? trimmed.slice(cut + 1) : trimmed;
|
|
162
160
|
return `Yes, allow reading from ${leaf || directory}/ from this project`;
|
|
163
161
|
}
|
|
164
|
-
function parentDirLexically(p) {
|
|
165
|
-
return normalizeAbsPathLexically(`${p}/..`);
|
|
166
|
-
}
|
|
167
|
-
export function deepestCommonDir(a, b) {
|
|
168
|
-
if (a === b)
|
|
169
|
-
return a;
|
|
170
|
-
const shorter = Math.min(a.length, b.length);
|
|
171
|
-
let end = 0;
|
|
172
|
-
let i = 0;
|
|
173
|
-
while (i < shorter && a[i] === b[i]) {
|
|
174
|
-
if (a[i] === "/" || a[i] === "\\")
|
|
175
|
-
end = i;
|
|
176
|
-
i++;
|
|
177
|
-
}
|
|
178
|
-
if (i === shorter && (a[shorter] === "/" || a[shorter] === "\\" || b[shorter] === "/" || b[shorter] === "\\"))
|
|
179
|
-
end = shorter;
|
|
180
|
-
const cut = a.slice(0, end);
|
|
181
|
-
const candidate = cut.includes("/") || cut.includes("\\") ? cut : a.slice(0, end + 1);
|
|
182
|
-
if (candidate.length === 0 || !withinAnyRoot([candidate], a) || !withinAnyRoot([candidate], b))
|
|
183
|
-
return undefined;
|
|
184
|
-
return candidate;
|
|
185
|
-
}
|
|
186
|
-
function outOfRootGrantCandidateOf(paths, boundary) {
|
|
187
|
-
if (paths.length === 0)
|
|
188
|
-
return undefined;
|
|
189
|
-
if (paths.some(hasWin32AliasedComponent))
|
|
190
|
-
return undefined;
|
|
191
|
-
let candidate = parentDirLexically(paths[0]);
|
|
192
|
-
for (const path of paths.slice(1)) {
|
|
193
|
-
if (candidate === undefined)
|
|
194
|
-
return undefined;
|
|
195
|
-
candidate = deepestCommonDir(candidate, parentDirLexically(path));
|
|
196
|
-
}
|
|
197
|
-
if (candidate === undefined)
|
|
198
|
-
return undefined;
|
|
199
|
-
const tooWide = parentDirLexically(candidate) === candidate ||
|
|
200
|
-
(boundary.homeDir !== undefined && withinAnyRoot([candidate], boundary.homeDir)) ||
|
|
201
|
-
boundary.roots.some((root) => withinAnyRoot([candidate], root));
|
|
202
|
-
if (tooWide)
|
|
203
|
-
return paths.length === 1 ? { path: paths[0], exact: true } : undefined;
|
|
204
|
-
return { path: candidate };
|
|
205
|
-
}
|
|
206
162
|
export const NO_PATH_OPERAND_COMMANDS = new Set([
|
|
207
163
|
"pwd", "echo", "printf", "whoami", "uname", "which", "tr", "basename", "dirname",
|
|
208
164
|
"cal", "uptime", "id", "groups", "nproc", "locale", "free", "true", "false", "seq", "expr", "type", "sleep",
|
|
@@ -243,193 +199,7 @@ function spellingsOf(absolute) {
|
|
|
243
199
|
function reheadSegment(toks, head, from = 0) {
|
|
244
200
|
return { folded: [head, ...toks.folded.slice(from + 1)], raw: [head, ...toks.raw.slice(from + 1)] };
|
|
245
201
|
}
|
|
246
|
-
function tokensMoveBase(toks) {
|
|
247
|
-
const pos = readProgramPosition(toks);
|
|
248
|
-
if (pos.written === undefined)
|
|
249
|
-
return false;
|
|
250
|
-
const written = toks.folded[pos.written];
|
|
251
|
-
if (toks.raw[pos.written] !== written)
|
|
252
|
-
return true;
|
|
253
|
-
if (pos.assignments.length > 0)
|
|
254
|
-
return false;
|
|
255
|
-
if (pos.launchers.length === 0) {
|
|
256
|
-
if (written.startsWith("-") && written !== "-")
|
|
257
|
-
return true;
|
|
258
|
-
return written === "cd";
|
|
259
|
-
}
|
|
260
|
-
if (!COMMAND_LAUNCHERS.has(written))
|
|
261
|
-
return false;
|
|
262
|
-
return pos.refusal !== undefined || pos.program?.basename === "cd";
|
|
263
|
-
}
|
|
264
202
|
export const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
265
|
-
export function refusedCommandMayRead(command, allow, boundary, declinesAreEvidence) {
|
|
266
|
-
const scan = { declined: false };
|
|
267
|
-
const exempt = refusedCommandExempt(command, allow, boundary, declinesAreEvidence, scan, 0);
|
|
268
|
-
return { mayRead: !exempt, declined: scan.declined };
|
|
269
|
-
}
|
|
270
|
-
const STDIN_ONLY_READER_FLAGS = new Map([
|
|
271
|
-
["cat", /^-[AbeEnstTuv]+$/],
|
|
272
|
-
["wc", /^-[clmwL]+$/],
|
|
273
|
-
]);
|
|
274
|
-
function whitelistSegment(segment) {
|
|
275
|
-
const tokens = [...segment.tokens];
|
|
276
|
-
for (let k = 0; k + 2 < tokens.length; k++) {
|
|
277
|
-
const [a, b, c] = [tokens[k], tokens[k + 1], tokens[k + 2]];
|
|
278
|
-
if (a.kind === "word" && b.kind === "group" && b.op === "(" && c.kind === "group" && c.op === ")" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(a.folded))
|
|
279
|
-
tokens.splice(k, 3);
|
|
280
|
-
}
|
|
281
|
-
const words = tokens.filter((t) => t.kind === "word" && !(t.raw === t.folded && (t.folded === "{" || t.folded === "}")));
|
|
282
|
-
let from = 0;
|
|
283
|
-
while (from < words.length && SHELL_BLOCK_WORDS.has(words[from].folded))
|
|
284
|
-
from++;
|
|
285
|
-
return {
|
|
286
|
-
words: { folded: words.slice(from).map((w) => w.folded), raw: words.slice(from).map((w) => w.raw) },
|
|
287
|
-
redirects: tokens.filter((t) => t.kind === "redirect"),
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, scan, depth, baseMovedOutside = false) {
|
|
291
|
-
if (depth > 4)
|
|
292
|
-
return false;
|
|
293
|
-
const lexed = lexShellText(command, { mode: "lenient" });
|
|
294
|
-
if ("reject" in lexed)
|
|
295
|
-
return false;
|
|
296
|
-
const bodies = [...lexed.substitutions];
|
|
297
|
-
const segments = [];
|
|
298
|
-
for (const segment of lexed.segments) {
|
|
299
|
-
for (const t of segment.tokens) {
|
|
300
|
-
if (t.kind !== "heredoc" || t.quoted)
|
|
301
|
-
continue;
|
|
302
|
-
const inner = substitutionsInHeredocBody(t.body);
|
|
303
|
-
if (inner === undefined)
|
|
304
|
-
return false;
|
|
305
|
-
for (const body of inner)
|
|
306
|
-
bodies.push(body);
|
|
307
|
-
}
|
|
308
|
-
segments.push(whitelistSegment(segment));
|
|
309
|
-
}
|
|
310
|
-
let declined;
|
|
311
|
-
for (const { words } of segments) {
|
|
312
|
-
const pos = readProgramPosition(words);
|
|
313
|
-
if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE))
|
|
314
|
-
return false;
|
|
315
|
-
const decline = shellScanDeclineOf(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED);
|
|
316
|
-
if (decline !== undefined)
|
|
317
|
-
scan.declined = true;
|
|
318
|
-
if (decline !== undefined && declinesAreEvidence.has(decline))
|
|
319
|
-
return false;
|
|
320
|
-
declined ??= decline;
|
|
321
|
-
}
|
|
322
|
-
if (declined !== undefined)
|
|
323
|
-
return true;
|
|
324
|
-
const baseMoved = baseMovedOutside ||
|
|
325
|
-
segments.some(({ words }) => {
|
|
326
|
-
const runs = readProgramPosition(words).program?.basename;
|
|
327
|
-
return runs === "cd" || (runs !== undefined && UNMODELLED_BASE_MOVERS.has(runs));
|
|
328
|
-
});
|
|
329
|
-
for (const body of bodies) {
|
|
330
|
-
const walked = classifyCompoundReadonlyDetailed(body, allow, boundary);
|
|
331
|
-
const fullyGreen = !baseMoved && walked.reason === undefined && walked.readDenied !== true && walked.outOfRootRead !== true && walked.refusedWhole !== true &&
|
|
332
|
-
(walked.undecidedPaths?.length ?? 0) === 0 && (walked.unresolvedOperands?.length ?? 0) === 0 && (walked.recursiveReadPaths?.length ?? 0) === 0;
|
|
333
|
-
if (fullyGreen)
|
|
334
|
-
continue;
|
|
335
|
-
if (!refusedCommandExempt(body, allow, boundary, declinesAreEvidence, scan, depth + 1, baseMoved))
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
const stdinFileJudgedInside = (word) => {
|
|
339
|
-
if (word === undefined || word === "" || word.includes(SUBSTITUTION_PLACEHOLDER) || UNREADABLE_WHITELIST_WORD.test(word))
|
|
340
|
-
return false;
|
|
341
|
-
if (boundary === undefined || baseMoved)
|
|
342
|
-
return false;
|
|
343
|
-
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], word, boundary.homeDir);
|
|
344
|
-
if (resolved === undefined || boundary.denyMatch?.(resolved) != null)
|
|
345
|
-
return false;
|
|
346
|
-
return boundary.face === "open" || withinAnyRoot(boundary.roots, resolved);
|
|
347
|
-
};
|
|
348
|
-
const unlistedSegmentJudgedInside = (words, from, leadingAssignments = []) => {
|
|
349
|
-
if (boundary === undefined)
|
|
350
|
-
return true;
|
|
351
|
-
const assignmentJudgedInside = (word) => {
|
|
352
|
-
const payload = assignmentWordPayload(word);
|
|
353
|
-
return payload === undefined || (payload.kind === "value" && stdinFileJudgedInside(payload.value));
|
|
354
|
-
};
|
|
355
|
-
for (const k of leadingAssignments) {
|
|
356
|
-
if (k >= from)
|
|
357
|
-
continue;
|
|
358
|
-
if (!assignmentJudgedInside(words.folded[k]))
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
const argv = argvProgramOf(words);
|
|
362
|
-
const argvStart = argv?.from ?? words.folded.length;
|
|
363
|
-
const declaredNotPaths = ASSIGNMENT_VALUE_NOT_PATHS.has(argv?.program ?? "");
|
|
364
|
-
let endOfOptions = false;
|
|
365
|
-
for (let j = from; j < words.folded.length; j++) {
|
|
366
|
-
const word = words.folded[j];
|
|
367
|
-
if (j > from && !endOfOptions && word === "--") {
|
|
368
|
-
endOfOptions = true;
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
if (j === from && !isPathShapedToken(word)) {
|
|
372
|
-
if (UNREADABLE_WHITELIST_WORD.test(word) || word.includes(SUBSTITUTION_PLACEHOLDER))
|
|
373
|
-
return false;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
const optionShaped = !endOfOptions && word.startsWith("-") && word !== "-";
|
|
377
|
-
const candidates = optionShaped ? [...attachedOptionPayloads(word), ...(isPathShapedToken(word) ? [word] : [])] : [word];
|
|
378
|
-
for (const candidate of candidates) {
|
|
379
|
-
if (!stdinFileJudgedInside(candidate))
|
|
380
|
-
return false;
|
|
381
|
-
}
|
|
382
|
-
if (!optionShaped && (j < argvStart || !declaredNotPaths) && !assignmentJudgedInside(word))
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
return true;
|
|
386
|
-
};
|
|
387
|
-
for (const { words, redirects } of segments) {
|
|
388
|
-
for (const r of redirects)
|
|
389
|
-
if (r.op === "<" && !stdinFileJudgedInside(r.target?.folded))
|
|
390
|
-
return false;
|
|
391
|
-
const pos = readProgramPosition(words);
|
|
392
|
-
for (const k of words.folded.keys()) {
|
|
393
|
-
const word = words.folded[k];
|
|
394
|
-
if (ASSIGNMENT_WORD.test(word) && /\$\(|`/.test(word) && (pos.assignments.includes(k) || pos.launchers.some((l) => l.index < k)))
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
const program = pos.program;
|
|
398
|
-
const listed = (name) => allow.has(name) || name === "cd";
|
|
399
|
-
if (program === undefined) {
|
|
400
|
-
if (pos.launchers.length === 0)
|
|
401
|
-
continue;
|
|
402
|
-
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
|
|
403
|
-
return false;
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
if (program.name.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(program.name))
|
|
407
|
-
return false;
|
|
408
|
-
if (pos.launchers.length > 0 && !listed(program.basename)) {
|
|
409
|
-
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
|
|
410
|
-
return false;
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
const head = program.basename;
|
|
414
|
-
if (head === "cd" || UNMODELLED_BASE_MOVERS.has(head))
|
|
415
|
-
continue;
|
|
416
|
-
if (!allow.has(head)) {
|
|
417
|
-
if (!unlistedSegmentJudgedInside(words, program.index, pos.assignments))
|
|
418
|
-
return false;
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (NO_PATH_OPERAND_COMMANDS.has(head))
|
|
422
|
-
continue;
|
|
423
|
-
const flags = STDIN_ONLY_READER_FLAGS.get(head);
|
|
424
|
-
if (flags === undefined)
|
|
425
|
-
return false;
|
|
426
|
-
for (let j = program.index + 1; j < words.folded.length; j++) {
|
|
427
|
-
if (!flags.test(words.folded[j]))
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return true;
|
|
432
|
-
}
|
|
433
203
|
function tokenizeSegment(segment) {
|
|
434
204
|
const lexed = lexShellText(segment, { mode: "strict", connectors: "text" });
|
|
435
205
|
if ("reject" in lexed)
|
|
@@ -438,7 +208,21 @@ function tokenizeSegment(segment) {
|
|
|
438
208
|
}
|
|
439
209
|
export function hasUnquotedExpansionMetachar(rawToken) {
|
|
440
210
|
let open;
|
|
441
|
-
for (
|
|
211
|
+
for (let i = 0; i < rawToken.length; i++) {
|
|
212
|
+
const ch = rawToken[i];
|
|
213
|
+
const opener = rawToken[i + 1];
|
|
214
|
+
if (open === undefined && ch === "$" && dollarOpensQuote(rawToken, i)) {
|
|
215
|
+
if (opener === '"') {
|
|
216
|
+
open = opener;
|
|
217
|
+
i += 1;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const close = ansiCQuoteClose(rawToken, i + 1);
|
|
221
|
+
if (close === undefined)
|
|
222
|
+
return true;
|
|
223
|
+
i = close;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
442
226
|
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
443
227
|
open = ch;
|
|
444
228
|
continue;
|
|
@@ -922,7 +706,7 @@ function sedReadingKind(reading) {
|
|
|
922
706
|
}
|
|
923
707
|
function readSedScript(script) {
|
|
924
708
|
if (script.split("\n").some((line) => utf8Length(line) >= SED_SCRIPT_LINE_LIMIT))
|
|
925
|
-
return "
|
|
709
|
+
return "oversized";
|
|
926
710
|
let i = 0;
|
|
927
711
|
let depth = 0;
|
|
928
712
|
const len = script.length;
|
|
@@ -1231,8 +1015,10 @@ export function sedScriptSlotFiles(args) {
|
|
|
1231
1015
|
const writes = new Set();
|
|
1232
1016
|
const commands = new Set();
|
|
1233
1017
|
let executesPatternSpace = false;
|
|
1018
|
+
const kinds = [];
|
|
1234
1019
|
for (const dialect of SED_OPTION_DIALECTS) {
|
|
1235
1020
|
const reading = readSedInvocationDetailed(args, dialect);
|
|
1021
|
+
kinds.push(sedReadingKind(reading));
|
|
1236
1022
|
if (typeof reading === "string")
|
|
1237
1023
|
continue;
|
|
1238
1024
|
for (const f of reading.names.reads)
|
|
@@ -1243,12 +1029,17 @@ export function sedScriptSlotFiles(args) {
|
|
|
1243
1029
|
commands.add(c);
|
|
1244
1030
|
executesPatternSpace ||= reading.names.executesPatternSpace;
|
|
1245
1031
|
}
|
|
1246
|
-
|
|
1032
|
+
const unread = kinds.includes("oversized")
|
|
1033
|
+
? "a script line is longer than an implementation compiles whole, so what runs is not the text written here"
|
|
1034
|
+
: kinds.every((k) => k === "unparsed")
|
|
1035
|
+
? "its script could not be read to its end"
|
|
1036
|
+
: undefined;
|
|
1037
|
+
return { reads: [...reads], writes: [...writes], commands: [...commands], executesPatternSpace, ...(unread !== undefined ? { unread } : {}) };
|
|
1247
1038
|
}
|
|
1248
1039
|
const SCRIPT_SLOT_VERBS = new Map([
|
|
1249
1040
|
["sed", (args) => {
|
|
1250
1041
|
const readings = SED_OPTION_DIALECTS.map((dialect) => readSedInvocation(args, dialect));
|
|
1251
|
-
if (readings.includes("names") || readings.includes("unseen"))
|
|
1042
|
+
if (readings.includes("names") || readings.includes("unseen") || readings.includes("oversized"))
|
|
1252
1043
|
return true;
|
|
1253
1044
|
return readings.every((reading) => reading === "unparsed");
|
|
1254
1045
|
}],
|
|
@@ -1667,28 +1458,51 @@ function isGrepFileStdinLongOption(tok) {
|
|
|
1667
1458
|
return name !== undefined && isLongOptionAbbrevOf(name, "file");
|
|
1668
1459
|
}
|
|
1669
1460
|
const CLUSTER_OPTION_CHAR = /[A-Za-z0-9]/;
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1461
|
+
function tildeExpandsInValue(rawValue) {
|
|
1462
|
+
if (rawValue === undefined)
|
|
1463
|
+
return false;
|
|
1464
|
+
for (let at = 0; at <= rawValue.length;) {
|
|
1465
|
+
if (rawValue[at] === "~") {
|
|
1466
|
+
let quoted = false;
|
|
1467
|
+
for (let k = at + 1; k < rawValue.length && rawValue[k] !== "/" && rawValue[k] !== ":"; k++) {
|
|
1468
|
+
if (rawValue[k] === "'" || rawValue[k] === '"' || rawValue[k] === "\\") {
|
|
1469
|
+
quoted = true;
|
|
1470
|
+
break;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (!quoted)
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
const next = rawValue.indexOf(":", at);
|
|
1477
|
+
if (next < 0)
|
|
1478
|
+
return false;
|
|
1479
|
+
at = next + 1;
|
|
1480
|
+
}
|
|
1481
|
+
return false;
|
|
1482
|
+
}
|
|
1483
|
+
export function assignmentWordPayload(word, raw = word) {
|
|
1484
|
+
const written = writtenAssignmentPrefix(word);
|
|
1485
|
+
if (written === undefined)
|
|
1673
1486
|
return undefined;
|
|
1674
|
-
const eq =
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
if (
|
|
1487
|
+
const eq = written.prefix.length - 1;
|
|
1488
|
+
const spelled = word.slice(eq + 1);
|
|
1489
|
+
const expands = tildeExpandsInValue(raw.startsWith(written.prefix) ? raw.slice(written.prefix.length) : undefined);
|
|
1490
|
+
const value = expands ? spelled : literalTildeSpelling(spelled);
|
|
1491
|
+
if (written.appends) {
|
|
1492
|
+
const reason = `"${word}" appends to a value the text does not contain`;
|
|
1493
|
+
return spelled === "" || expands ? { kind: "unresolved", reason } : { kind: "unresolved", reason, value };
|
|
1494
|
+
}
|
|
1495
|
+
if (expands)
|
|
1679
1496
|
return { kind: "unresolved", reason: `"${word}" carries a tilde whose expansion the text does not settle` };
|
|
1680
|
-
return
|
|
1497
|
+
return spelled === "" ? undefined : { kind: "value", value };
|
|
1681
1498
|
}
|
|
1682
1499
|
export const ASSIGNMENT_VALUE_NOT_PATHS = new Set(["make", "gmake", "bmake", "cmake", "ninja", "meson", "docker", "podman"]);
|
|
1683
1500
|
export function argvProgramOf(tokens) {
|
|
1684
|
-
|
|
1685
|
-
while (lead < tokens.folded.length && ASSIGNMENT_OR_APPEND_WORD.test(tokens.folded[lead]))
|
|
1686
|
-
lead++;
|
|
1687
|
-
const pos = readProgramPosition({ folded: tokens.folded.slice(lead), raw: tokens.raw.slice(lead) });
|
|
1501
|
+
const pos = readProgramPosition(tokens);
|
|
1688
1502
|
if (pos.program === undefined)
|
|
1689
1503
|
return undefined;
|
|
1690
|
-
const at =
|
|
1691
|
-
return
|
|
1504
|
+
const at = pos.program.index;
|
|
1505
|
+
return ASSIGNMENT_PREFIX.test(tokens.folded[at] ?? "") ? undefined : { program: pos.program.basename, from: at };
|
|
1692
1506
|
}
|
|
1693
1507
|
export function attachedOptionPayloads(tok) {
|
|
1694
1508
|
const literal = literalTildeSpelling;
|
|
@@ -1711,182 +1525,6 @@ export function attachedOptionPayloads(tok) {
|
|
|
1711
1525
|
}
|
|
1712
1526
|
return out;
|
|
1713
1527
|
}
|
|
1714
|
-
function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
1715
|
-
const name = tokens.folded[0];
|
|
1716
|
-
const slotVerb = tokens.slotVerb ?? name;
|
|
1717
|
-
const declaredNotPaths = ASSIGNMENT_VALUE_NOT_PATHS.has(tokens.argvProgram ?? slotVerb);
|
|
1718
|
-
const argvFrom = tokens.argvFrom ?? (tokens.slotVerb === undefined ? 1 : Number.MAX_SAFE_INTEGER);
|
|
1719
|
-
if (NO_PATH_OPERAND_COMMANDS.has(name))
|
|
1720
|
-
return [];
|
|
1721
|
-
const declaresNoPathOperands = tokens.slotVerb !== undefined && NO_PATH_OPERAND_COMMANDS.has(tokens.slotVerb) && tokens.argvFrom !== undefined;
|
|
1722
|
-
const args = declaresNoPathOperands ? tokens.folded.slice(1, tokens.argvFrom + 1) : tokens.folded.slice(1);
|
|
1723
|
-
const findings = [];
|
|
1724
|
-
const candidates = [];
|
|
1725
|
-
const bareWordOperands = [];
|
|
1726
|
-
const judgeOperand = (text, globbed) => {
|
|
1727
|
-
(isPathShapedToken(text) || isAbsolutePathForm(text) ? candidates : bareWordOperands).push({ text, globbed });
|
|
1728
|
-
};
|
|
1729
|
-
const positionalOperands = [];
|
|
1730
|
-
const argvOperandsStartAt = tokens.slotVerb !== undefined ? 1 : 0;
|
|
1731
|
-
let stdinOperand = false;
|
|
1732
|
-
const argGlobs = (k) => {
|
|
1733
|
-
const raw = tokens.raw[k + 1];
|
|
1734
|
-
return raw !== undefined && hasUnquotedGlobMetachar(raw);
|
|
1735
|
-
};
|
|
1736
|
-
const expandingArgs = new Set();
|
|
1737
|
-
tokens.raw.slice(1).forEach((rawArg, k) => {
|
|
1738
|
-
if (!hasUnquotedExpansionMetachar(rawArg))
|
|
1739
|
-
return;
|
|
1740
|
-
expandingArgs.add(k);
|
|
1741
|
-
findings.push({
|
|
1742
|
-
kind: "unresolvable",
|
|
1743
|
-
operand: rawArg,
|
|
1744
|
-
reason: `"${name}" is given the argument "${rawArg}", which the shell expands (brace/variable/command expansion) before the command runs — the path it would actually read cannot be resolved statically, so it is not auto-allowed`,
|
|
1745
|
-
});
|
|
1746
|
-
});
|
|
1747
|
-
if (name === "cd" && expandingArgs.size > 0)
|
|
1748
|
-
return findings;
|
|
1749
|
-
if (name === "cd") {
|
|
1750
|
-
const targetIdx = args.findIndex((t) => !t.startsWith("-") || t === "-");
|
|
1751
|
-
const target = targetIdx === -1 ? undefined : args[targetIdx];
|
|
1752
|
-
if (target !== undefined && argGlobs(targetIdx)) {
|
|
1753
|
-
return [{ kind: "unresolvable", operand: target, reason: `\`cd\` is given the pattern "${target}", which the shell expands to a directory this check cannot know ${NOT_AUTO_ALLOWED}` }];
|
|
1754
|
-
}
|
|
1755
|
-
if (target === undefined) {
|
|
1756
|
-
return [{ kind: "unresolvable", operand: "cd", reason: '`cd` with no argument targets the home directory, which cannot be checked against the allowed directories ' + NOT_AUTO_ALLOWED }];
|
|
1757
|
-
}
|
|
1758
|
-
if (target === "-") {
|
|
1759
|
-
return [{ kind: "unresolvable", operand: "-", reason: '`cd -` targets the previous working directory, which cannot be resolved statically ' + NOT_AUTO_ALLOWED }];
|
|
1760
|
-
}
|
|
1761
|
-
candidates.push({ text: target, globbed: false });
|
|
1762
|
-
}
|
|
1763
|
-
else {
|
|
1764
|
-
const eoo = args.indexOf("--");
|
|
1765
|
-
const firstOperandSlot = VERB_OPTION_SLOTS[name]?.firstOperand;
|
|
1766
|
-
const firstOperandSuppliedByFlag = firstOperandSlot !== undefined && (eoo === -1 ? args : args.slice(0, eoo)).some((t) => suppliesFirstOperandSlot(firstOperandSlot, t));
|
|
1767
|
-
let sawOperand = false;
|
|
1768
|
-
let sawArgvOperand = false;
|
|
1769
|
-
let endOfOptions = false;
|
|
1770
|
-
for (let k = 0; k < args.length; k++) {
|
|
1771
|
-
const t = args[k];
|
|
1772
|
-
if (expandingArgs.has(k))
|
|
1773
|
-
continue;
|
|
1774
|
-
if (!endOfOptions) {
|
|
1775
|
-
if (t === "--") {
|
|
1776
|
-
endOfOptions = true;
|
|
1777
|
-
continue;
|
|
1778
|
-
}
|
|
1779
|
-
if (t.startsWith("-") && t !== "-") {
|
|
1780
|
-
const slot = readVerbOptionSlot(slotVerb, t);
|
|
1781
|
-
const permutes = !verbOptionsArePositionIndependent(slotVerb);
|
|
1782
|
-
if (sawArgvOperand && permutes) {
|
|
1783
|
-
judgeOperand(t, argGlobs(k));
|
|
1784
|
-
if (slot !== undefined && slot.form === "separated") {
|
|
1785
|
-
const ahead = args[k + 1];
|
|
1786
|
-
if (ahead !== undefined && !expandingArgs.has(k + 1)) {
|
|
1787
|
-
for (const c of slotValueCandidates(ahead, slot.kind))
|
|
1788
|
-
judgeOperand(c, argGlobs(k + 1));
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawArgvOperand && permutes)) {
|
|
1793
|
-
if (slot.form === "attached") {
|
|
1794
|
-
for (const c of slotValueCandidates(literalTildeSpelling(slot.value), slot.kind))
|
|
1795
|
-
judgeOperand(c, argGlobs(k));
|
|
1796
|
-
}
|
|
1797
|
-
else if (slot.form === "separated") {
|
|
1798
|
-
const value = args[++k];
|
|
1799
|
-
if (value !== undefined && !expandingArgs.has(k)) {
|
|
1800
|
-
const named = slotValueCandidates(value, slot.kind);
|
|
1801
|
-
for (const c of named)
|
|
1802
|
-
judgeOperand(c, argGlobs(k));
|
|
1803
|
-
if (named.length > 0)
|
|
1804
|
-
positionalOperands.push(value);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
for (const payload of attachedOptionPayloads(t))
|
|
1810
|
-
judgeOperand(payload, argGlobs(k));
|
|
1811
|
-
continue;
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
const inFirstOperandSlot = firstOperandSlot !== undefined && !firstOperandSuppliedByFlag && !sawOperand;
|
|
1815
|
-
sawOperand = true;
|
|
1816
|
-
if (k >= argvOperandsStartAt)
|
|
1817
|
-
sawArgvOperand = true;
|
|
1818
|
-
if (t === "-") {
|
|
1819
|
-
if (inFirstOperandSlot)
|
|
1820
|
-
continue;
|
|
1821
|
-
const m = RECURSIVE_READ_FORMS[slotVerb];
|
|
1822
|
-
if (m === undefined || m.dashIsStdin === true) {
|
|
1823
|
-
stdinOperand = true;
|
|
1824
|
-
continue;
|
|
1825
|
-
}
|
|
1826
|
-
if (k >= argvOperandsStartAt)
|
|
1827
|
-
positionalOperands.push(t);
|
|
1828
|
-
continue;
|
|
1829
|
-
}
|
|
1830
|
-
if (inFirstOperandSlot) {
|
|
1831
|
-
const named = slotValueCandidates(t, firstOperandSlot.kind);
|
|
1832
|
-
for (const c of named)
|
|
1833
|
-
judgeOperand(c, argGlobs(k));
|
|
1834
|
-
if (named.length > 0 && k >= argvOperandsStartAt)
|
|
1835
|
-
positionalOperands.push(t);
|
|
1836
|
-
continue;
|
|
1837
|
-
}
|
|
1838
|
-
if (k >= argvOperandsStartAt)
|
|
1839
|
-
positionalOperands.push(t);
|
|
1840
|
-
judgeOperand(t, argGlobs(k));
|
|
1841
|
-
const payload = assignmentWordPayload(t);
|
|
1842
|
-
if (payload !== undefined && (k + 1 < argvFrom || !declaredNotPaths)) {
|
|
1843
|
-
if (payload.kind === "value")
|
|
1844
|
-
judgeOperand(payload.value, argGlobs(k));
|
|
1845
|
-
else
|
|
1846
|
-
findings.push({ kind: "unresolvable", operand: t, reason: `"${name}": ${payload.reason} ${NOT_AUTO_ALLOWED}` });
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
for (let k = 0; k < args.length; k++) {
|
|
1851
|
-
if (!argGlobs(k) || expandingArgs.has(k))
|
|
1852
|
-
continue;
|
|
1853
|
-
const r = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], args[k], boundary.homeDir);
|
|
1854
|
-
findings.push({ kind: "undecided", path: r?.folded ?? args[k], ...(r?.spelled !== undefined ? { spelled: r.spelled } : {}) });
|
|
1855
|
-
}
|
|
1856
|
-
for (const { text: candidate, globbed } of candidates) {
|
|
1857
|
-
const spellings = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], candidate, boundary.homeDir);
|
|
1858
|
-
if (spellings === undefined) {
|
|
1859
|
-
findings.push({
|
|
1860
|
-
kind: "unresolvable",
|
|
1861
|
-
operand: candidate,
|
|
1862
|
-
reason: `"${name}" names the path "${candidate}", which cannot be resolved statically ${NOT_AUTO_ALLOWED}`,
|
|
1863
|
-
});
|
|
1864
|
-
continue;
|
|
1865
|
-
}
|
|
1866
|
-
const resolved = spellings.folded;
|
|
1867
|
-
if (withinAnyRoot(boundary.roots, resolved)) {
|
|
1868
|
-
if (!globbed)
|
|
1869
|
-
findings.push({ kind: "inside", path: resolved, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1870
|
-
}
|
|
1871
|
-
else
|
|
1872
|
-
findings.push({ kind: "outside", command: name, path: resolved, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1873
|
-
}
|
|
1874
|
-
for (const { text: operand, globbed } of bareWordOperands) {
|
|
1875
|
-
const spellings = resolveOperandSpellings(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
|
|
1876
|
-
if (spellings === undefined)
|
|
1877
|
-
continue;
|
|
1878
|
-
if (!globbed && withinAnyRoot(boundary.roots, spellings.folded))
|
|
1879
|
-
findings.push({ kind: "inside", path: spellings.folded, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1880
|
-
}
|
|
1881
|
-
if (boundary.denyMatch !== undefined && (boundary.denyEntries?.length ?? 1) > 0 && segmentSelectsRecursiveRead(slotVerb, args.slice(argvOperandsStartAt))) {
|
|
1882
|
-
const roots = positionalOperands.length > 0 ? positionalOperands : stdinOperand ? [] : ["."];
|
|
1883
|
-
for (const operand of roots) {
|
|
1884
|
-
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
|
|
1885
|
-
findings.push({ kind: "recursive", path: resolved ?? operand });
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
return findings;
|
|
1889
|
-
}
|
|
1890
1528
|
export function foldHomeVariableSpelling(command, homeDir) {
|
|
1891
1529
|
if (homeDir === undefined || !command.includes("$") || /[\\`]/.test(command))
|
|
1892
1530
|
return command;
|
|
@@ -1945,11 +1583,11 @@ function segmentCompoundForReadonly(command, homeDir) {
|
|
|
1945
1583
|
: redirectionStripped;
|
|
1946
1584
|
return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep" });
|
|
1947
1585
|
}
|
|
1948
|
-
export function classifyCompoundReadonlyDetailed(command, allow, boundary
|
|
1586
|
+
export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
1949
1587
|
const split = segmentCompoundForReadonly(command, boundary?.homeDir);
|
|
1950
1588
|
if ("reject" in split)
|
|
1951
|
-
return { reason: split.reject
|
|
1952
|
-
const { segments, pipeFed
|
|
1589
|
+
return { reason: split.reject };
|
|
1590
|
+
const { segments, pipeFed } = split;
|
|
1953
1591
|
let nameRefusal;
|
|
1954
1592
|
const unlistedSegments = new Set();
|
|
1955
1593
|
const reheaded = new Map();
|
|
@@ -1962,7 +1600,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1962
1600
|
if (written === undefined)
|
|
1963
1601
|
return { reason: "empty command" };
|
|
1964
1602
|
if (written === "coproc")
|
|
1965
|
-
return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command"
|
|
1603
|
+
return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command" };
|
|
1966
1604
|
if (isBareControlWord(toks, from, SHELL_BLOCK_WORDS)) {
|
|
1967
1605
|
unlistedSegments.add(si);
|
|
1968
1606
|
nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
|
|
@@ -1976,13 +1614,13 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1976
1614
|
}
|
|
1977
1615
|
const parsed = parseLeadingCommandName(toks.raw.slice(from).join(" "), { quotedOperatorsAreText: true, pathPrefixedNameIsText: true });
|
|
1978
1616
|
if ("reject" in parsed)
|
|
1979
|
-
return { reason: parsed.reject
|
|
1617
|
+
return { reason: parsed.reject };
|
|
1980
1618
|
const pathPrefixed = parsed.name.includes("/");
|
|
1981
1619
|
const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
|
|
1982
1620
|
if (pathPrefixed)
|
|
1983
1621
|
nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
|
|
1984
1622
|
if (pos.refusal !== undefined)
|
|
1985
|
-
return { reason: programPositionRefusalSentence(pos.refusal)
|
|
1623
|
+
return { reason: programPositionRefusalSentence(pos.refusal) };
|
|
1986
1624
|
if (allow.has(name)) {
|
|
1987
1625
|
if (pathPrefixed || from > 0)
|
|
1988
1626
|
reheaded.set(si, { from, head: name });
|
|
@@ -1992,14 +1630,14 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1992
1630
|
}
|
|
1993
1631
|
nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
|
|
1994
1632
|
if (SHELL_REENTRY_PROGRAM_RE.test(name))
|
|
1995
|
-
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text")
|
|
1633
|
+
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text") };
|
|
1996
1634
|
if (pos.launchers.length > 0) {
|
|
1997
1635
|
const listed = program !== undefined && (allow.has(program.basename) || program.basename === "cd");
|
|
1998
1636
|
if (program !== undefined && !listed && SHELL_REENTRY_PROGRAM_RE.test(program.basename)) {
|
|
1999
|
-
return { reason: `"${written}" runs "${program.basename}", which runs its argument as shell text this
|
|
1637
|
+
return { reason: `"${written}" runs "${program.basename}", which runs its argument as shell text this classifier does not read ${NOT_AUTO_ALLOWED}` };
|
|
2000
1638
|
}
|
|
2001
1639
|
if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED)) {
|
|
2002
|
-
return { reason: `"${written}" takes the operands of the program it runs from its standard input, which this
|
|
1640
|
+
return { reason: `"${written}" takes the operands of the program it runs from its standard input, which this classifier does not read ${NOT_AUTO_ALLOWED}` };
|
|
2003
1641
|
}
|
|
2004
1642
|
if (!listed || program === undefined)
|
|
2005
1643
|
unlistedSegments.add(si);
|
|
@@ -2039,44 +1677,16 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
2039
1677
|
};
|
|
2040
1678
|
const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity, diff: 2, cmp: 2, comm: 2, sed: 2 };
|
|
2041
1679
|
const foldedSegments = [];
|
|
2042
|
-
const foldedReach = [];
|
|
2043
|
-
let crossedAllAnd = true;
|
|
2044
|
-
let crossedAnyOr = false;
|
|
2045
1680
|
let shapeRefusal = nameRefusal;
|
|
2046
1681
|
for (let si = 0; si < segments.length; si++) {
|
|
2047
|
-
if (si
|
|
2048
|
-
if (connectors[si] !== "&&")
|
|
2049
|
-
crossedAllAnd = false;
|
|
2050
|
-
if (connectors[si] === "||")
|
|
2051
|
-
crossedAnyOr = true;
|
|
2052
|
-
}
|
|
2053
|
-
const moveProvable = pipeFed[si] !== true && pipeFed[si + 1] !== true && reheaded.get(si) === undefined;
|
|
2054
|
-
const pushReach = () => {
|
|
2055
|
-
foldedReach.push({ andJoined: si > 0 && crossedAllAnd, orBroken: !crossedAllAnd && crossedAnyOr, moveProvable });
|
|
2056
|
-
crossedAllAnd = true;
|
|
2057
|
-
crossedAnyOr = false;
|
|
2058
|
-
};
|
|
2059
|
-
if (unlistedSegments.has(si)) {
|
|
2060
|
-
const unlisted = tokenizeSegment(segments[si]);
|
|
2061
|
-
if (unlisted.folded.length === 0)
|
|
2062
|
-
continue;
|
|
2063
|
-
const unlistedArgv = argvProgramOf(unlisted);
|
|
2064
|
-
foldedSegments.push({
|
|
2065
|
-
folded: [LAUNCHER_SCAN_HEAD, ...unlisted.folded],
|
|
2066
|
-
raw: [LAUNCHER_SCAN_HEAD, ...unlisted.raw],
|
|
2067
|
-
slotVerb: programBasename(unlisted.folded[0]),
|
|
2068
|
-
...(unlistedArgv !== undefined ? { argvProgram: unlistedArgv.program, argvFrom: unlistedArgv.from + 1 } : {}),
|
|
2069
|
-
});
|
|
2070
|
-
pushReach();
|
|
1682
|
+
if (unlistedSegments.has(si))
|
|
2071
1683
|
continue;
|
|
2072
|
-
}
|
|
2073
1684
|
const rehead = reheaded.get(si);
|
|
2074
1685
|
const segmentTokens = rehead === undefined ? tokenizeSegment(segments[si]) : reheadSegment(tokenizeSegment(segments[si]), rehead.head, rehead.from);
|
|
2075
1686
|
const toks = segmentTokens.folded;
|
|
2076
1687
|
if (toks.length === 0)
|
|
2077
1688
|
continue;
|
|
2078
1689
|
foldedSegments.push(segmentTokens);
|
|
2079
|
-
pushReach();
|
|
2080
1690
|
const name = toks[0];
|
|
2081
1691
|
const guardReason = guardedVerbFormReason(name, toks, segmentTokens.raw);
|
|
2082
1692
|
if (guardReason !== undefined) {
|
|
@@ -2152,311 +1762,13 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
2152
1762
|
shapeRefusal ??= "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) " + NOT_AUTO_ALLOWED;
|
|
2153
1763
|
}
|
|
2154
1764
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
}
|
|
2160
|
-
if (boundary !== undefined) {
|
|
2161
|
-
const walk = evaluateReadBoundary(foldedSegments, boundary, true, foldedReach);
|
|
2162
|
-
if (shapeRefusal === undefined)
|
|
2163
|
-
return walk;
|
|
2164
|
-
const { reason: _walkSentence, outOfRootGrantCandidate: walkGrantCandidate, outOfRootGrantCoversExactPath: walkGrantExact, ...structure } = walk;
|
|
2165
|
-
const nameOnly = shapeRefusal === nameRefusal;
|
|
2166
|
-
return {
|
|
2167
|
-
...structure,
|
|
2168
|
-
reason: shapeRefusal,
|
|
2169
|
-
...(nameOnly && walkGrantCandidate !== undefined ? { outOfRootGrantCandidate: walkGrantCandidate, ...(walkGrantExact === true ? { outOfRootGrantCoversExactPath: true } : {}) } : {}),
|
|
2170
|
-
...(nameOnly ? { segmentNameRefusal: true } : {}),
|
|
2171
|
-
};
|
|
2172
|
-
}
|
|
2173
|
-
if (shapeRefusal !== undefined)
|
|
2174
|
-
return { reason: shapeRefusal };
|
|
2175
|
-
if (foldedSegments.some((st) => st.folded[0] === "cd")) {
|
|
2176
|
-
return { reason: `\`cd\` cannot be judged without a read boundary (no base to resolve the directory change against) ${NOT_AUTO_ALLOWED}` };
|
|
2177
|
-
}
|
|
2178
|
-
return {};
|
|
1765
|
+
const cdSegments = foldedSegments.filter((st) => st.folded[0] === "cd");
|
|
1766
|
+
if (cdSegments.length > 1)
|
|
1767
|
+
shapeRefusal ??= `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}`;
|
|
1768
|
+
return shapeRefusal !== undefined ? { reason: shapeRefusal } : {};
|
|
2179
1769
|
}
|
|
2180
|
-
const BASE_CANDIDATE_MAX = 256;
|
|
2181
|
-
function evaluateReadBoundary(foldedSegments, boundary, mintGrantCandidate, reach) {
|
|
2182
|
-
const seen = { outside: new Set(), inside: new Set(), undecided: new Set(), recursive: new Set(), unresolved: new Set(), checkedSpelled: new Set(), undecidedSpelled: new Set(), outsideSpelled: new Set() };
|
|
2183
|
-
const outside = [];
|
|
2184
|
-
const inside = [];
|
|
2185
|
-
let unresolvable;
|
|
2186
|
-
const unresolved = [];
|
|
2187
|
-
let baseLost = false;
|
|
2188
|
-
const undecided = [];
|
|
2189
|
-
const checkedSpellings = [];
|
|
2190
|
-
const undecidedSpellings = [];
|
|
2191
|
-
const outOfRootSpellings = [];
|
|
2192
|
-
const recordSpelling = (into, seenHere, finding) => {
|
|
2193
|
-
if (finding.spelled === undefined || seenHere.has(finding.spelled))
|
|
2194
|
-
return;
|
|
2195
|
-
seenHere.add(finding.spelled);
|
|
2196
|
-
into.push(finding.spelled);
|
|
2197
|
-
};
|
|
2198
|
-
const recursive = [];
|
|
2199
|
-
let candidates = [{ base: boundary.cwd ?? boundary.roots[0], skippedCd: -1 }];
|
|
2200
|
-
let andRunStart = 0;
|
|
2201
|
-
const findingsByWords = new Map();
|
|
2202
|
-
const findingsRowFor = (wordsKey) => {
|
|
2203
|
-
const row = findingsByWords.get(wordsKey);
|
|
2204
|
-
if (row !== undefined)
|
|
2205
|
-
return row;
|
|
2206
|
-
const fresh = new Map();
|
|
2207
|
-
findingsByWords.set(wordsKey, fresh);
|
|
2208
|
-
return fresh;
|
|
2209
|
-
};
|
|
2210
|
-
const findingsFor = (row, toks, base) => {
|
|
2211
|
-
const hit = row.get(base);
|
|
2212
|
-
if (hit !== undefined)
|
|
2213
|
-
return hit;
|
|
2214
|
-
const segBoundary = base === (boundary.cwd ?? boundary.roots[0]) ? boundary : { ...boundary, cwd: base };
|
|
2215
|
-
const computed = collectSegmentBoundaryFindings(toks, segBoundary);
|
|
2216
|
-
row.set(base, computed);
|
|
2217
|
-
return computed;
|
|
2218
|
-
};
|
|
2219
|
-
for (let si = 0; si < foldedSegments.length; si++) {
|
|
2220
|
-
const toks = foldedSegments[si];
|
|
2221
|
-
if (baseLost)
|
|
2222
|
-
break;
|
|
2223
|
-
if (si > 0 && reach?.[si]?.andJoined !== true)
|
|
2224
|
-
andRunStart = reach?.[si]?.orBroken === true ? si + 1 : si;
|
|
2225
|
-
const reachable = candidates.filter((c) => c.skippedCd < andRunStart);
|
|
2226
|
-
const row = findingsRowFor(JSON.stringify([toks.folded, toks.raw, toks.slotVerb]));
|
|
2227
|
-
for (const { base: currentBase } of reachable) {
|
|
2228
|
-
for (const finding of findingsFor(row, toks, currentBase)) {
|
|
2229
|
-
if (finding.kind === "unresolvable") {
|
|
2230
|
-
unresolvable ??= finding.reason;
|
|
2231
|
-
if (!seen.unresolved.has(finding.operand)) {
|
|
2232
|
-
seen.unresolved.add(finding.operand);
|
|
2233
|
-
unresolved.push(finding.operand);
|
|
2234
|
-
}
|
|
2235
|
-
if (toks.folded[0] === "cd")
|
|
2236
|
-
baseLost = true;
|
|
2237
|
-
continue;
|
|
2238
|
-
}
|
|
2239
|
-
const denied = boundary.denyMatch?.(finding.path);
|
|
2240
|
-
if (denied != null) {
|
|
2241
|
-
return {
|
|
2242
|
-
reason: `a command operand resolves to "${finding.path}", which matches ${sensitivePathDenyList(denied)} ${NOT_AUTO_ALLOWED}`,
|
|
2243
|
-
readDenied: true,
|
|
2244
|
-
readDeniedPath: finding.path,
|
|
2245
|
-
readDeniedPattern: denied,
|
|
2246
|
-
};
|
|
2247
|
-
}
|
|
2248
|
-
if (finding.kind === "inside") {
|
|
2249
|
-
if (!seen.inside.has(finding.path)) {
|
|
2250
|
-
seen.inside.add(finding.path);
|
|
2251
|
-
inside.push(finding.path);
|
|
2252
|
-
}
|
|
2253
|
-
recordSpelling(checkedSpellings, seen.checkedSpelled, finding);
|
|
2254
|
-
continue;
|
|
2255
|
-
}
|
|
2256
|
-
if (finding.kind === "undecided") {
|
|
2257
|
-
if (!seen.undecided.has(finding.path)) {
|
|
2258
|
-
seen.undecided.add(finding.path);
|
|
2259
|
-
undecided.push(finding.path);
|
|
2260
|
-
}
|
|
2261
|
-
recordSpelling(undecidedSpellings, seen.undecidedSpelled, finding);
|
|
2262
|
-
continue;
|
|
2263
|
-
}
|
|
2264
|
-
if (finding.kind === "recursive") {
|
|
2265
|
-
if (!seen.recursive.has(finding.path)) {
|
|
2266
|
-
seen.recursive.add(finding.path);
|
|
2267
|
-
recursive.push(finding.path);
|
|
2268
|
-
}
|
|
2269
|
-
continue;
|
|
2270
|
-
}
|
|
2271
|
-
if (boundary.face === "open") {
|
|
2272
|
-
if (!seen.inside.has(finding.path)) {
|
|
2273
|
-
seen.inside.add(finding.path);
|
|
2274
|
-
inside.push(finding.path);
|
|
2275
|
-
}
|
|
2276
|
-
continue;
|
|
2277
|
-
}
|
|
2278
|
-
if (!seen.outside.has(finding.path)) {
|
|
2279
|
-
seen.outside.add(finding.path);
|
|
2280
|
-
outside.push(finding);
|
|
2281
|
-
}
|
|
2282
|
-
recordSpelling(outOfRootSpellings, seen.outsideSpelled, finding);
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
if (!baseLost && toks.folded[0] === "cd") {
|
|
2286
|
-
const cdArgs = toks.folded.slice(1);
|
|
2287
|
-
const cdTargetIdx = cdArgs.findIndex((t) => !t.startsWith("-") || t === "-");
|
|
2288
|
-
const cdTarget = cdTargetIdx === -1 ? undefined : cdArgs[cdTargetIdx];
|
|
2289
|
-
if (cdTarget !== undefined && cdTarget !== "-") {
|
|
2290
|
-
const provable = reach?.[si]?.moveProvable === true;
|
|
2291
|
-
const branched = [];
|
|
2292
|
-
for (const c of candidates) {
|
|
2293
|
-
if (c.skippedCd < andRunStart)
|
|
2294
|
-
branched.push({ base: resolveOperandLexically(c.base, cdTarget, boundary.homeDir) ?? c.base, skippedCd: c.skippedCd });
|
|
2295
|
-
branched.push({ base: c.base, skippedCd: provable ? si : c.skippedCd });
|
|
2296
|
-
}
|
|
2297
|
-
const byBase = new Map();
|
|
2298
|
-
for (const c of branched) {
|
|
2299
|
-
const prior = byBase.get(c.base);
|
|
2300
|
-
if (prior === undefined || c.skippedCd < prior.skippedCd)
|
|
2301
|
-
byBase.set(c.base, c);
|
|
2302
|
-
}
|
|
2303
|
-
const next = [...byBase.values()];
|
|
2304
|
-
if (next.length > BASE_CANDIDATE_MAX) {
|
|
2305
|
-
unresolvable ??= `too many directory changes to track where "${cdTarget}" leaves the working directory ${NOT_AUTO_ALLOWED}`;
|
|
2306
|
-
if (!seen.unresolved.has(cdTarget)) {
|
|
2307
|
-
seen.unresolved.add(cdTarget);
|
|
2308
|
-
unresolved.push(cdTarget);
|
|
2309
|
-
}
|
|
2310
|
-
baseLost = true;
|
|
2311
|
-
}
|
|
2312
|
-
else
|
|
2313
|
-
candidates = next;
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
const undecidedAll = [...undecided, ...recursive.filter((p) => !seen.undecided.has(p))];
|
|
2318
|
-
const undecidedField = {
|
|
2319
|
-
...(undecidedAll.length > 0 ? { undecidedPaths: undecidedAll } : {}),
|
|
2320
|
-
...(recursive.length > 0 ? { recursiveReadPaths: recursive } : {}),
|
|
2321
|
-
...(undecidedSpellings.length > 0 ? { undecidedSpellings } : {}),
|
|
2322
|
-
};
|
|
2323
|
-
const checkedField = {
|
|
2324
|
-
...(inside.length > 0 ? { checkedPaths: inside } : {}),
|
|
2325
|
-
...(checkedSpellings.length > 0 ? { checkedSpellings } : {}),
|
|
2326
|
-
};
|
|
2327
|
-
const outsideField = outOfRootSpellings.length > 0 ? { outOfRootSpellings } : {};
|
|
2328
|
-
if (unresolvable !== undefined) {
|
|
2329
|
-
return {
|
|
2330
|
-
reason: unresolvable,
|
|
2331
|
-
unresolvedOperands: unresolved,
|
|
2332
|
-
...(outside.length > 0 ? { outOfRootRead: true, outOfRootPaths: outside.map((o) => o.path), ...outsideField } : {}),
|
|
2333
|
-
...checkedField,
|
|
2334
|
-
...undecidedField,
|
|
2335
|
-
};
|
|
2336
|
-
}
|
|
2337
|
-
if (outside.length === 0)
|
|
2338
|
-
return { ...checkedField, ...undecidedField };
|
|
2339
|
-
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
2340
|
-
const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
|
|
2341
|
-
const grantCandidate = (() => {
|
|
2342
|
-
if (!mintGrantCandidate || outOfRootSpellings.length > 0)
|
|
2343
|
-
return undefined;
|
|
2344
|
-
const proposed = outOfRootGrantCandidateOf(outside.map((o) => o.path), boundary);
|
|
2345
|
-
if (proposed === undefined)
|
|
2346
|
-
return undefined;
|
|
2347
|
-
const replay = evaluateReadBoundary(foldedSegments, { ...boundary, roots: [...boundary.roots, proposed.path] }, false, reach);
|
|
2348
|
-
return replay.reason === undefined && (replay.undecidedPaths ?? []).length === 0 ? proposed : undefined;
|
|
2349
|
-
})();
|
|
2350
|
-
return {
|
|
2351
|
-
reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} ${NOT_AUTO_ALLOWED}`,
|
|
2352
|
-
outOfRootRead: true,
|
|
2353
|
-
outOfRootPaths: outside.map((o) => o.path),
|
|
2354
|
-
...(grantCandidate !== undefined ? { outOfRootGrantCandidate: grantCandidate.path, ...(grantCandidate.exact === true ? { outOfRootGrantCoversExactPath: true } : {}) } : {}),
|
|
2355
|
-
...outsideField,
|
|
2356
|
-
...checkedField,
|
|
2357
|
-
...undecidedField,
|
|
2358
|
-
};
|
|
2359
|
-
}
|
|
2360
|
-
export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
2361
|
-
const toks = tokenizeSegment(command);
|
|
2362
|
-
if (toks.folded.length === 0)
|
|
2363
|
-
return {};
|
|
2364
|
-
const pos = readProgramPosition(toks);
|
|
2365
|
-
if (pos.refusal !== undefined)
|
|
2366
|
-
return { reason: programPositionRefusalSentence(pos.refusal), refusedWhole: true };
|
|
2367
|
-
if (pos.program !== undefined) {
|
|
2368
|
-
const familyRefusal = argumentAsProgramRefusal(pos.program.basename, pos.program.basename, toks.folded.slice(pos.program.index + 1));
|
|
2369
|
-
if (familyRefusal !== undefined)
|
|
2370
|
-
return { reason: familyRefusal };
|
|
2371
|
-
}
|
|
2372
|
-
return evaluateReadBoundary([toks], boundary, true);
|
|
2373
|
-
}
|
|
2374
|
-
const NOT_GATED = { gated: false, paths: [], checked: [], undecided: [] };
|
|
2375
|
-
const UNREADABLE_WHITELIST_WORD = /[$`()*?[\]{}]/;
|
|
2376
1770
|
export const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
2377
1771
|
export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
|
|
2378
|
-
const LAUNCHER_SCAN_HEAD = "cat";
|
|
2379
|
-
export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
2380
|
-
const containmentOnly = {
|
|
2381
|
-
roots: boundary.roots,
|
|
2382
|
-
...(boundary.cwd !== undefined ? { cwd: boundary.cwd } : {}),
|
|
2383
|
-
...(boundary.homeDir !== undefined ? { homeDir: boundary.homeDir } : {}),
|
|
2384
|
-
...(boundary.face !== undefined ? { face: boundary.face } : {}),
|
|
2385
|
-
};
|
|
2386
|
-
const judge = (text) => {
|
|
2387
|
-
const split = segmentCompoundForReadonly(text, containmentOnly.homeDir);
|
|
2388
|
-
if ("reject" in split)
|
|
2389
|
-
return NOT_GATED;
|
|
2390
|
-
const readers = [];
|
|
2391
|
-
const readerReach = [];
|
|
2392
|
-
let crossedAllAnd = true;
|
|
2393
|
-
let crossedAnyOr = false;
|
|
2394
|
-
let baseMoved = false;
|
|
2395
|
-
for (let si = 0; si < split.segments.length; si++) {
|
|
2396
|
-
const segment = split.segments[si];
|
|
2397
|
-
if (si > 0) {
|
|
2398
|
-
if (split.connectors[si] !== "&&")
|
|
2399
|
-
crossedAllAnd = false;
|
|
2400
|
-
if (split.connectors[si] === "||")
|
|
2401
|
-
crossedAnyOr = true;
|
|
2402
|
-
}
|
|
2403
|
-
const moveProvable = split.pipeFed[si] !== true && split.pipeFed[si + 1] !== true;
|
|
2404
|
-
const pushReader = (st) => {
|
|
2405
|
-
readers.push(st);
|
|
2406
|
-
readerReach.push({ andJoined: si > 0 && crossedAllAnd, orBroken: !crossedAllAnd && crossedAnyOr, moveProvable });
|
|
2407
|
-
crossedAllAnd = true;
|
|
2408
|
-
crossedAnyOr = false;
|
|
2409
|
-
};
|
|
2410
|
-
const toks = tokenizeSegment(segment);
|
|
2411
|
-
if (toks.folded.length === 0)
|
|
2412
|
-
continue;
|
|
2413
|
-
let i = 0;
|
|
2414
|
-
while (i < toks.folded.length && ASSIGNMENT_WORD.test(toks.folded[i]))
|
|
2415
|
-
i++;
|
|
2416
|
-
const written = toks.folded[i];
|
|
2417
|
-
if (written === undefined)
|
|
2418
|
-
continue;
|
|
2419
|
-
const name = programBasename(written);
|
|
2420
|
-
if (UNMODELLED_BASE_MOVERS.has(name))
|
|
2421
|
-
baseMoved = true;
|
|
2422
|
-
if (!allow.has(name) && name !== "cd") {
|
|
2423
|
-
const argv = argvProgramOf(toks);
|
|
2424
|
-
pushReader({ folded: [LAUNCHER_SCAN_HEAD, ...toks.folded], raw: [LAUNCHER_SCAN_HEAD, ...toks.raw], slotVerb: name, ...(argv !== undefined ? { argvProgram: argv.program, argvFrom: argv.from + 1 } : {}) });
|
|
2425
|
-
continue;
|
|
2426
|
-
}
|
|
2427
|
-
pushReader(i === 0 && name === written ? toks : { folded: [name, ...toks.folded.slice(i + 1)], raw: [toks.raw[i] ?? name, ...toks.raw.slice(i + 1)] });
|
|
2428
|
-
}
|
|
2429
|
-
if (readers.length === 0)
|
|
2430
|
-
return NOT_GATED;
|
|
2431
|
-
if (baseMoved)
|
|
2432
|
-
return { gated: true, paths: [], checked: [], undecided: [] };
|
|
2433
|
-
const verdict = evaluateReadBoundary(readers, containmentOnly, false, readerReach);
|
|
2434
|
-
const denyPass = boundary.denyMatch === undefined ? undefined : evaluateReadBoundary(readers, boundary, false, readerReach);
|
|
2435
|
-
const denied = denyPass?.readDenied === true && denyPass.readDeniedPath !== undefined && denyPass.readDeniedPattern !== undefined
|
|
2436
|
-
? { path: denyPass.readDeniedPath, pattern: denyPass.readDeniedPattern }
|
|
2437
|
-
: undefined;
|
|
2438
|
-
const resolvedOperands = {
|
|
2439
|
-
checked: [...(verdict.checkedPaths ?? []), ...(verdict.checkedSpellings ?? [])],
|
|
2440
|
-
undecided: [...(verdict.undecidedPaths ?? []), ...(verdict.undecidedSpellings ?? [])],
|
|
2441
|
-
};
|
|
2442
|
-
if (verdict.outOfRootRead === true) {
|
|
2443
|
-
return { gated: true, paths: verdict.outOfRootPaths ?? [], ...resolvedOperands, ...(denied !== undefined ? { denied } : {}) };
|
|
2444
|
-
}
|
|
2445
|
-
if (denied !== undefined)
|
|
2446
|
-
return { gated: false, paths: [], ...resolvedOperands, denied };
|
|
2447
|
-
return resolvedOperands.checked.length === 0 && resolvedOperands.undecided.length === 0 ? NOT_GATED : { gated: false, paths: [], ...resolvedOperands };
|
|
2448
|
-
};
|
|
2449
|
-
const direct = judge(command);
|
|
2450
|
-
if (direct.gated || direct.denied !== undefined)
|
|
2451
|
-
return direct;
|
|
2452
|
-
const shape = POLL_LOOP_SHAPE.exec(command.trim());
|
|
2453
|
-
if (shape === null)
|
|
2454
|
-
return direct;
|
|
2455
|
-
const loop = judge(shape[3]);
|
|
2456
|
-
return loop.gated || loop.denied !== undefined
|
|
2457
|
-
? loop
|
|
2458
|
-
: { ...loop, checked: [...direct.checked, ...loop.checked], undecided: [...direct.undecided, ...loop.undecided] };
|
|
2459
|
-
}
|
|
2460
1772
|
export function classifyCompoundReadonly(command, allow, boundary) {
|
|
2461
1773
|
return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
|
|
2462
1774
|
}
|
|
@@ -2549,18 +1861,5 @@ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary
|
|
|
2549
1861
|
if (readSegments.length === 0) {
|
|
2550
1862
|
return { reason: "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed" };
|
|
2551
1863
|
}
|
|
2552
|
-
|
|
2553
|
-
const modelled = bodyHasCd ? Array.from({ length: beats }, () => readSegments.join("; ")).join("; ") : readSegments.join("; ");
|
|
2554
|
-
const verdict = classifyCompoundReadonlyDetailed(modelled, allow, boundary, { iterated: bodyHasCd });
|
|
2555
|
-
if (verdict.reason !== undefined)
|
|
2556
|
-
return verdict;
|
|
2557
|
-
if (verdict.recursiveReadPaths !== undefined) {
|
|
2558
|
-
const roots = verdict.recursiveReadPaths;
|
|
2559
|
-
const shown = roots.slice(0, POLL_LOOP_CAUSE_MAX_PATHS);
|
|
2560
|
-
return { ...verdict, reason: `this lexical check cannot bound what a recursive read form in the loop body traverses — ${roots.length} path(s) taken as traversal roots: ${shown.join(", ")}${roots.length > shown.length ? `, and ${roots.length - shown.length} more` : ""}` };
|
|
2561
|
-
}
|
|
2562
|
-
if (verdict.undecidedPaths !== undefined) {
|
|
2563
|
-
return { ...verdict, reason: `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed` };
|
|
2564
|
-
}
|
|
2565
|
-
return verdict;
|
|
1864
|
+
return classifyCompoundReadonlyDetailed(readSegments.join("; "), allow, boundary);
|
|
2566
1865
|
}
|