@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
|
@@ -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, quoteMask, wordsOf, } from "./bash-lexer.js";
|
|
3
|
+
import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRefusalSentence, readProgramPosition, } 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)
|
|
@@ -922,7 +692,7 @@ function sedReadingKind(reading) {
|
|
|
922
692
|
}
|
|
923
693
|
function readSedScript(script) {
|
|
924
694
|
if (script.split("\n").some((line) => utf8Length(line) >= SED_SCRIPT_LINE_LIMIT))
|
|
925
|
-
return "
|
|
695
|
+
return "oversized";
|
|
926
696
|
let i = 0;
|
|
927
697
|
let depth = 0;
|
|
928
698
|
const len = script.length;
|
|
@@ -1231,8 +1001,10 @@ export function sedScriptSlotFiles(args) {
|
|
|
1231
1001
|
const writes = new Set();
|
|
1232
1002
|
const commands = new Set();
|
|
1233
1003
|
let executesPatternSpace = false;
|
|
1004
|
+
const kinds = [];
|
|
1234
1005
|
for (const dialect of SED_OPTION_DIALECTS) {
|
|
1235
1006
|
const reading = readSedInvocationDetailed(args, dialect);
|
|
1007
|
+
kinds.push(sedReadingKind(reading));
|
|
1236
1008
|
if (typeof reading === "string")
|
|
1237
1009
|
continue;
|
|
1238
1010
|
for (const f of reading.names.reads)
|
|
@@ -1243,12 +1015,17 @@ export function sedScriptSlotFiles(args) {
|
|
|
1243
1015
|
commands.add(c);
|
|
1244
1016
|
executesPatternSpace ||= reading.names.executesPatternSpace;
|
|
1245
1017
|
}
|
|
1246
|
-
|
|
1018
|
+
const unread = kinds.includes("oversized")
|
|
1019
|
+
? "a script line is longer than an implementation compiles whole, so what runs is not the text written here"
|
|
1020
|
+
: kinds.every((k) => k === "unparsed")
|
|
1021
|
+
? "its script could not be read to its end"
|
|
1022
|
+
: undefined;
|
|
1023
|
+
return { reads: [...reads], writes: [...writes], commands: [...commands], executesPatternSpace, ...(unread !== undefined ? { unread } : {}) };
|
|
1247
1024
|
}
|
|
1248
1025
|
const SCRIPT_SLOT_VERBS = new Map([
|
|
1249
1026
|
["sed", (args) => {
|
|
1250
1027
|
const readings = SED_OPTION_DIALECTS.map((dialect) => readSedInvocation(args, dialect));
|
|
1251
|
-
if (readings.includes("names") || readings.includes("unseen"))
|
|
1028
|
+
if (readings.includes("names") || readings.includes("unseen") || readings.includes("oversized"))
|
|
1252
1029
|
return true;
|
|
1253
1030
|
return readings.every((reading) => reading === "unparsed");
|
|
1254
1031
|
}],
|
|
@@ -1711,182 +1488,6 @@ export function attachedOptionPayloads(tok) {
|
|
|
1711
1488
|
}
|
|
1712
1489
|
return out;
|
|
1713
1490
|
}
|
|
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
1491
|
export function foldHomeVariableSpelling(command, homeDir) {
|
|
1891
1492
|
if (homeDir === undefined || !command.includes("$") || /[\\`]/.test(command))
|
|
1892
1493
|
return command;
|
|
@@ -1945,11 +1546,11 @@ function segmentCompoundForReadonly(command, homeDir) {
|
|
|
1945
1546
|
: redirectionStripped;
|
|
1946
1547
|
return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep" });
|
|
1947
1548
|
}
|
|
1948
|
-
export function classifyCompoundReadonlyDetailed(command, allow, boundary
|
|
1549
|
+
export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
1949
1550
|
const split = segmentCompoundForReadonly(command, boundary?.homeDir);
|
|
1950
1551
|
if ("reject" in split)
|
|
1951
|
-
return { reason: split.reject
|
|
1952
|
-
const { segments, pipeFed
|
|
1552
|
+
return { reason: split.reject };
|
|
1553
|
+
const { segments, pipeFed } = split;
|
|
1953
1554
|
let nameRefusal;
|
|
1954
1555
|
const unlistedSegments = new Set();
|
|
1955
1556
|
const reheaded = new Map();
|
|
@@ -1962,7 +1563,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1962
1563
|
if (written === undefined)
|
|
1963
1564
|
return { reason: "empty command" };
|
|
1964
1565
|
if (written === "coproc")
|
|
1965
|
-
return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command"
|
|
1566
|
+
return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command" };
|
|
1966
1567
|
if (isBareControlWord(toks, from, SHELL_BLOCK_WORDS)) {
|
|
1967
1568
|
unlistedSegments.add(si);
|
|
1968
1569
|
nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
|
|
@@ -1976,13 +1577,13 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1976
1577
|
}
|
|
1977
1578
|
const parsed = parseLeadingCommandName(toks.raw.slice(from).join(" "), { quotedOperatorsAreText: true, pathPrefixedNameIsText: true });
|
|
1978
1579
|
if ("reject" in parsed)
|
|
1979
|
-
return { reason: parsed.reject
|
|
1580
|
+
return { reason: parsed.reject };
|
|
1980
1581
|
const pathPrefixed = parsed.name.includes("/");
|
|
1981
1582
|
const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
|
|
1982
1583
|
if (pathPrefixed)
|
|
1983
1584
|
nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
|
|
1984
1585
|
if (pos.refusal !== undefined)
|
|
1985
|
-
return { reason: programPositionRefusalSentence(pos.refusal)
|
|
1586
|
+
return { reason: programPositionRefusalSentence(pos.refusal) };
|
|
1986
1587
|
if (allow.has(name)) {
|
|
1987
1588
|
if (pathPrefixed || from > 0)
|
|
1988
1589
|
reheaded.set(si, { from, head: name });
|
|
@@ -1992,14 +1593,14 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1992
1593
|
}
|
|
1993
1594
|
nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
|
|
1994
1595
|
if (SHELL_REENTRY_PROGRAM_RE.test(name))
|
|
1995
|
-
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text")
|
|
1596
|
+
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text") };
|
|
1996
1597
|
if (pos.launchers.length > 0) {
|
|
1997
1598
|
const listed = program !== undefined && (allow.has(program.basename) || program.basename === "cd");
|
|
1998
1599
|
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
|
|
1600
|
+
return { reason: `"${written}" runs "${program.basename}", which runs its argument as shell text this classifier does not read ${NOT_AUTO_ALLOWED}` };
|
|
2000
1601
|
}
|
|
2001
1602
|
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
|
|
1603
|
+
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
1604
|
}
|
|
2004
1605
|
if (!listed || program === undefined)
|
|
2005
1606
|
unlistedSegments.add(si);
|
|
@@ -2039,44 +1640,16 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
2039
1640
|
};
|
|
2040
1641
|
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
1642
|
const foldedSegments = [];
|
|
2042
|
-
const foldedReach = [];
|
|
2043
|
-
let crossedAllAnd = true;
|
|
2044
|
-
let crossedAnyOr = false;
|
|
2045
1643
|
let shapeRefusal = nameRefusal;
|
|
2046
1644
|
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();
|
|
1645
|
+
if (unlistedSegments.has(si))
|
|
2071
1646
|
continue;
|
|
2072
|
-
}
|
|
2073
1647
|
const rehead = reheaded.get(si);
|
|
2074
1648
|
const segmentTokens = rehead === undefined ? tokenizeSegment(segments[si]) : reheadSegment(tokenizeSegment(segments[si]), rehead.head, rehead.from);
|
|
2075
1649
|
const toks = segmentTokens.folded;
|
|
2076
1650
|
if (toks.length === 0)
|
|
2077
1651
|
continue;
|
|
2078
1652
|
foldedSegments.push(segmentTokens);
|
|
2079
|
-
pushReach();
|
|
2080
1653
|
const name = toks[0];
|
|
2081
1654
|
const guardReason = guardedVerbFormReason(name, toks, segmentTokens.raw);
|
|
2082
1655
|
if (guardReason !== undefined) {
|
|
@@ -2152,311 +1725,13 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
2152
1725
|
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
1726
|
}
|
|
2154
1727
|
}
|
|
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 {};
|
|
1728
|
+
const cdSegments = foldedSegments.filter((st) => st.folded[0] === "cd");
|
|
1729
|
+
if (cdSegments.length > 1)
|
|
1730
|
+
shapeRefusal ??= `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}`;
|
|
1731
|
+
return shapeRefusal !== undefined ? { reason: shapeRefusal } : {};
|
|
2179
1732
|
}
|
|
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
1733
|
export const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
2377
1734
|
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
1735
|
export function classifyCompoundReadonly(command, allow, boundary) {
|
|
2461
1736
|
return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
|
|
2462
1737
|
}
|
|
@@ -2549,18 +1824,5 @@ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary
|
|
|
2549
1824
|
if (readSegments.length === 0) {
|
|
2550
1825
|
return { reason: "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed" };
|
|
2551
1826
|
}
|
|
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;
|
|
1827
|
+
return classifyCompoundReadonlyDetailed(readSegments.join("; "), allow, boundary);
|
|
2566
1828
|
}
|