@sema-agent/core 7.21.1 → 7.22.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 +123 -1
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +2 -1
- package/dist/core/ask-origin.d.ts +1 -0
- package/dist/core/ask-origin.js +3 -2
- package/dist/core/auto-mode-rebuild.js +6 -2
- package/dist/core/auto-mode.d.ts +40 -18
- package/dist/core/auto-mode.js +63 -45
- package/dist/core/checkpoint-store.d.ts +8 -8
- package/dist/core/engine-notice.d.ts +49 -1
- package/dist/core/engine-notice.js +14 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/mcp.d.ts +65 -0
- package/dist/core/mcp.js +190 -41
- package/dist/core/memory-engine/consolidation.d.ts +5 -0
- package/dist/core/memory-engine/consolidation.js +7 -3
- package/dist/core/memory-engine/engine.d.ts +8 -0
- package/dist/core/memory-engine/engine.js +25 -19
- package/dist/core/memory-engine/layout.d.ts +47 -9
- package/dist/core/memory-engine/layout.js +157 -57
- package/dist/core/memory-engine/session-incarnation.d.ts +27 -0
- package/dist/core/memory-engine/session-incarnation.js +63 -0
- package/dist/core/parent-spec-seat.d.ts +15 -3
- package/dist/core/parent-spec-seat.js +5 -0
- package/dist/core/permission-rule-model.d.ts +1 -2
- package/dist/core/retention.d.ts +8 -1
- package/dist/core/retention.js +2 -0
- package/dist/core/runner/classifier-ladder.js +1 -0
- package/dist/core/runner/contracts.d.ts +11 -0
- package/dist/core/runner/mcp-redial.d.ts +109 -0
- package/dist/core/runner/mcp-redial.js +60 -0
- package/dist/core/runner/memory-consolidation-driver.d.ts +2 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +9 -12
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -0
- package/dist/core/runner/prepare-hands-readface.js +13 -3
- package/dist/core/runner/prepare-memory.js +3 -3
- package/dist/core/runner/prepare-protocol-tools.d.ts +6 -1
- package/dist/core/runner/prepare-protocol-tools.js +20 -38
- package/dist/core/runner/prepare-task.js +3 -3
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
- package/dist/core/runner/prepare-wiring-manifest.js +2 -1
- package/dist/core/runner/run-identity-wiring.d.ts +3 -1
- package/dist/core/runner/run-identity-wiring.js +4 -1
- package/dist/core/runner/run-settle-and-teardown.d.ts +3 -1
- package/dist/core/runner/run-settle-and-teardown.js +6 -2
- package/dist/core/runner/run-terminal-adoption.d.ts +4 -1
- package/dist/core/runner/run-terminal-adoption.js +2 -6
- package/dist/core/runner/runtask.js +8 -5
- package/dist/core/runner/stream-lifecycle-verbs.d.ts +5 -1
- package/dist/core/runner/stream-lifecycle-verbs.js +23 -2
- package/dist/core/runner-deps.d.ts +16 -7
- package/dist/core/skill-listing-budget.d.ts +5 -4
- package/dist/core/task-result.d.ts +3 -2
- package/dist/core/task-spec.d.ts +2 -1
- package/dist/core/task-stream.d.ts +26 -0
- package/dist/core/tool-spec.d.ts +5 -1
- package/dist/core/trace.d.ts +5 -0
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +23 -0
- package/dist/core/wiring-manifest.js +3 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +2 -2
- package/dist/tools/fs/bash-program-position.d.ts +26 -5
- package/dist/tools/fs/bash-program-position.js +63 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +27 -17
- package/dist/tools/fs/bash-readonly-classifier.js +134 -55
- package/dist/tools/fs/fs-bash.js +43 -18
- package/dist/tools/fs/index.d.ts +4 -2
- package/dist/tools/fs/read-deny.d.ts +25 -11
- package/dist/tools/fs/read-deny.js +5 -1
- package/dist/tools/fs/safety.d.ts +1 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -1
- package/wire-changes/core@7.22.0.json +637 -0
|
@@ -12,23 +12,33 @@ export function isBareControlWord(toks, i, words) {
|
|
|
12
12
|
export const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
|
|
13
13
|
export const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
|
|
14
14
|
const NO_DECLARED_OPTIONS = { optionArity: new Map() };
|
|
15
|
+
const launcherRow = (pairs, extra = {}) => ({ optionArity: new Map(pairs), ...extra });
|
|
15
16
|
export const LAUNCHER_TABLE = new Map([
|
|
16
|
-
["env",
|
|
17
|
+
["env", launcherRow([["-i", 0], ["-0", 0], ["-v", 0], ["--ignore-environment", 0], ["--null", 0], ["--debug", 0], ["-u", 1], ["--unset", 1]], { assignmentOperands: true })],
|
|
18
|
+
["command", launcherRow([["-p", 0]])],
|
|
19
|
+
["builtin", NO_DECLARED_OPTIONS], ["exec", NO_DECLARED_OPTIONS],
|
|
17
20
|
["noglob", NO_DECLARED_OPTIONS], ["xargs", NO_DECLARED_OPTIONS], ["busybox", NO_DECLARED_OPTIONS],
|
|
18
|
-
["sudo",
|
|
19
|
-
["
|
|
20
|
-
["
|
|
21
|
-
["
|
|
21
|
+
["sudo", { optionArity: new Map(), assignmentOperands: true }], ["doas", NO_DECLARED_OPTIONS], ["pkexec", { optionArity: new Map(), whenNoProgram: "shell" }],
|
|
22
|
+
["su", { optionArity: new Map(), shellText: true }], ["runuser", { optionArity: new Map(), shellText: true }],
|
|
23
|
+
["chroot", { optionArity: new Map(), requiredOperands: 1, whenNoProgram: "shell" }], ["setpriv", NO_DECLARED_OPTIONS],
|
|
24
|
+
["nohup", NO_DECLARED_OPTIONS],
|
|
25
|
+
["nice", launcherRow([["-n", 1], ["--adjustment", 1]], { negativeIntegerIsOwnArgument: true })],
|
|
26
|
+
["ionice", NO_DECLARED_OPTIONS], ["chrt", { optionArity: new Map(), requiredOperands: 1 }], ["taskset", { optionArity: new Map(), requiredOperands: 1 }],
|
|
27
|
+
["stdbuf", launcherRow([["-i", 1], ["-o", 1], ["-e", 1], ["--input", 1], ["--output", 1], ["--error", 1]])],
|
|
28
|
+
["timeout", launcherRow([
|
|
29
|
+
["--foreground", 0], ["--preserve-status", 0], ["--verbose", 0], ["-v", 0],
|
|
30
|
+
["-k", 1], ["-s", 1], ["--kill-after", 1], ["--signal", 1],
|
|
31
|
+
], { requiredOperands: 1 })],
|
|
22
32
|
["time", {
|
|
23
33
|
optionArity: new Map([
|
|
24
34
|
["-p", 0], ["-a", 0], ["-v", 0], ["-q", 0], ["--portability", 0], ["--append", 0], ["--verbose", 0], ["--quiet", 0], ["--help", 0], ["--version", 0],
|
|
25
35
|
["-f", 1], ["-o", 1], ["--format", 1], ["--output", 1],
|
|
26
36
|
]),
|
|
27
37
|
}],
|
|
28
|
-
["watch",
|
|
29
|
-
["nsenter",
|
|
38
|
+
["watch", { optionArity: new Map(), shellText: true }], ["setsid", NO_DECLARED_OPTIONS], ["flock", { optionArity: new Map(), requiredOperands: 1 }], ["unshare", { optionArity: new Map(), whenNoProgram: "shell" }],
|
|
39
|
+
["nsenter", { optionArity: new Map(), whenNoProgram: "shell" }], ["script", { optionArity: new Map(), shellText: true }], ["numactl", NO_DECLARED_OPTIONS], ["prlimit", NO_DECLARED_OPTIONS],
|
|
30
40
|
["systemd-run", NO_DECLARED_OPTIONS],
|
|
31
|
-
["strace", NO_DECLARED_OPTIONS], ["ltrace", NO_DECLARED_OPTIONS], ["valgrind", NO_DECLARED_OPTIONS], ["firejail",
|
|
41
|
+
["strace", NO_DECLARED_OPTIONS], ["ltrace", NO_DECLARED_OPTIONS], ["valgrind", NO_DECLARED_OPTIONS], ["firejail", { optionArity: new Map(), whenNoProgram: "shell" }],
|
|
32
42
|
["bwrap", NO_DECLARED_OPTIONS],
|
|
33
43
|
]);
|
|
34
44
|
export const COMMAND_LAUNCHERS = new Set(LAUNCHER_TABLE.keys());
|
|
@@ -38,15 +48,33 @@ function pastLauncherOptions(launcher, folded, i) {
|
|
|
38
48
|
return i;
|
|
39
49
|
for (;;) {
|
|
40
50
|
const tok = folded[i];
|
|
41
|
-
|
|
42
|
-
if (arity === undefined)
|
|
51
|
+
if (tok === undefined)
|
|
43
52
|
return i;
|
|
44
|
-
if (
|
|
53
|
+
if (row.negativeIntegerIsOwnArgument === true && /^-\d+$/.test(tok)) {
|
|
54
|
+
i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const step = launcherOptionStep(row, tok);
|
|
58
|
+
if (step === undefined)
|
|
59
|
+
return i;
|
|
60
|
+
if (step === 2 && folded[i + 1] === undefined)
|
|
45
61
|
return i;
|
|
46
|
-
i +=
|
|
62
|
+
i += step;
|
|
47
63
|
}
|
|
48
64
|
}
|
|
49
|
-
|
|
65
|
+
function launcherOptionStep(row, tok) {
|
|
66
|
+
const exact = row.optionArity.get(tok);
|
|
67
|
+
if (exact !== undefined)
|
|
68
|
+
return exact === 1 ? 2 : 1;
|
|
69
|
+
const eq = tok.startsWith("--") ? tok.indexOf("=") : -1;
|
|
70
|
+
if (eq > 2 && row.optionArity.get(tok.slice(0, eq)) === 1)
|
|
71
|
+
return 1;
|
|
72
|
+
if (/^-[A-Za-z]./.test(tok) && row.optionArity.get(tok.slice(0, 2)) === 1)
|
|
73
|
+
return 1;
|
|
74
|
+
if (/^-[A-Za-z]+$/.test(tok) && [...tok.slice(1)].every((ch) => row.optionArity.get(`-${ch}`) === 0))
|
|
75
|
+
return 1;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
50
78
|
export const PROGRAM_POSITION_REFUSAL_KINDS = ["option_at_program_position", "control_syntax_at_program_position"];
|
|
51
79
|
export function readProgramPosition(toks) {
|
|
52
80
|
const { folded } = toks;
|
|
@@ -79,21 +107,38 @@ export function readProgramPosition(toks) {
|
|
|
79
107
|
}
|
|
80
108
|
const launchers = [{ index: i, name: basename }];
|
|
81
109
|
const owner = head;
|
|
110
|
+
const shellRefusal = () => ({ kind: "control_syntax_at_program_position", owner, word: "sh" });
|
|
111
|
+
let owedOperands = LAUNCHER_TABLE.get(basename)?.requiredOperands ?? 0;
|
|
112
|
+
let lastRow = LAUNCHER_TABLE.get(basename);
|
|
113
|
+
const chain0 = (program, refusal) => ({ controlWords, written, assignments, launchers, argvFromStdin: false, runsDefaultProgram: false, program, refusal });
|
|
114
|
+
if (lastRow?.shellText === true)
|
|
115
|
+
return chain0(undefined, shellRefusal());
|
|
82
116
|
let argvFromStdin = ARGV_REENTRY_PROGRAM_RE.test(basename);
|
|
83
117
|
let lastReentry = argvFromStdin ? i : -1;
|
|
84
118
|
const chain = (program, refusal) => ({ controlWords, written, assignments, launchers, argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1, program, refusal });
|
|
85
119
|
for (let j = pastLauncherOptions(basename, folded, i + 1); j < folded.length; j++) {
|
|
86
120
|
const t = folded[j];
|
|
87
|
-
if (
|
|
121
|
+
if (owedOperands > 0 && !t.startsWith("-")) {
|
|
122
|
+
owedOperands -= 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (lastRow?.assignmentOperands === true && !t.startsWith("-") && t.includes("="))
|
|
88
126
|
continue;
|
|
89
127
|
const wordBasename = programBasename(t);
|
|
90
128
|
if (ARGV_REENTRY_PROGRAM_RE.test(wordBasename)) {
|
|
91
129
|
argvFromStdin = true;
|
|
92
130
|
lastReentry = j;
|
|
131
|
+
lastRow = undefined;
|
|
132
|
+
owedOperands = 0;
|
|
93
133
|
continue;
|
|
94
134
|
}
|
|
95
135
|
if (COMMAND_LAUNCHERS.has(wordBasename)) {
|
|
96
136
|
launchers.push({ index: j, name: wordBasename });
|
|
137
|
+
lastRow = LAUNCHER_TABLE.get(wordBasename);
|
|
138
|
+
if (lastRow?.shellText === true)
|
|
139
|
+
return chain(undefined, shellRefusal());
|
|
140
|
+
owedOperands = lastRow?.requiredOperands ?? 0;
|
|
141
|
+
j = pastLauncherOptions(wordBasename, folded, j + 1) - 1;
|
|
97
142
|
continue;
|
|
98
143
|
}
|
|
99
144
|
if (t.startsWith("-"))
|
|
@@ -103,13 +148,15 @@ export function readProgramPosition(toks) {
|
|
|
103
148
|
}
|
|
104
149
|
return chain({ index: j, name: t, basename: wordBasename });
|
|
105
150
|
}
|
|
151
|
+
if (lastRow?.whenNoProgram === "shell")
|
|
152
|
+
return chain(undefined, shellRefusal());
|
|
106
153
|
return chain(undefined);
|
|
107
154
|
}
|
|
108
|
-
export function argvReentryUnjudged(pos,
|
|
155
|
+
export function argvReentryUnjudged(pos, acceptedTargets) {
|
|
109
156
|
if (!pos.argvFromStdin)
|
|
110
157
|
return false;
|
|
111
158
|
const runs = pos.program?.basename ?? (pos.runsDefaultProgram ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
|
|
112
|
-
return runs === undefined || !
|
|
159
|
+
return runs === undefined || !acceptedTargets.has(runs);
|
|
113
160
|
}
|
|
114
161
|
export function programPositionRefusalSentence(refusal) {
|
|
115
162
|
switch (refusal.kind) {
|
|
@@ -293,6 +293,15 @@ export interface BashReadonlyRootBoundary {
|
|
|
293
293
|
* roots containment + command allowlist double gate is the deployment's own read-safety
|
|
294
294
|
* declaration — v1 ruling). */
|
|
295
295
|
denyMatch?: (resolvedPath: string) => string | null;
|
|
296
|
+
/** What the wired {@link denyMatch} judge would apply — the ACTIVE deny patterns. Read for ONE question:
|
|
297
|
+
* is there anything for a traversal to defeat? EMPTY ⇒ the judge refuses nothing, so a walk under it
|
|
298
|
+
* bypasses nothing and {@link CompoundReadonlyVerdict.recursiveReadPaths} is not minted. ABSENT ⇒ the
|
|
299
|
+
* caller did not say, which is not "none": the walk is reported, the reading every caller had before
|
|
300
|
+
* this seat existed. It is deliberately NOT the reference implementation's question at the same place —
|
|
301
|
+
* its deny rules are ROOTED, so "is the guarded place inside this traversal?" is lexically answerable
|
|
302
|
+
* there; these patterns match a segment run ANYWHERE in a path ({@link
|
|
303
|
+
* import("./read-deny.js").ReadDenyEntry}), so only emptiness is. */
|
|
304
|
+
denyEntries?: readonly string[];
|
|
296
305
|
/** The resolved read-face containment state. Under "open" the CONTAINMENT half of
|
|
297
306
|
* this boundary is structurally satisfied (an out-of-roots operand is not a demotion; it reports
|
|
298
307
|
* as a checked candidate instead), while the deny half above keeps judging in both faces.
|
|
@@ -425,20 +434,25 @@ export interface CompoundReadonlyVerdict {
|
|
|
425
434
|
*/
|
|
426
435
|
recursiveReadPaths?: readonly string[];
|
|
427
436
|
/**
|
|
428
|
-
* The ONE
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* declared home or a declared {@link BashReadonlyRootBoundary.roots} entry. Meaning and spelling
|
|
439
|
-
* contract: {@link import("../../core/tool-spec.js").ReadRootGrantCandidate}.
|
|
437
|
+
* The ONE path whose addition to the declared roots would make every objection THIS WALK raised go away,
|
|
438
|
+
* MEASURED not argued: the walk runs again with the proposed root in place and the value is offered only
|
|
439
|
+
* if that walk has no objection left (`..` spellings are excluded on their own account — a folded-vs-kernel
|
|
440
|
+
* disagreement no replay of the text settles). It rides the containment sentence, and a per-segment NAME
|
|
441
|
+
* refusal ({@link segmentNameRefusal}), which is not a refusal of the READ; a SHAPE refusal takes it with
|
|
442
|
+
* it. VALUE: the deepest directory PROPERLY containing every {@link outOfRootPaths} entry, folded (zero
|
|
443
|
+
* I/O, so a directory contributes its PARENT); where that would contain the declared home or a root, the
|
|
444
|
+
* single path read is offered VERBATIM instead ({@link outOfRootGrantCoversExactPath}) — `cat
|
|
445
|
+
* ~/.gitconfig` is one file and the alternative is a card with no way out. Contract:
|
|
446
|
+
* {@link import("../../core/tool-spec.js").ReadRootGrantCandidate}.
|
|
440
447
|
*/
|
|
441
448
|
outOfRootGrantCandidate?: string;
|
|
449
|
+
/** {@link outOfRootGrantCandidate} is the path the command NAMED, offered verbatim, rather than a
|
|
450
|
+
* directory containing it — minted where that directory would have handed out the home or the workspace.
|
|
451
|
+
* A consumer adds the string ITSELF; expanding it into a directory pattern is what this prevents. */
|
|
452
|
+
outOfRootGrantCoversExactPath?: true;
|
|
453
|
+
/** The verdict's {@link reason} is a per-segment NAME refusal (an unlisted program, a path-prefixed name,
|
|
454
|
+
* a control word before a reader) rather than the walk's own sentence or a refused SHAPE. */
|
|
455
|
+
segmentNameRefusal?: true;
|
|
442
456
|
}
|
|
443
457
|
/**
|
|
444
458
|
* The single minting point for the out-of-root-read approval option text, so a gate rendering
|
|
@@ -745,11 +759,7 @@ export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
|
|
|
745
759
|
* run. (The substitution is a flagged, high-severity shape kept per the ruling that names
|
|
746
760
|
* `$(seq <int> <int>)` as the one exemption — recorded for a
|
|
747
761
|
* tightening decision, since the brace form is a drop-in equivalent.)
|
|
748
|
-
* · STDIN-BLOCKING BODY (an availability residual, not a safety one).
|
|
749
|
-
* used to record: a quoted multiword operand like `grep "x y"` read, to the then whitespace-blind
|
|
750
|
-
* tokenizer, as two operands meeting grep's file floor, while bash passes ONE pattern and no file and
|
|
751
|
-
* grep blocks on stdin. the lexer now counts the operand bash actually passes, so
|
|
752
|
-
* that command demotes on both this arm and the plain face. What REMAINS residual is the general
|
|
762
|
+
* · STDIN-BLOCKING BODY (an availability residual, not a safety one). What REMAINS residual is the general
|
|
753
763
|
* statement: this arm inherits whatever the shared availability arms can see, they are best-effort
|
|
754
764
|
* (an operand that only turns out to be unreadable at run time is not in the text), and a hang is
|
|
755
765
|
* bounded by the tool timeout — outside the irreversibility / egress envelope this arm governs. The
|
|
@@ -173,18 +173,18 @@ function outOfRootGrantCandidateOf(paths, boundary) {
|
|
|
173
173
|
}
|
|
174
174
|
if (candidate === undefined)
|
|
175
175
|
return undefined;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return candidate;
|
|
176
|
+
const tooWide = parentDirLexically(candidate) === candidate ||
|
|
177
|
+
(boundary.homeDir !== undefined && withinAnyRoot([candidate], boundary.homeDir)) ||
|
|
178
|
+
boundary.roots.some((root) => withinAnyRoot([candidate], root));
|
|
179
|
+
if (tooWide)
|
|
180
|
+
return paths.length === 1 ? { path: paths[0], exact: true } : undefined;
|
|
181
|
+
return { path: candidate };
|
|
183
182
|
}
|
|
184
183
|
const NO_PATH_OPERAND_COMMANDS = new Set([
|
|
185
|
-
"pwd", "echo", "whoami", "uname", "which", "tr", "basename", "dirname",
|
|
184
|
+
"pwd", "echo", "printf", "whoami", "uname", "which", "tr", "basename", "dirname",
|
|
186
185
|
"cal", "uptime", "id", "groups", "nproc", "locale", "free", "true", "false", "seq", "expr", "type", "sleep",
|
|
187
186
|
]);
|
|
187
|
+
const XARGS_TARGETS_ACCEPTED_UNJUDGED = NO_PATH_OPERAND_COMMANDS;
|
|
188
188
|
function isPathShapedToken(tok) {
|
|
189
189
|
return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
|
|
190
190
|
}
|
|
@@ -322,6 +322,9 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
322
322
|
if (!assignmentJudgedInside(words.folded[k]))
|
|
323
323
|
return false;
|
|
324
324
|
}
|
|
325
|
+
const argv = argvProgramOf(words);
|
|
326
|
+
const argvStart = argv?.from ?? words.folded.length;
|
|
327
|
+
const declaredNotPaths = ASSIGNMENT_VALUE_NOT_PATHS.has(argv?.program ?? "");
|
|
325
328
|
let endOfOptions = false;
|
|
326
329
|
for (let j = from; j < words.folded.length; j++) {
|
|
327
330
|
const word = words.folded[j];
|
|
@@ -340,7 +343,7 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
340
343
|
if (!stdinFileJudgedInside(candidate))
|
|
341
344
|
return false;
|
|
342
345
|
}
|
|
343
|
-
if (!optionShaped && !assignmentJudgedInside(word))
|
|
346
|
+
if (!optionShaped && (j < argvStart || !declaredNotPaths) && !assignmentJudgedInside(word))
|
|
344
347
|
return false;
|
|
345
348
|
}
|
|
346
349
|
return true;
|
|
@@ -362,7 +365,7 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
362
365
|
if (program === undefined) {
|
|
363
366
|
if (pos.launchers.length === 0)
|
|
364
367
|
continue;
|
|
365
|
-
if (argvReentryUnjudged(pos,
|
|
368
|
+
if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED))
|
|
366
369
|
return false;
|
|
367
370
|
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
|
|
368
371
|
return false;
|
|
@@ -370,12 +373,10 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
370
373
|
}
|
|
371
374
|
if (program.name.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(program.name))
|
|
372
375
|
return false;
|
|
373
|
-
if (argumentAsProgramRefusal(program.basename, program.basename, words.folded.slice(program.index + 1)) !== undefined)
|
|
374
|
-
return false;
|
|
375
376
|
if (SHELL_REENTRY_PROGRAM_RE.test(program.basename))
|
|
376
377
|
return false;
|
|
377
378
|
if (pos.launchers.length > 0) {
|
|
378
|
-
if (argvReentryUnjudged(pos,
|
|
379
|
+
if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED))
|
|
379
380
|
return false;
|
|
380
381
|
if (!listed(program.basename)) {
|
|
381
382
|
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
|
|
@@ -609,6 +610,32 @@ const VERB_OPTION_SLOTS = {
|
|
|
609
610
|
"verbose", "verify", "version",
|
|
610
611
|
],
|
|
611
612
|
},
|
|
613
|
+
find: {
|
|
614
|
+
shortValue: { D: "label", O: "count", f: "path" },
|
|
615
|
+
shortPlain: "HLPdsx",
|
|
616
|
+
longValue: {},
|
|
617
|
+
longOptionalValue: {},
|
|
618
|
+
longPlain: ["help", "version"],
|
|
619
|
+
dashWordValue: {
|
|
620
|
+
amin: "count", anewer: "path", atime: "count", cmin: "count", cnewer: "path", context: "label",
|
|
621
|
+
ctime: "count", fstype: "label", gid: "count", group: "label", ilname: "path", iname: "name-pattern",
|
|
622
|
+
inum: "count", ipath: "path", iregex: "name-pattern", iwholename: "path", links: "count",
|
|
623
|
+
lname: "path", maxdepth: "count", mindepth: "count", mmin: "count", mtime: "count", name: "name-pattern",
|
|
624
|
+
newer: "path", newerat: "path", neweraa: "path", neweram: "path", newerac: "path",
|
|
625
|
+
newerct: "path", newerca: "path", newercm: "path", newercc: "path",
|
|
626
|
+
newermt: "path", newerma: "path", newermm: "path", newermc: "path",
|
|
627
|
+
newerbt: "path", newerba: "path", newerbm: "path", newerbc: "path",
|
|
628
|
+
path: "path", perm: "label", printf: "label", regex: "name-pattern", regextype: "label",
|
|
629
|
+
samefile: "path", size: "count", type: "label", uid: "count", used: "count", user: "label",
|
|
630
|
+
wholename: "path", xtype: "label",
|
|
631
|
+
},
|
|
632
|
+
dashWordPlain: [
|
|
633
|
+
"and", "daystart", "depth", "empty", "executable", "false", "follow", "help", "ignore_readdir_race",
|
|
634
|
+
"mount", "noignore_readdir_race", "noleaf", "not", "nogroup", "nouser", "or", "print",
|
|
635
|
+
"print0", "prune", "quit", "readable", "true", "version", "writable", "xdev",
|
|
636
|
+
],
|
|
637
|
+
optionsArePositionIndependent: true,
|
|
638
|
+
},
|
|
612
639
|
curl: {
|
|
613
640
|
shortValue: {
|
|
614
641
|
A: "label", b: "indirect", C: "count", c: "path", D: "path", d: "indirect", E: "path", e: "label",
|
|
@@ -699,6 +726,17 @@ function readVerbOptionSlot(name, tok) {
|
|
|
699
726
|
return { form: "unexplained" };
|
|
700
727
|
return withValue([...kinds][0]);
|
|
701
728
|
}
|
|
729
|
+
const word = row.dashWordValue !== undefined || row.dashWordPlain !== undefined ? /^-([A-Za-z_][\w-]+)$/.exec(tok)?.[1] : undefined;
|
|
730
|
+
if (word !== undefined) {
|
|
731
|
+
if (row.shortValue[word[0]] !== undefined)
|
|
732
|
+
return { form: "unexplained" };
|
|
733
|
+
const kind = row.dashWordValue?.[word];
|
|
734
|
+
if (kind !== undefined)
|
|
735
|
+
return { form: "separated", kind };
|
|
736
|
+
if (row.dashWordPlain?.includes(word) === true)
|
|
737
|
+
return { form: "no-value" };
|
|
738
|
+
return { form: "unexplained" };
|
|
739
|
+
}
|
|
702
740
|
for (let i = 1; i < tok.length; i++) {
|
|
703
741
|
const ch = tok[i];
|
|
704
742
|
const kind = row.shortValue[ch];
|
|
@@ -711,6 +749,9 @@ function readVerbOptionSlot(name, tok) {
|
|
|
711
749
|
}
|
|
712
750
|
return { form: "no-value" };
|
|
713
751
|
}
|
|
752
|
+
function verbOptionsArePositionIndependent(name) {
|
|
753
|
+
return VERB_OPTION_SLOTS[name]?.optionsArePositionIndependent === true;
|
|
754
|
+
}
|
|
714
755
|
function localFileUrlPath(value) {
|
|
715
756
|
const m = /^file:(?:\/\/[^/?#]*)?(\/[^?#]*)/i.exec(value.trim());
|
|
716
757
|
if (m === null)
|
|
@@ -800,23 +841,27 @@ function takesSeparatedValue(name, tok) {
|
|
|
800
841
|
}
|
|
801
842
|
return false;
|
|
802
843
|
}
|
|
844
|
+
const GREP_RECURSIVE_READ_FORM = {
|
|
845
|
+
discloses: "contents",
|
|
846
|
+
shortLetters: "rR",
|
|
847
|
+
valueOwners: "efmABCD",
|
|
848
|
+
longNames: ["recursive", "dereference-recursive"],
|
|
849
|
+
enumOptions: [{ shortLetter: "d", longName: "directories", recursiveValue: "recurse" }],
|
|
850
|
+
dashIsStdin: true,
|
|
851
|
+
};
|
|
803
852
|
const RECURSIVE_READ_FORMS = {
|
|
804
|
-
grep:
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
},
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
ag: { always: true, dashIsStdin: true },
|
|
817
|
-
ack: { always: true, dashIsStdin: true },
|
|
818
|
-
tar: { shortLetters: "cru", valueOwners: "fCTXbg", longNames: ["create", "append", "update"], bundledModeLetters: "cru", dashIsStdin: true },
|
|
819
|
-
diff: { shortLetters: "r", valueOwners: "UCWISFXx", longNames: ["recursive"], dashIsStdin: true },
|
|
853
|
+
grep: GREP_RECURSIVE_READ_FORM,
|
|
854
|
+
egrep: GREP_RECURSIVE_READ_FORM,
|
|
855
|
+
fgrep: GREP_RECURSIVE_READ_FORM,
|
|
856
|
+
ls: { discloses: "names", shortLetters: "R", longNames: ["recursive"] },
|
|
857
|
+
du: { discloses: "names", always: true },
|
|
858
|
+
find: { discloses: "names", always: true },
|
|
859
|
+
rg: { discloses: "contents", always: true, dashIsStdin: true },
|
|
860
|
+
tree: { discloses: "names", always: true },
|
|
861
|
+
ag: { discloses: "contents", always: true, dashIsStdin: true },
|
|
862
|
+
ack: { discloses: "contents", always: true, dashIsStdin: true },
|
|
863
|
+
tar: { discloses: "contents", shortLetters: "cru", valueOwners: "fCTXbg", longNames: ["create", "append", "update"], bundledModeLetters: "cru", dashIsStdin: true },
|
|
864
|
+
diff: { discloses: "contents", shortLetters: "r", valueOwners: "UCWISFXx", longNames: ["recursive"], dashIsStdin: true },
|
|
820
865
|
};
|
|
821
866
|
const FIND_DANGEROUS_PREDICATES = new Set([
|
|
822
867
|
"-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprint0", "-fls", "-fprintf", "-files0-from",
|
|
@@ -1458,6 +1503,8 @@ function segmentSelectsRecursiveRead(name, args) {
|
|
|
1458
1503
|
const model = RECURSIVE_READ_FORMS[name];
|
|
1459
1504
|
if (model === undefined)
|
|
1460
1505
|
return false;
|
|
1506
|
+
if (model.discloses !== "contents")
|
|
1507
|
+
return false;
|
|
1461
1508
|
if (model.always === true)
|
|
1462
1509
|
return true;
|
|
1463
1510
|
if (model.bundledModeLetters !== undefined) {
|
|
@@ -1540,6 +1587,17 @@ function assignmentWordPayload(word) {
|
|
|
1540
1587
|
return { kind: "unresolved", reason: `"${word}" carries a tilde whose expansion the text does not settle` };
|
|
1541
1588
|
return value === "" ? undefined : { kind: "value", value };
|
|
1542
1589
|
}
|
|
1590
|
+
const ASSIGNMENT_VALUE_NOT_PATHS = new Set(["make", "gmake", "bmake", "cmake", "ninja", "meson", "docker", "podman"]);
|
|
1591
|
+
function argvProgramOf(tokens) {
|
|
1592
|
+
let lead = 0;
|
|
1593
|
+
while (lead < tokens.folded.length && ASSIGNMENT_OR_APPEND_WORD.test(tokens.folded[lead]))
|
|
1594
|
+
lead++;
|
|
1595
|
+
const pos = readProgramPosition({ folded: tokens.folded.slice(lead), raw: tokens.raw.slice(lead) });
|
|
1596
|
+
if (pos.program === undefined)
|
|
1597
|
+
return undefined;
|
|
1598
|
+
const at = lead + pos.program.index;
|
|
1599
|
+
return ASSIGNMENT_OR_APPEND_WORD.test(tokens.folded[at] ?? "") ? undefined : { program: pos.program.basename, from: at };
|
|
1600
|
+
}
|
|
1543
1601
|
function attachedOptionPayloads(tok) {
|
|
1544
1602
|
const literal = literalTildeSpelling;
|
|
1545
1603
|
const out = [];
|
|
@@ -1564,9 +1622,12 @@ function attachedOptionPayloads(tok) {
|
|
|
1564
1622
|
function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
1565
1623
|
const name = tokens.folded[0];
|
|
1566
1624
|
const slotVerb = tokens.slotVerb ?? name;
|
|
1625
|
+
const declaredNotPaths = ASSIGNMENT_VALUE_NOT_PATHS.has(tokens.argvProgram ?? slotVerb);
|
|
1626
|
+
const argvFrom = tokens.argvFrom ?? (tokens.slotVerb === undefined ? 1 : Number.MAX_SAFE_INTEGER);
|
|
1567
1627
|
if (NO_PATH_OPERAND_COMMANDS.has(name))
|
|
1568
1628
|
return [];
|
|
1569
|
-
const
|
|
1629
|
+
const declaresNoPathOperands = tokens.slotVerb !== undefined && NO_PATH_OPERAND_COMMANDS.has(tokens.slotVerb) && tokens.argvFrom !== undefined;
|
|
1630
|
+
const args = declaresNoPathOperands ? tokens.folded.slice(1, tokens.argvFrom + 1) : tokens.folded.slice(1);
|
|
1570
1631
|
const findings = [];
|
|
1571
1632
|
const candidates = [];
|
|
1572
1633
|
const bareWordOperands = [];
|
|
@@ -1574,6 +1635,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1574
1635
|
(isPathShapedToken(text) || isAbsolutePathForm(text) ? candidates : bareWordOperands).push({ text, globbed });
|
|
1575
1636
|
};
|
|
1576
1637
|
const positionalOperands = [];
|
|
1638
|
+
const argvOperandsStartAt = tokens.slotVerb !== undefined ? 1 : 0;
|
|
1577
1639
|
let stdinOperand = false;
|
|
1578
1640
|
const argGlobs = (k) => {
|
|
1579
1641
|
const raw = tokens.raw[k + 1];
|
|
@@ -1610,7 +1672,6 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1610
1672
|
const eoo = args.indexOf("--");
|
|
1611
1673
|
const patternSuppliedByFlag = name === "grep" && (eoo === -1 ? args : args.slice(0, eoo)).some(isGrepPatternFlagToken);
|
|
1612
1674
|
let sawOperand = false;
|
|
1613
|
-
const argvOperandsStartAt = tokens.slotVerb !== undefined ? 1 : 0;
|
|
1614
1675
|
let sawArgvOperand = false;
|
|
1615
1676
|
let endOfOptions = false;
|
|
1616
1677
|
for (let k = 0; k < args.length; k++) {
|
|
@@ -1624,7 +1685,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1624
1685
|
}
|
|
1625
1686
|
if (t.startsWith("-") && t !== "-") {
|
|
1626
1687
|
const slot = readVerbOptionSlot(slotVerb, t);
|
|
1627
|
-
|
|
1688
|
+
const permutes = !verbOptionsArePositionIndependent(slotVerb);
|
|
1689
|
+
if (sawArgvOperand && permutes) {
|
|
1628
1690
|
judgeOperand(t, argGlobs(k));
|
|
1629
1691
|
if (slot !== undefined && slot.form === "separated") {
|
|
1630
1692
|
const ahead = args[k + 1];
|
|
@@ -1634,7 +1696,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1634
1696
|
}
|
|
1635
1697
|
}
|
|
1636
1698
|
}
|
|
1637
|
-
if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawArgvOperand)) {
|
|
1699
|
+
if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawArgvOperand && permutes)) {
|
|
1638
1700
|
if (slot.form === "attached") {
|
|
1639
1701
|
for (const c of slotValueCandidates(literalTildeSpelling(slot.value), slot.kind))
|
|
1640
1702
|
judgeOperand(c, argGlobs(k));
|
|
@@ -1663,23 +1725,27 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1663
1725
|
if (t === "-") {
|
|
1664
1726
|
if (isGrepPatternSlot)
|
|
1665
1727
|
continue;
|
|
1666
|
-
const m = RECURSIVE_READ_FORMS[
|
|
1728
|
+
const m = RECURSIVE_READ_FORMS[slotVerb];
|
|
1667
1729
|
if (m === undefined || m.dashIsStdin === true) {
|
|
1668
1730
|
stdinOperand = true;
|
|
1669
1731
|
continue;
|
|
1670
1732
|
}
|
|
1671
|
-
|
|
1733
|
+
if (k >= argvOperandsStartAt)
|
|
1734
|
+
positionalOperands.push(t);
|
|
1672
1735
|
continue;
|
|
1673
1736
|
}
|
|
1674
1737
|
if (isGrepPatternSlot)
|
|
1675
1738
|
continue;
|
|
1676
|
-
|
|
1739
|
+
if (k >= argvOperandsStartAt)
|
|
1740
|
+
positionalOperands.push(t);
|
|
1677
1741
|
judgeOperand(t, argGlobs(k));
|
|
1678
1742
|
const payload = assignmentWordPayload(t);
|
|
1679
|
-
if (payload
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1743
|
+
if (payload !== undefined && (k + 1 < argvFrom || !declaredNotPaths)) {
|
|
1744
|
+
if (payload.kind === "value")
|
|
1745
|
+
judgeOperand(payload.value, argGlobs(k));
|
|
1746
|
+
else
|
|
1747
|
+
findings.push({ kind: "unresolvable", operand: t, reason: `"${name}": ${payload.reason} ${NOT_AUTO_ALLOWED}` });
|
|
1748
|
+
}
|
|
1683
1749
|
}
|
|
1684
1750
|
}
|
|
1685
1751
|
for (let k = 0; k < args.length; k++) {
|
|
@@ -1713,7 +1779,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1713
1779
|
if (!globbed && withinAnyRoot(boundary.roots, spellings.folded))
|
|
1714
1780
|
findings.push({ kind: "inside", path: spellings.folded, ...(spellings.spelled !== undefined ? { spelled: spellings.spelled } : {}) });
|
|
1715
1781
|
}
|
|
1716
|
-
if (boundary.denyMatch !== undefined && segmentSelectsRecursiveRead(
|
|
1782
|
+
if (boundary.denyMatch !== undefined && (boundary.denyEntries?.length ?? 1) > 0 && segmentSelectsRecursiveRead(slotVerb, args.slice(argvOperandsStartAt))) {
|
|
1717
1783
|
const roots = positionalOperands.length > 0 ? positionalOperands : stdinOperand ? [] : ["."];
|
|
1718
1784
|
for (const operand of roots) {
|
|
1719
1785
|
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
|
|
@@ -1803,6 +1869,12 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1803
1869
|
nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
|
|
1804
1870
|
continue;
|
|
1805
1871
|
}
|
|
1872
|
+
const program = pos.program;
|
|
1873
|
+
if (program !== undefined) {
|
|
1874
|
+
const familyRefusal = argumentAsProgramRefusal(program.basename, program.basename, folded.slice(program.index + 1));
|
|
1875
|
+
if (familyRefusal !== undefined)
|
|
1876
|
+
nameRefusal ??= familyRefusal;
|
|
1877
|
+
}
|
|
1806
1878
|
const parsed = parseLeadingCommandName(toks.raw.slice(from).join(" "), { quotedOperatorsAreText: true, pathPrefixedNameIsText: true });
|
|
1807
1879
|
if ("reject" in parsed)
|
|
1808
1880
|
return { reason: parsed.reject, refusedWhole: true };
|
|
@@ -1812,12 +1884,6 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1812
1884
|
nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
|
|
1813
1885
|
if (pos.refusal !== undefined)
|
|
1814
1886
|
return { reason: programPositionRefusalSentence(pos.refusal), refusedWhole: true };
|
|
1815
|
-
const program = pos.program;
|
|
1816
|
-
if (program !== undefined) {
|
|
1817
|
-
const familyRefusal = argumentAsProgramRefusal(program.basename, program.basename, folded.slice(program.index + 1));
|
|
1818
|
-
if (familyRefusal !== undefined)
|
|
1819
|
-
return { reason: familyRefusal, refusedWhole: true };
|
|
1820
|
-
}
|
|
1821
1887
|
if (allow.has(name)) {
|
|
1822
1888
|
if (pathPrefixed || from > 0)
|
|
1823
1889
|
reheaded.set(si, { from, head: name });
|
|
@@ -1833,7 +1899,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1833
1899
|
if (program !== undefined && !listed && SHELL_REENTRY_PROGRAM_RE.test(program.basename)) {
|
|
1834
1900
|
return { reason: `"${written}" runs "${program.basename}", which runs its argument as shell text this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1835
1901
|
}
|
|
1836
|
-
if (argvReentryUnjudged(pos,
|
|
1902
|
+
if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED)) {
|
|
1837
1903
|
return { reason: `"${written}" takes the operands of the program it runs from its standard input, which this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1838
1904
|
}
|
|
1839
1905
|
if (!listed || program === undefined)
|
|
@@ -1895,7 +1961,13 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1895
1961
|
const unlisted = tokenizeSegment(segments[si]);
|
|
1896
1962
|
if (unlisted.folded.length === 0)
|
|
1897
1963
|
continue;
|
|
1898
|
-
|
|
1964
|
+
const unlistedArgv = argvProgramOf(unlisted);
|
|
1965
|
+
foldedSegments.push({
|
|
1966
|
+
folded: [LAUNCHER_SCAN_HEAD, ...unlisted.folded],
|
|
1967
|
+
raw: [LAUNCHER_SCAN_HEAD, ...unlisted.raw],
|
|
1968
|
+
slotVerb: programBasename(unlisted.folded[0]),
|
|
1969
|
+
...(unlistedArgv !== undefined ? { argvProgram: unlistedArgv.program, argvFrom: unlistedArgv.from + 1 } : {}),
|
|
1970
|
+
});
|
|
1899
1971
|
pushReach();
|
|
1900
1972
|
continue;
|
|
1901
1973
|
}
|
|
@@ -1989,8 +2061,14 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1989
2061
|
const walk = evaluateReadBoundary(foldedSegments, boundary, true, foldedReach);
|
|
1990
2062
|
if (shapeRefusal === undefined)
|
|
1991
2063
|
return walk;
|
|
1992
|
-
const { reason: _walkSentence, outOfRootGrantCandidate:
|
|
1993
|
-
|
|
2064
|
+
const { reason: _walkSentence, outOfRootGrantCandidate: walkGrantCandidate, outOfRootGrantCoversExactPath: walkGrantExact, ...structure } = walk;
|
|
2065
|
+
const nameOnly = shapeRefusal === nameRefusal;
|
|
2066
|
+
return {
|
|
2067
|
+
...structure,
|
|
2068
|
+
reason: shapeRefusal,
|
|
2069
|
+
...(nameOnly && walkGrantCandidate !== undefined ? { outOfRootGrantCandidate: walkGrantCandidate, ...(walkGrantExact === true ? { outOfRootGrantCoversExactPath: true } : {}) } : {}),
|
|
2070
|
+
...(nameOnly ? { segmentNameRefusal: true } : {}),
|
|
2071
|
+
};
|
|
1994
2072
|
}
|
|
1995
2073
|
if (shapeRefusal !== undefined)
|
|
1996
2074
|
return { reason: shapeRefusal };
|
|
@@ -2164,14 +2242,14 @@ function evaluateReadBoundary(foldedSegments, boundary, mintGrantCandidate, reac
|
|
|
2164
2242
|
const proposed = outOfRootGrantCandidateOf(outside.map((o) => o.path), boundary);
|
|
2165
2243
|
if (proposed === undefined)
|
|
2166
2244
|
return undefined;
|
|
2167
|
-
const replay = evaluateReadBoundary(foldedSegments, { ...boundary, roots: [...boundary.roots, proposed] }, false, reach);
|
|
2245
|
+
const replay = evaluateReadBoundary(foldedSegments, { ...boundary, roots: [...boundary.roots, proposed.path] }, false, reach);
|
|
2168
2246
|
return replay.reason === undefined && (replay.undecidedPaths ?? []).length === 0 ? proposed : undefined;
|
|
2169
2247
|
})();
|
|
2170
2248
|
return {
|
|
2171
2249
|
reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} ${NOT_AUTO_ALLOWED}`,
|
|
2172
2250
|
outOfRootRead: true,
|
|
2173
2251
|
outOfRootPaths: outside.map((o) => o.path),
|
|
2174
|
-
...(grantCandidate !== undefined ? { outOfRootGrantCandidate: grantCandidate } : {}),
|
|
2252
|
+
...(grantCandidate !== undefined ? { outOfRootGrantCandidate: grantCandidate.path, ...(grantCandidate.exact === true ? { outOfRootGrantCoversExactPath: true } : {}) } : {}),
|
|
2175
2253
|
...outsideField,
|
|
2176
2254
|
...checkedField,
|
|
2177
2255
|
...undecidedField,
|
|
@@ -2187,7 +2265,7 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
2187
2265
|
if (pos.program !== undefined) {
|
|
2188
2266
|
const familyRefusal = argumentAsProgramRefusal(pos.program.basename, pos.program.basename, toks.folded.slice(pos.program.index + 1));
|
|
2189
2267
|
if (familyRefusal !== undefined)
|
|
2190
|
-
return { reason: familyRefusal
|
|
2268
|
+
return { reason: familyRefusal };
|
|
2191
2269
|
}
|
|
2192
2270
|
return evaluateReadBoundary([toks], boundary, true);
|
|
2193
2271
|
}
|
|
@@ -2240,7 +2318,8 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
|
2240
2318
|
if (UNMODELLED_BASE_MOVERS.has(name))
|
|
2241
2319
|
baseMoved = true;
|
|
2242
2320
|
if (!allow.has(name) && name !== "cd") {
|
|
2243
|
-
|
|
2321
|
+
const argv = argvProgramOf(toks);
|
|
2322
|
+
pushReader({ folded: [LAUNCHER_SCAN_HEAD, ...toks.folded], raw: [LAUNCHER_SCAN_HEAD, ...toks.raw], slotVerb: name, ...(argv !== undefined ? { argvProgram: argv.program, argvFrom: argv.from + 1 } : {}) });
|
|
2244
2323
|
continue;
|
|
2245
2324
|
}
|
|
2246
2325
|
pushReader(i === 0 && name === written ? toks : { folded: [name, ...toks.folded.slice(i + 1)], raw: [toks.raw[i] ?? name, ...toks.raw.slice(i + 1)] });
|