@sema-agent/core 7.17.6 → 7.18.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 +165 -13
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/agents/teacher.js +1 -0
- package/dist/agents/verify.js +3 -0
- package/dist/brain/route-adjudicator.d.ts +18 -0
- package/dist/brain/route-adjudicator.js +16 -0
- package/dist/core/agent-definition.d.ts +10 -0
- package/dist/core/artifact-host.d.ts +41 -4
- package/dist/core/artifact-host.js +18 -2
- package/dist/core/auto-mode.d.ts +141 -45
- package/dist/core/auto-mode.js +109 -3
- package/dist/core/checkpoint-store.d.ts +16 -2
- package/dist/core/engine-notice.d.ts +47 -1
- package/dist/core/engine-notice.js +7 -0
- package/dist/core/gate-lanes.js +2 -1
- package/dist/core/gate-outcome.d.ts +17 -2
- package/dist/core/gate-outcome.js +16 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +8 -0
- package/dist/core/hook-seats.d.ts +66 -0
- package/dist/core/hook-seats.js +37 -0
- package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
- package/dist/core/runner/assemble-result.d.ts +2 -1
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/classifier-ladder.d.ts +89 -0
- package/dist/core/runner/classifier-ladder.js +65 -0
- package/dist/core/runner/contracts.d.ts +69 -13
- package/dist/core/runner/decide-continuation.d.ts +1 -3
- package/dist/core/runner/decide-continuation.js +1 -7
- package/dist/core/runner/deferral-census.d.ts +52 -0
- package/dist/core/runner/deferral-census.js +44 -0
- package/dist/core/runner/derived-route-fallback.d.ts +8 -4
- package/dist/core/runner/derived-route-fallback.js +8 -3
- package/dist/core/runner/gate-exit.d.ts +12 -0
- package/dist/core/runner/gate-exit.js +6 -3
- package/dist/core/runner/prepare-artifact.d.ts +10 -2
- package/dist/core/runner/prepare-artifact.js +5 -4
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +30 -31
- package/dist/core/runner/prepare-config-doors.d.ts +1 -1
- package/dist/core/runner/prepare-config-doors.js +5 -3
- package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
- package/dist/core/runner/prepare-defer-classify.js +8 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
- package/dist/core/runner/prepare-hands-readface.js +60 -24
- package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
- package/dist/core/runner/prepare-inherited-gate.js +10 -2
- package/dist/core/runner/prepare-lsp.d.ts +2 -2
- package/dist/core/runner/prepare-lsp.js +1 -1
- package/dist/core/runner/prepare-policy-chain.js +2 -0
- package/dist/core/runner/prepare-project-context.js +4 -2
- package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
- package/dist/core/runner/prepare-prompt-inputs.js +4 -1
- package/dist/core/runner/prepare-run-refs.d.ts +9 -1
- package/dist/core/runner/prepare-run-refs.js +3 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
- package/dist/core/runner/prepare-suspend-saga.js +5 -2
- package/dist/core/runner/prepare-task.js +18 -13
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
- package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
- package/dist/core/runner/prepare-turn-wiring.js +3 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
- package/dist/core/runner/prepare-wiring-manifest.js +5 -2
- package/dist/core/runner/resume-checkpoint-screen.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +4 -1
- package/dist/core/runner/run-identity-wiring.js +6 -2
- package/dist/core/runner/run-leg.d.ts +2 -0
- package/dist/core/runner/run-leg.js +35 -41
- package/dist/core/runner/run-notification-lane.js +55 -27
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +16 -13
- package/dist/core/runner/run-turn-boundary.js +3 -1
- package/dist/core/runner/runtask.d.ts +6 -5
- package/dist/core/runner/runtask.js +26 -14
- package/dist/core/runner/skill-base-dir.d.ts +54 -0
- package/dist/core/runner/skill-base-dir.js +42 -0
- package/dist/core/runner/synthetic-tools.d.ts +16 -19
- package/dist/core/runner/synthetic-tools.js +18 -10
- package/dist/core/runner/tool-defer-gate.d.ts +22 -9
- package/dist/core/runner/tool-defer-gate.js +15 -5
- package/dist/core/runner/tool-disclosure.d.ts +5 -0
- package/dist/core/runner/tool-disclosure.js +2 -0
- package/dist/core/runner/tool-progress-lane.d.ts +42 -0
- package/dist/core/runner/tool-progress-lane.js +61 -0
- package/dist/core/runner-deps.d.ts +48 -7
- package/dist/core/swappable-deps.d.ts +9 -7
- package/dist/core/swappable-deps.js +7 -3
- package/dist/core/task-event.d.ts +96 -29
- package/dist/core/task-notification.d.ts +142 -16
- package/dist/core/task-notification.js +106 -0
- package/dist/core/task-result.d.ts +9 -1
- package/dist/core/task-spec.d.ts +20 -3
- package/dist/core/tighten-task-spec.js +7 -5
- package/dist/core/tool-model-gate.d.ts +1 -1
- package/dist/core/tool-search-setting.d.ts +78 -0
- package/dist/core/tool-search-setting.js +90 -0
- package/dist/core/tool-spec.d.ts +8 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +46 -0
- package/dist/core/wiring-manifest.js +9 -1
- package/dist/engine/execution-env/node-execution-env.js +10 -4
- package/dist/engine/harness/agent-harness.d.ts +42 -0
- package/dist/engine/harness/agent-harness.js +61 -29
- package/dist/engine/loop/agent-loop.d.ts +68 -3
- package/dist/engine/loop/agent-loop.js +109 -16
- package/dist/index.d.ts +10 -5
- package/dist/index.js +9 -5
- package/dist/internal/harness.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +3 -0
- package/dist/orchestration/run-workflow-tool.js +6 -1
- package/dist/orchestration/workflow-governance.d.ts +7 -1
- package/dist/orchestration/workflow-governance.js +9 -5
- package/dist/orchestration/workflow-primitives.d.ts +3 -0
- package/dist/orchestration/workflow-primitives.js +6 -1
- package/dist/orchestration/workflow-types.d.ts +48 -13
- package/dist/orchestration/workflow-types.js +1 -1
- package/dist/orchestration/workflow.js +45 -17
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/section-usage.d.ts +26 -0
- package/dist/prompt-assembly/section-usage.js +10 -0
- package/dist/prompt-assembly/types.d.ts +8 -6
- package/dist/prompts/default.d.ts +13 -0
- package/dist/prompts/default.js +16 -8
- package/dist/tools/artifact/artifact-text.d.ts +10 -1
- package/dist/tools/artifact/artifact-text.js +6 -3
- package/dist/tools/artifact/artifact-tool.d.ts +12 -2
- package/dist/tools/artifact/artifact-tool.js +203 -70
- package/dist/tools/artifact/conformance.d.ts +56 -0
- package/dist/tools/artifact/conformance.js +349 -0
- package/dist/tools/artifact/local-stub.js +1 -1
- package/dist/tools/artifact/reject-breaker.d.ts +28 -0
- package/dist/tools/artifact/reject-breaker.js +42 -0
- package/dist/tools/fs/bash-lexer.d.ts +132 -0
- package/dist/tools/fs/bash-lexer.js +563 -0
- package/dist/tools/fs/bash-program-position.d.ts +142 -0
- package/dist/tools/fs/bash-program-position.js +125 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +54 -16
- package/dist/tools/fs/bash-readonly-classifier.js +375 -530
- package/dist/tools/fs/containment-face.d.ts +89 -0
- package/dist/tools/fs/containment-face.js +81 -0
- package/dist/tools/fs/fs-bash.d.ts +46 -1
- package/dist/tools/fs/fs-bash.js +158 -22
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -3
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +24 -5
- package/dist/tools/fs/index.js +18 -6
- package/dist/tools/fs/repo-map.d.ts +2 -2
- package/dist/tools/fs/safety.d.ts +3 -3
- package/dist/tools/fs/safety.js +4 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +85 -9
- package/wire-changes/core@7.17.6.json +81 -0
- package/wire-changes/core@7.17.7.json +49 -0
- package/dist/tools/fs/read-face.d.ts +0 -68
- package/dist/tools/fs/read-face.js +0 -47
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
|
|
2
|
-
|
|
2
|
+
import { lexShellText, literalTildeSpelling, quoteMask, substitutionsInHeredocBody, SUBSTITUTION_PLACEHOLDER, wordsOf, } from "./bash-lexer.js";
|
|
3
|
+
import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
|
|
4
|
+
export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
|
|
3
5
|
export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
4
6
|
"ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
|
|
5
7
|
"grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
|
|
@@ -8,24 +10,6 @@ export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
|
8
10
|
];
|
|
9
11
|
export const BASH_CLASSIFY_DEFAULT_ALLOW = [...BASH_READONLY_DEFAULT_ALLOW, "find", "sed", "cd"];
|
|
10
12
|
const SHELL_OPERATORS = /[;&|<>$()`\n\r\\]/;
|
|
11
|
-
function quoteMask(s) {
|
|
12
|
-
const quoted = new Array(s.length).fill(false);
|
|
13
|
-
let open;
|
|
14
|
-
for (let i = 0; i < s.length; i++) {
|
|
15
|
-
const ch = s[i];
|
|
16
|
-
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
17
|
-
open = ch;
|
|
18
|
-
quoted[i] = true;
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (open !== undefined) {
|
|
22
|
-
quoted[i] = true;
|
|
23
|
-
if (open === ch)
|
|
24
|
-
open = undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return { quoted, balanced: open === undefined };
|
|
28
|
-
}
|
|
29
13
|
function hasBareShellOperator(s, quotedOperatorsAreText) {
|
|
30
14
|
if (!quotedOperatorsAreText)
|
|
31
15
|
return SHELL_OPERATORS.test(s);
|
|
@@ -108,48 +92,11 @@ export function splitShellCompoundSegments(source, options) {
|
|
|
108
92
|
return { reject: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
|
|
109
93
|
}
|
|
110
94
|
const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
for (let i = 0; i < scanned.length; i++) {
|
|
117
|
-
const c = scanned[i];
|
|
118
|
-
if (quoted(i)) {
|
|
119
|
-
cur += c;
|
|
120
|
-
}
|
|
121
|
-
else if (c === "&") {
|
|
122
|
-
if (scanned[i + 1] === "&") {
|
|
123
|
-
segments.push(cur);
|
|
124
|
-
cur = "";
|
|
125
|
-
connectors.push("&&");
|
|
126
|
-
i++;
|
|
127
|
-
}
|
|
128
|
-
else
|
|
129
|
-
return { reject: "`&` backgrounding is not allowed — a backgrounded process outlives the command" };
|
|
130
|
-
}
|
|
131
|
-
else if (c === "|") {
|
|
132
|
-
if (scanned[i + 1] === "|") {
|
|
133
|
-
segments.push(cur);
|
|
134
|
-
cur = "";
|
|
135
|
-
connectors.push("||");
|
|
136
|
-
i++;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
segments.push(cur);
|
|
140
|
-
cur = "";
|
|
141
|
-
connectors.push("|");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
else if (c === ";") {
|
|
145
|
-
segments.push(cur);
|
|
146
|
-
cur = "";
|
|
147
|
-
connectors.push(";");
|
|
148
|
-
}
|
|
149
|
-
else
|
|
150
|
-
cur += c;
|
|
151
|
-
}
|
|
152
|
-
segments.push(cur);
|
|
95
|
+
const lexed = lexShellText(scanned, { mode: "strict" });
|
|
96
|
+
if ("reject" in lexed)
|
|
97
|
+
return lexed;
|
|
98
|
+
const segments = lexed.segments.map((s) => s.text);
|
|
99
|
+
const connectors = lexed.segments.map((s) => s.connector);
|
|
153
100
|
return { segments, pipeFed: connectors.map((k) => k === "|"), connectors };
|
|
154
101
|
}
|
|
155
102
|
const EXEMPT_REDIRECTION = /^(?:2>\/dev\/null|[12]?>&[12])$/;
|
|
@@ -181,24 +128,6 @@ function stripExemptRedirections(command) {
|
|
|
181
128
|
flush();
|
|
182
129
|
return out;
|
|
183
130
|
}
|
|
184
|
-
function foldQuoteRemovalToken(tok) {
|
|
185
|
-
if (!tok.includes('"') && !tok.includes("'"))
|
|
186
|
-
return tok;
|
|
187
|
-
let out = "";
|
|
188
|
-
let open;
|
|
189
|
-
for (const ch of tok) {
|
|
190
|
-
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
191
|
-
open = ch;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (open === ch) {
|
|
195
|
-
open = undefined;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
out += ch;
|
|
199
|
-
}
|
|
200
|
-
return out;
|
|
201
|
-
}
|
|
202
131
|
const POLL_LOOP_CAUSE_MAX_PATHS = 3;
|
|
203
132
|
export function formatOutOfRootReadApprovalOption(directory) {
|
|
204
133
|
const trimmed = directory.replace(/[/\\]+$/, "");
|
|
@@ -245,30 +174,25 @@ function spellingsOf(absolute) {
|
|
|
245
174
|
function reheadSegment(toks, head, from = 0) {
|
|
246
175
|
return { folded: [head, ...toks.folded.slice(from + 1)], raw: [head, ...toks.raw.slice(from + 1)] };
|
|
247
176
|
}
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
const word = toks.folded[i];
|
|
252
|
-
return word !== undefined && toks.raw[i] === word && words.has(word);
|
|
253
|
-
}
|
|
254
|
-
function tokensMoveBase(toks, allow) {
|
|
255
|
-
let from = 0;
|
|
256
|
-
while (from < toks.folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES))
|
|
257
|
-
from++;
|
|
258
|
-
const head = toks.folded[from];
|
|
259
|
-
if (head === undefined)
|
|
177
|
+
function tokensMoveBase(toks) {
|
|
178
|
+
const pos = readProgramPosition(toks);
|
|
179
|
+
if (pos.written === undefined)
|
|
260
180
|
return false;
|
|
261
|
-
|
|
181
|
+
const written = toks.folded[pos.written];
|
|
182
|
+
if (toks.raw[pos.written] !== written)
|
|
262
183
|
return true;
|
|
263
|
-
if (
|
|
264
|
-
return
|
|
265
|
-
if (
|
|
184
|
+
if (pos.assignments.length > 0)
|
|
185
|
+
return false;
|
|
186
|
+
if (pos.launchers.length === 0) {
|
|
187
|
+
if (written.startsWith("-") && written !== "-")
|
|
188
|
+
return true;
|
|
189
|
+
return written === "cd";
|
|
190
|
+
}
|
|
191
|
+
if (!COMMAND_LAUNCHERS.has(written))
|
|
266
192
|
return false;
|
|
267
|
-
|
|
268
|
-
return unwrapped.kind === "refused" || (unwrapped.kind === "reader" && unwrapped.head === "cd");
|
|
193
|
+
return pos.refusal !== undefined || pos.program?.basename === "cd";
|
|
269
194
|
}
|
|
270
|
-
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi)?sh|eval|source|\.|parallel)$/;
|
|
271
|
-
const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
|
|
195
|
+
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
272
196
|
export function refusedCommandMayRead(command, allow, boundary) {
|
|
273
197
|
return !refusedCommandExempt(command, allow, boundary, 0);
|
|
274
198
|
}
|
|
@@ -276,245 +200,49 @@ const STDIN_ONLY_READER_FLAGS = new Map([
|
|
|
276
200
|
["cat", /^-[AbeEnstTuv]+$/],
|
|
277
201
|
["wc", /^-[clmwL]+$/],
|
|
278
202
|
]);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
else
|
|
296
|
-
body.push(line);
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
out.push(line);
|
|
300
|
-
const header = heredocHeaderOf(line);
|
|
301
|
-
if (header === null)
|
|
302
|
-
return undefined;
|
|
303
|
-
if (header !== undefined) {
|
|
304
|
-
terminator = header.word;
|
|
305
|
-
expanded = !header.quoted;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return terminator === undefined ? { outer: out.join("\n"), expandedBodies } : undefined;
|
|
309
|
-
}
|
|
310
|
-
function heredocHeaderOf(line) {
|
|
311
|
-
let inSingle = false;
|
|
312
|
-
let inDouble = false;
|
|
313
|
-
for (let i = 0; i < line.length; i++) {
|
|
314
|
-
const ch = line[i];
|
|
315
|
-
if (ch === "\\" && !inSingle) {
|
|
316
|
-
i++;
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
if (ch === "'" && !inDouble) {
|
|
320
|
-
inSingle = !inSingle;
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
if (ch === '"' && !inSingle) {
|
|
324
|
-
inDouble = !inDouble;
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (inSingle || inDouble)
|
|
328
|
-
continue;
|
|
329
|
-
if (ch === "#")
|
|
330
|
-
return undefined;
|
|
331
|
-
if (ch === "<" && line[i + 1] === "<") {
|
|
332
|
-
if (line[i + 2] === "<") {
|
|
333
|
-
i += 2;
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
const m = /^<<-?\s*([^\s;|&<>()]+)/.exec(line.slice(i));
|
|
337
|
-
if (m === null)
|
|
338
|
-
return null;
|
|
339
|
-
const word = m[1];
|
|
340
|
-
if (/[$`]/.test(word))
|
|
341
|
-
return null;
|
|
342
|
-
const delimiter = word.replace(/\\(.)/g, "$1").replace(/["']/g, "");
|
|
343
|
-
if (delimiter === "")
|
|
344
|
-
return null;
|
|
345
|
-
return { word: delimiter, quoted: /["'\\]/.test(word) };
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return undefined;
|
|
349
|
-
}
|
|
350
|
-
function splitSubstitutions(text, quotesAreLiteral = false) {
|
|
351
|
-
let outer = "";
|
|
352
|
-
const bodies = [];
|
|
353
|
-
let i = 0;
|
|
354
|
-
let inSingle = false;
|
|
355
|
-
let inDouble = false;
|
|
356
|
-
while (i < text.length) {
|
|
357
|
-
const ch = text[i];
|
|
358
|
-
if (ch === "\\" && !inSingle) {
|
|
359
|
-
outer += text.slice(i, i + 2);
|
|
360
|
-
i += 2;
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
if (ch === "'" && !inDouble && !quotesAreLiteral) {
|
|
364
|
-
inSingle = !inSingle;
|
|
365
|
-
outer += ch;
|
|
366
|
-
i++;
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
if (ch === '"' && !inSingle && !quotesAreLiteral) {
|
|
370
|
-
inDouble = !inDouble;
|
|
371
|
-
outer += ch;
|
|
372
|
-
i++;
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (inSingle) {
|
|
376
|
-
outer += ch;
|
|
377
|
-
i++;
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
if (ch === "`") {
|
|
381
|
-
const close = text.indexOf("`", i + 1);
|
|
382
|
-
if (close < 0)
|
|
383
|
-
return undefined;
|
|
384
|
-
bodies.push(text.slice(i + 1, close));
|
|
385
|
-
outer += SUBSTITUTION_PLACEHOLDER;
|
|
386
|
-
i = close + 1;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
if ((ch === "$" || ch === "<" || ch === ">") && text[i + 1] === "(") {
|
|
390
|
-
let depth = 0;
|
|
391
|
-
let j = i + 1;
|
|
392
|
-
let quoted;
|
|
393
|
-
for (; j < text.length; j++) {
|
|
394
|
-
const c = text[j];
|
|
395
|
-
if (quoted !== undefined) {
|
|
396
|
-
if (c === "\\" && quoted === '"')
|
|
397
|
-
j++;
|
|
398
|
-
else if (c === quoted)
|
|
399
|
-
quoted = undefined;
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
if (c === "'" || c === '"') {
|
|
403
|
-
quoted = c;
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
if (c === "(")
|
|
407
|
-
depth++;
|
|
408
|
-
else if (c === ")") {
|
|
409
|
-
depth--;
|
|
410
|
-
if (depth === 0)
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
if (j >= text.length)
|
|
415
|
-
return undefined;
|
|
416
|
-
bodies.push(text.slice(i + 2, j));
|
|
417
|
-
outer += SUBSTITUTION_PLACEHOLDER;
|
|
418
|
-
i = j + 1;
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
outer += ch;
|
|
422
|
-
i++;
|
|
423
|
-
}
|
|
424
|
-
if (inSingle)
|
|
425
|
-
return undefined;
|
|
426
|
-
return { outer, bodies };
|
|
203
|
+
function whitelistSegment(segment) {
|
|
204
|
+
const tokens = [...segment.tokens];
|
|
205
|
+
for (let k = 0; k + 2 < tokens.length; k++) {
|
|
206
|
+
const [a, b, c] = [tokens[k], tokens[k + 1], tokens[k + 2]];
|
|
207
|
+
if (a.kind === "word" && b.kind === "group" && b.op === "(" && c.kind === "group" && c.op === ")" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(a.folded))
|
|
208
|
+
tokens.splice(k, 3);
|
|
209
|
+
}
|
|
210
|
+
const words = tokens.filter((t) => t.kind === "word" && !(t.raw === t.folded && (t.folded === "{" || t.folded === "}")));
|
|
211
|
+
let from = 0;
|
|
212
|
+
while (from < words.length && SHELL_BLOCK_WORDS.has(words[from].folded))
|
|
213
|
+
from++;
|
|
214
|
+
return {
|
|
215
|
+
words: { folded: words.slice(from).map((w) => w.folded), raw: words.slice(from).map((w) => w.raw) },
|
|
216
|
+
redirects: tokens.filter((t) => t.kind === "redirect"),
|
|
217
|
+
};
|
|
427
218
|
}
|
|
428
219
|
function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside = false) {
|
|
429
220
|
if (depth > 4)
|
|
430
221
|
return false;
|
|
431
|
-
const
|
|
432
|
-
if (
|
|
433
|
-
return false;
|
|
434
|
-
if (!whitelistReadsTheSameWords(command, stripped.outer))
|
|
222
|
+
const lexed = lexShellText(command, { mode: "lenient" });
|
|
223
|
+
if ("reject" in lexed)
|
|
435
224
|
return false;
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const inner = splitSubstitutions(expandedBody, true);
|
|
442
|
-
if (inner === undefined)
|
|
443
|
-
return false;
|
|
444
|
-
bodies.push(...inner.bodies);
|
|
445
|
-
}
|
|
446
|
-
const fold = (w) => w.replace(/\\(.)/g, "$1").replace(/["']/g, "");
|
|
447
|
-
const programOf = (w) => {
|
|
448
|
-
const inner = fold(w);
|
|
449
|
-
return inner.includes("/") ? inner.slice(inner.lastIndexOf("/") + 1) : inner;
|
|
450
|
-
};
|
|
451
|
-
const segments = split.outer.split(/\n|;|&&|\|\||\||(?<![>&\d])&(?![>&])/).map((segment) => {
|
|
452
|
-
const toks = [];
|
|
453
|
-
for (const word of segment.trim().split(/\s+/)) {
|
|
454
|
-
let rest = word;
|
|
455
|
-
while (rest !== "") {
|
|
456
|
-
const m = /^(\d*(?:<<<|<<|<|>>|>)(?:&\d*)?|&>>?)(.*)$/.exec(rest);
|
|
457
|
-
if (m !== null) {
|
|
458
|
-
toks.push(m[1].replace(/^\d+/, ""));
|
|
459
|
-
rest = m[2];
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
const at = rest.search(/\d*(?:<<<|<<|<|>>|>)|&>/);
|
|
463
|
-
if (at <= 0) {
|
|
464
|
-
toks.push(rest);
|
|
465
|
-
break;
|
|
466
|
-
}
|
|
467
|
-
toks.push(rest.slice(0, at));
|
|
468
|
-
rest = rest.slice(at);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
const commentAt = toks.findIndex((t) => t.startsWith("#"));
|
|
472
|
-
return (commentAt >= 0 ? toks.slice(0, commentAt) : toks)
|
|
473
|
-
.map((t) => (/^[A-Za-z_][A-Za-z0-9_]*\(\)$/.test(t) ? "{" : t.replace(/^[({]+|[)}]+$/g, "")))
|
|
474
|
-
.filter((t) => t !== "");
|
|
475
|
-
});
|
|
476
|
-
const headIndex = (toks) => {
|
|
477
|
-
let i = 0;
|
|
478
|
-
while (i < toks.length) {
|
|
479
|
-
const w = fold(toks[i]);
|
|
480
|
-
if (ASSIGNMENT_WORD.test(w) || SHELL_CONTROL_PREFIXES.has(w) || SHELL_BLOCK_WORDS.has(w) || /^[{}]$/.test(w)) {
|
|
481
|
-
i++;
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(w)) {
|
|
485
|
-
i += 2;
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
if (/^(?:>>?|<)&\d*$/.test(w)) {
|
|
489
|
-
i++;
|
|
225
|
+
const bodies = [...lexed.substitutions];
|
|
226
|
+
const segments = [];
|
|
227
|
+
for (const segment of lexed.segments) {
|
|
228
|
+
for (const t of segment.tokens) {
|
|
229
|
+
if (t.kind !== "heredoc" || t.quoted)
|
|
490
230
|
continue;
|
|
491
|
-
|
|
492
|
-
|
|
231
|
+
const inner = substitutionsInHeredocBody(t.body);
|
|
232
|
+
if (inner === undefined)
|
|
233
|
+
return false;
|
|
234
|
+
for (const body of inner)
|
|
235
|
+
bodies.push(body);
|
|
493
236
|
}
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
const runProgramOf = (toks, i) => {
|
|
497
|
-
let head = programOf(toks[i] ?? "");
|
|
498
|
-
let guard = 0;
|
|
499
|
-
while (COMMAND_LAUNCHERS.has(head) && guard++ < 8) {
|
|
500
|
-
let j = i + 1;
|
|
501
|
-
while (j < toks.length && (ASSIGNMENT_WORD.test(fold(toks[j])) || /^\d+[smhd]?$/.test(fold(toks[j]))))
|
|
502
|
-
j++;
|
|
503
|
-
const next = toks[j];
|
|
504
|
-
if (next === undefined || fold(next).startsWith("-"))
|
|
505
|
-
return undefined;
|
|
506
|
-
i = j;
|
|
507
|
-
head = programOf(next);
|
|
508
|
-
}
|
|
509
|
-
return head;
|
|
510
|
-
};
|
|
237
|
+
segments.push(whitelistSegment(segment));
|
|
238
|
+
}
|
|
511
239
|
const baseMoved = baseMovedOutside ||
|
|
512
|
-
segments.some((
|
|
513
|
-
const
|
|
514
|
-
if (
|
|
515
|
-
return
|
|
516
|
-
const
|
|
517
|
-
return
|
|
240
|
+
segments.some(({ words }) => {
|
|
241
|
+
const pos = readProgramPosition(words);
|
|
242
|
+
if (pos.refusal !== undefined)
|
|
243
|
+
return true;
|
|
244
|
+
const runs = pos.program?.basename;
|
|
245
|
+
return runs === "cd" || (runs !== undefined && UNMODELLED_BASE_MOVERS.has(runs));
|
|
518
246
|
});
|
|
519
247
|
for (const body of bodies) {
|
|
520
248
|
const walked = classifyCompoundReadonlyDetailed(body, allow, boundary);
|
|
@@ -525,33 +253,22 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
525
253
|
if (!refusedCommandExempt(body, allow, boundary, depth + 1, baseMoved))
|
|
526
254
|
return false;
|
|
527
255
|
}
|
|
528
|
-
const stdinFileJudgedInside = (
|
|
529
|
-
if (
|
|
256
|
+
const stdinFileJudgedInside = (word) => {
|
|
257
|
+
if (word === undefined || word === "" || word.includes(SUBSTITUTION_PLACEHOLDER) || UNREADABLE_WHITELIST_WORD.test(word))
|
|
530
258
|
return false;
|
|
531
259
|
if (boundary === undefined || baseMoved)
|
|
532
260
|
return false;
|
|
533
|
-
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0],
|
|
261
|
+
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], word, boundary.homeDir);
|
|
534
262
|
if (resolved === undefined || boundary.denyMatch?.(resolved) != null)
|
|
535
263
|
return false;
|
|
536
264
|
return withinAnyRoot(boundary.roots, resolved);
|
|
537
265
|
};
|
|
538
|
-
const unlistedSegmentJudgedInside = (
|
|
266
|
+
const unlistedSegmentJudgedInside = (words, from) => {
|
|
539
267
|
if (boundary === undefined)
|
|
540
268
|
return true;
|
|
541
269
|
let endOfOptions = false;
|
|
542
|
-
for (let j = from; j <
|
|
543
|
-
const
|
|
544
|
-
if (t === "<<<" || t === "<<" || t === "<") {
|
|
545
|
-
j++;
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
if (/^(?:>>?|&>>?)$/.test(t)) {
|
|
549
|
-
j++;
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
if (/^(?:>>?|<)&\d*$/.test(t))
|
|
553
|
-
continue;
|
|
554
|
-
const word = fold(t);
|
|
270
|
+
for (let j = from; j < words.folded.length; j++) {
|
|
271
|
+
const word = words.folded[j];
|
|
555
272
|
if (j > from && !endOfOptions && word === "--") {
|
|
556
273
|
endOfOptions = true;
|
|
557
274
|
continue;
|
|
@@ -570,40 +287,49 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
570
287
|
}
|
|
571
288
|
return true;
|
|
572
289
|
};
|
|
573
|
-
for (const
|
|
574
|
-
for (
|
|
575
|
-
if (
|
|
290
|
+
for (const { words, redirects } of segments) {
|
|
291
|
+
for (const r of redirects)
|
|
292
|
+
if (r.op === "<" && !stdinFileJudgedInside(r.target?.folded))
|
|
576
293
|
return false;
|
|
577
|
-
|
|
578
|
-
if (
|
|
579
|
-
continue;
|
|
580
|
-
const headWord = fold(toks[i]);
|
|
581
|
-
if (headWord.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(headWord))
|
|
294
|
+
const pos = readProgramPosition(words);
|
|
295
|
+
if (pos.refusal !== undefined)
|
|
582
296
|
return false;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (wrapped === undefined || /[$`]/.test(wrapped) || SHELL_REENTRY_PROGRAM_RE.test(wrapped))
|
|
297
|
+
for (const k of words.folded.keys()) {
|
|
298
|
+
const word = words.folded[k];
|
|
299
|
+
if (ASSIGNMENT_WORD.test(word) && /\$\(|`/.test(word) && (pos.assignments.includes(k) || pos.launchers.some((l) => l.index < k)))
|
|
587
300
|
return false;
|
|
588
|
-
|
|
589
|
-
|
|
301
|
+
}
|
|
302
|
+
const program = pos.program;
|
|
303
|
+
const listed = (name) => allow.has(name) || name === "cd";
|
|
304
|
+
if (program === undefined) {
|
|
305
|
+
if (pos.launchers.length === 0)
|
|
306
|
+
continue;
|
|
307
|
+
if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS))
|
|
590
308
|
return false;
|
|
591
|
-
if (
|
|
309
|
+
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index))
|
|
592
310
|
return false;
|
|
593
|
-
|
|
594
|
-
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (program.name.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(program.name))
|
|
314
|
+
return false;
|
|
315
|
+
if (argumentAsProgramRefusal(program.basename, program.basename, words.folded.slice(program.index + 1)) !== undefined)
|
|
316
|
+
return false;
|
|
317
|
+
if (SHELL_REENTRY_PROGRAM_RE.test(program.basename))
|
|
318
|
+
return false;
|
|
319
|
+
if (pos.launchers.length > 0) {
|
|
320
|
+
if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS))
|
|
321
|
+
return false;
|
|
322
|
+
if (!listed(program.basename)) {
|
|
323
|
+
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index))
|
|
595
324
|
return false;
|
|
596
325
|
continue;
|
|
597
326
|
}
|
|
598
|
-
head = unwrapped.head;
|
|
599
|
-
i += unwrapped.index;
|
|
600
327
|
}
|
|
601
|
-
|
|
602
|
-
return false;
|
|
328
|
+
const head = program.basename;
|
|
603
329
|
if (head === "cd" || UNMODELLED_BASE_MOVERS.has(head))
|
|
604
330
|
continue;
|
|
605
331
|
if (!allow.has(head)) {
|
|
606
|
-
if (!unlistedSegmentJudgedInside(
|
|
332
|
+
if (!unlistedSegmentJudgedInside(words, program.index))
|
|
607
333
|
return false;
|
|
608
334
|
continue;
|
|
609
335
|
}
|
|
@@ -612,110 +338,18 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
612
338
|
const flags = STDIN_ONLY_READER_FLAGS.get(head);
|
|
613
339
|
if (flags === undefined)
|
|
614
340
|
return false;
|
|
615
|
-
for (let j =
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
j++;
|
|
619
|
-
continue;
|
|
620
|
-
}
|
|
621
|
-
if (/^(?:>>?|&>>?)$/.test(t)) {
|
|
622
|
-
j++;
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
if (/^(?:>>?|<)&\d*$/.test(t))
|
|
626
|
-
continue;
|
|
627
|
-
if (flags.test(fold(t)))
|
|
628
|
-
continue;
|
|
629
|
-
return false;
|
|
341
|
+
for (let j = program.index + 1; j < words.folded.length; j++) {
|
|
342
|
+
if (!flags.test(words.folded[j]))
|
|
343
|
+
return false;
|
|
630
344
|
}
|
|
631
345
|
}
|
|
632
346
|
return true;
|
|
633
347
|
}
|
|
634
|
-
function unwrapLauncher(folded, allow) {
|
|
635
|
-
let argvFromStdin = ARGV_REENTRY_PROGRAM_RE.test(programBasename(folded[0] ?? ""));
|
|
636
|
-
let lastReentry = argvFromStdin ? 0 : -1;
|
|
637
|
-
for (let i = 1; i < folded.length; i++) {
|
|
638
|
-
const t = folded[i];
|
|
639
|
-
if (ASSIGNMENT_WORD.test(t) || /^\d+[smhd]?$/.test(t))
|
|
640
|
-
continue;
|
|
641
|
-
const wordBasename = programBasename(t);
|
|
642
|
-
if (ARGV_REENTRY_PROGRAM_RE.test(wordBasename)) {
|
|
643
|
-
argvFromStdin = true;
|
|
644
|
-
lastReentry = i;
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
if (COMMAND_LAUNCHERS.has(wordBasename))
|
|
648
|
-
continue;
|
|
649
|
-
if (t.startsWith("-"))
|
|
650
|
-
return { kind: "refused", reason: `"${folded[0]}" is given an option before the program it runs — which program runs is not readable without the launcher's option table ${NOT_AUTO_ALLOWED}` };
|
|
651
|
-
const head = wordBasename;
|
|
652
|
-
if (SHELL_CONTROL_PREFIXES.has(head) || SHELL_BLOCK_WORDS.has(head))
|
|
653
|
-
return { kind: "refused", reason: `"${folded[0]}" is given shell control syntax ("${head}") where the program it runs would be ${NOT_AUTO_ALLOWED}` };
|
|
654
|
-
return allow.has(head) || head === "cd" ? { kind: "reader", index: i, head, argvFromStdin } : { kind: "unlisted", head, index: i, argvFromStdin };
|
|
655
|
-
}
|
|
656
|
-
return { kind: "unlisted", argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1 };
|
|
657
|
-
}
|
|
658
|
-
const LAUNCHER_OPTION_GRAMMAR = new Map([
|
|
659
|
-
["time", {
|
|
660
|
-
plain: new Set(["-p", "-a", "-v", "-q", "--portability", "--append", "--verbose", "--quiet", "--help", "--version"]),
|
|
661
|
-
valued: new Set(["-f", "-o", "--format", "--output"]),
|
|
662
|
-
}],
|
|
663
|
-
]);
|
|
664
|
-
function launcherOptionSpan(launcher, folded, i) {
|
|
665
|
-
const row = launcher === undefined ? undefined : LAUNCHER_OPTION_GRAMMAR.get(launcher);
|
|
666
|
-
const tok = folded[i];
|
|
667
|
-
if (row === undefined || tok === undefined)
|
|
668
|
-
return 0;
|
|
669
|
-
if (row.plain.has(tok))
|
|
670
|
-
return 1;
|
|
671
|
-
return row.valued.has(tok) && folded[i + 1] !== undefined ? 2 : 0;
|
|
672
|
-
}
|
|
673
|
-
const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
|
|
674
|
-
function argvReentryUnjudged(unwrapped) {
|
|
675
|
-
if (!unwrapped.argvFromStdin)
|
|
676
|
-
return false;
|
|
677
|
-
const runs = unwrapped.head ?? (unwrapped.runsDefaultProgram === true ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
|
|
678
|
-
return runs === undefined || !NO_PATH_OPERAND_COMMANDS.has(runs);
|
|
679
|
-
}
|
|
680
348
|
function tokenizeSegment(segment) {
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
return word.startsWith("~") ? `./${word}` : word;
|
|
686
|
-
}
|
|
687
|
-
function tildeIsLiteral(raw) {
|
|
688
|
-
if (!raw.startsWith("~"))
|
|
689
|
-
return true;
|
|
690
|
-
for (let i = 1; i < raw.length; i++) {
|
|
691
|
-
const ch = raw[i];
|
|
692
|
-
if (ch === '"' || ch === "'")
|
|
693
|
-
return true;
|
|
694
|
-
if (ch === "/")
|
|
695
|
-
return false;
|
|
696
|
-
}
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
function splitWordsQuoteAware(segment) {
|
|
700
|
-
const s = segment.trim();
|
|
701
|
-
const mask = quoteMask(s);
|
|
702
|
-
if (!mask.balanced)
|
|
703
|
-
return s.split(/\s+/).filter((t) => t.length > 0);
|
|
704
|
-
const out = [];
|
|
705
|
-
let cur = "";
|
|
706
|
-
for (let i = 0; i < s.length; i++) {
|
|
707
|
-
const ch = s[i];
|
|
708
|
-
if (!mask.quoted[i] && /\s/.test(ch)) {
|
|
709
|
-
if (cur.length > 0)
|
|
710
|
-
out.push(cur);
|
|
711
|
-
cur = "";
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
cur += ch;
|
|
715
|
-
}
|
|
716
|
-
if (cur.length > 0)
|
|
717
|
-
out.push(cur);
|
|
718
|
-
return out;
|
|
349
|
+
const lexed = lexShellText(segment, { mode: "strict", connectors: "text" });
|
|
350
|
+
if ("reject" in lexed)
|
|
351
|
+
return { folded: [], raw: [] };
|
|
352
|
+
return wordsOf(lexed.segments[0]);
|
|
719
353
|
}
|
|
720
354
|
function hasUnquotedExpansionMetachar(rawToken) {
|
|
721
355
|
let open;
|
|
@@ -1159,14 +793,259 @@ const SCRIPT_SLOT_VERBS = new Map([
|
|
|
1159
793
|
return readings.every((reading) => reading === "unparsed");
|
|
1160
794
|
}],
|
|
1161
795
|
]);
|
|
796
|
+
const INLINE_PROGRAM_TEXT = "runs its argument as an interpreted program";
|
|
797
|
+
const INLINE_PROGRAM_STDIN = "reads the program it runs from its standard input";
|
|
798
|
+
const INTERPRETER_BANNER_OPTIONS = new Set(["--version", "--help", "--usage", "-version", "-help"]);
|
|
799
|
+
const BLANK_INLINE = {
|
|
800
|
+
longText: new Set(), longFile: new Set(), longNamesProgram: new Set(), longSupplemental: new Set(),
|
|
801
|
+
longValue: new Set(), longPlain: new Set(), fileOptionEndsOptions: false,
|
|
802
|
+
textLetters: "", fileLetters: "", separatedValueLetters: "", attachedValueLetters: "",
|
|
803
|
+
plainLetters: "", bannerLetters: "", interactiveLetters: "",
|
|
804
|
+
programOperand: { at: 0, isText: false },
|
|
805
|
+
};
|
|
806
|
+
const PERL_INLINE = {
|
|
807
|
+
...BLANK_INLINE, textLetters: "eEmM", separatedValueLetters: "I", attachedValueLetters: "iCFxDV",
|
|
808
|
+
plainLetters: "nplawcsStTUWX", bannerLetters: "vVh", stdinWhenUnnamed: true,
|
|
809
|
+
};
|
|
810
|
+
const RUBY_INLINE = {
|
|
811
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "IrECFKT", plainLetters: "nplawcsSU",
|
|
812
|
+
bannerLetters: "vh", stdinWhenUnnamed: true,
|
|
813
|
+
};
|
|
814
|
+
const PYTHON_INLINE = {
|
|
815
|
+
...BLANK_INLINE, textLetters: "c", fileLetters: "m", separatedValueLetters: "WXQ",
|
|
816
|
+
plainLetters: "uBEIsSqbdvxOtR", bannerLetters: "Vh?", interactiveLetters: "i",
|
|
817
|
+
fileOptionEndsOptions: true, stdinWhenUnnamed: true,
|
|
818
|
+
};
|
|
819
|
+
const NODE_INLINE = {
|
|
820
|
+
...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
|
|
821
|
+
longValue: new Set(["--import", "--loader", "--experimental-loader", "--require", "--env-file", "--conditions", "--max-old-space-size"]),
|
|
822
|
+
longNamesProgram: new Set(["--test"]),
|
|
823
|
+
longPlain: new Set(["--inspect", "--inspect-brk", "--no-warnings", "--trace-warnings", "--watch", "--enable-source-maps", "--experimental-modules", "--no-deprecation", "--check"]),
|
|
824
|
+
separatedValueLetters: "r", plainLetters: "c", bannerLetters: "vh", interactiveLetters: "i",
|
|
825
|
+
stdinWhenUnnamed: true,
|
|
826
|
+
};
|
|
827
|
+
const PHP_INLINE = {
|
|
828
|
+
...BLANK_INLINE, longText: new Set(["--run", "--process-begin", "--process-code", "--process-end"]),
|
|
829
|
+
textLetters: "rRBE", fileLetters: "fF", separatedValueLetters: "dcz", plainLetters: "nqswl",
|
|
830
|
+
bannerLetters: "vhi", interactiveLetters: "a", stdinWhenUnnamed: true,
|
|
831
|
+
};
|
|
832
|
+
const LUA_INLINE = {
|
|
833
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "l", bannerLetters: "vh",
|
|
834
|
+
interactiveLetters: "i", stdinWhenUnnamed: true,
|
|
835
|
+
};
|
|
836
|
+
const TCL_INLINE = { ...BLANK_INLINE, stdinWhenUnnamed: true };
|
|
837
|
+
const EXPECT_INLINE = {
|
|
838
|
+
...BLANK_INLINE, textLetters: "c", separatedValueLetters: "Db", plainLetters: "dN",
|
|
839
|
+
bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: true,
|
|
840
|
+
};
|
|
841
|
+
const AWK_INLINE = {
|
|
842
|
+
...BLANK_INLINE, longText: new Set(["--source"]), textLetters: "e",
|
|
843
|
+
longFile: new Set(["--file", "--exec"]), fileLetters: "fE", separatedValueLetters: "vF",
|
|
844
|
+
bannerLetters: "V", programOperand: { at: 0, isText: true }, stdinWhenUnnamed: false,
|
|
845
|
+
};
|
|
846
|
+
const JQ_INLINE = {
|
|
847
|
+
...BLANK_INLINE, longFile: new Set(["--from-file"]), fileLetters: "f", separatedValueLetters: "L",
|
|
848
|
+
plainLetters: "nrjcsSaCMRe", bannerLetters: "h", programOperand: { at: 0, isText: true },
|
|
849
|
+
stdinWhenUnnamed: false,
|
|
850
|
+
};
|
|
851
|
+
const BC_INLINE = {
|
|
852
|
+
...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e", plainLetters: "lqsw",
|
|
853
|
+
bannerLetters: "vh", interactiveLetters: "i", programOperand: undefined, stdinWhenUnnamed: true,
|
|
854
|
+
};
|
|
855
|
+
const DC_INLINE = {
|
|
856
|
+
...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e",
|
|
857
|
+
longFile: new Set(["--file"]), fileLetters: "f", bannerLetters: "hV",
|
|
858
|
+
programOperand: undefined, stdinWhenUnnamed: true,
|
|
859
|
+
};
|
|
860
|
+
const SQLITE_INLINE = {
|
|
861
|
+
...BLANK_INLINE, longText: new Set(["-cmd"]), longSupplemental: new Set(["-init"]),
|
|
862
|
+
programOperand: { at: 1, isText: true }, stdinWhenUnnamed: true,
|
|
863
|
+
};
|
|
864
|
+
const PSQL_INLINE = {
|
|
865
|
+
...BLANK_INLINE, longText: new Set(["--command"]), textLetters: "c",
|
|
866
|
+
longFile: new Set(["--file"]), fileLetters: "f", separatedValueLetters: "hpUdoF",
|
|
867
|
+
plainLetters: "aeEqtxXwW", bannerLetters: "lV?", programOperand: undefined, stdinWhenUnnamed: true,
|
|
868
|
+
};
|
|
869
|
+
const OSASCRIPT_INLINE = {
|
|
870
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "ls", plainLetters: "i", bannerLetters: "h",
|
|
871
|
+
stdinWhenUnnamed: true,
|
|
872
|
+
};
|
|
873
|
+
const RSCRIPT_INLINE = {
|
|
874
|
+
...BLANK_INLINE, textLetters: "e", bannerLetters: "h", stdinWhenUnnamed: false,
|
|
875
|
+
};
|
|
876
|
+
const JULIA_INLINE = {
|
|
877
|
+
...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "eE",
|
|
878
|
+
separatedValueLetters: "JLp", plainLetters: "q", bannerLetters: "vh", interactiveLetters: "i",
|
|
879
|
+
stdinWhenUnnamed: true,
|
|
880
|
+
};
|
|
881
|
+
const GHCI_INLINE = {
|
|
882
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "io", programOperand: undefined,
|
|
883
|
+
stdinWhenUnnamed: true,
|
|
884
|
+
};
|
|
885
|
+
const GROOVY_INLINE = {
|
|
886
|
+
...BLANK_INLINE, textLetters: "e", separatedValueLetters: "Dcl", plainLetters: "n",
|
|
887
|
+
bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: false,
|
|
888
|
+
};
|
|
889
|
+
const BUN_INLINE = {
|
|
890
|
+
...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
|
|
891
|
+
separatedValueLetters: "r", bannerLetters: "vh", stdinWhenUnnamed: false,
|
|
892
|
+
};
|
|
893
|
+
const INTERPRETER_INLINE_PROGRAMS = new Map([
|
|
894
|
+
["perl", PERL_INLINE],
|
|
895
|
+
["ruby", RUBY_INLINE],
|
|
896
|
+
["python", PYTHON_INLINE],
|
|
897
|
+
["node", NODE_INLINE],
|
|
898
|
+
["nodejs", NODE_INLINE],
|
|
899
|
+
["php", PHP_INLINE],
|
|
900
|
+
["lua", LUA_INLINE],
|
|
901
|
+
["luajit", LUA_INLINE],
|
|
902
|
+
["tclsh", TCL_INLINE],
|
|
903
|
+
["wish", TCL_INLINE],
|
|
904
|
+
["expect", EXPECT_INLINE],
|
|
905
|
+
["awk", AWK_INLINE],
|
|
906
|
+
["gawk", AWK_INLINE],
|
|
907
|
+
["mawk", AWK_INLINE],
|
|
908
|
+
["nawk", AWK_INLINE],
|
|
909
|
+
["jq", JQ_INLINE],
|
|
910
|
+
["bc", BC_INLINE],
|
|
911
|
+
["dc", DC_INLINE],
|
|
912
|
+
["sqlite", SQLITE_INLINE],
|
|
913
|
+
["psql", PSQL_INLINE],
|
|
914
|
+
["osascript", OSASCRIPT_INLINE],
|
|
915
|
+
["Rscript", RSCRIPT_INLINE],
|
|
916
|
+
["julia", JULIA_INLINE],
|
|
917
|
+
["ghci", GHCI_INLINE],
|
|
918
|
+
["groovy", GROOVY_INLINE],
|
|
919
|
+
["bun", BUN_INLINE],
|
|
920
|
+
]);
|
|
921
|
+
function interpreterKey(name) {
|
|
922
|
+
return name.replace(/\d+(?:\.\d+)*$/, "");
|
|
923
|
+
}
|
|
924
|
+
function inlineProgramClause(model, args) {
|
|
925
|
+
let endOfOptions = false;
|
|
926
|
+
let banner = false;
|
|
927
|
+
let interactive = false;
|
|
928
|
+
let named = false;
|
|
929
|
+
let stdin = false;
|
|
930
|
+
let explained = true;
|
|
931
|
+
let operands = 0;
|
|
932
|
+
scan: for (let i = 0; i < args.length; i++) {
|
|
933
|
+
const arg = args[i];
|
|
934
|
+
if (!endOfOptions && arg === "--") {
|
|
935
|
+
endOfOptions = true;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (arg === "-") {
|
|
939
|
+
if (model.programOperand?.isText !== true)
|
|
940
|
+
stdin = true;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (!endOfOptions && arg.startsWith("-")) {
|
|
944
|
+
const eq = arg.indexOf("=");
|
|
945
|
+
const whole = eq < 0 ? arg : arg.slice(0, eq);
|
|
946
|
+
const valueOf = () => (eq < 0 ? args[++i] : arg.slice(eq + 1));
|
|
947
|
+
if (model.longText.has(whole))
|
|
948
|
+
return INLINE_PROGRAM_TEXT;
|
|
949
|
+
if (model.longFile.has(whole)) {
|
|
950
|
+
if (valueOf() === "-") {
|
|
951
|
+
stdin = true;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
named = true;
|
|
955
|
+
if (explained && model.fileOptionEndsOptions)
|
|
956
|
+
break scan;
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (model.longNamesProgram.has(whole)) {
|
|
960
|
+
named = true;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (model.longSupplemental.has(whole)) {
|
|
964
|
+
valueOf();
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (INTERPRETER_BANNER_OPTIONS.has(whole)) {
|
|
968
|
+
banner = true;
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (model.longValue.has(whole)) {
|
|
972
|
+
valueOf();
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
if (model.longPlain.has(whole))
|
|
976
|
+
continue;
|
|
977
|
+
if (arg.startsWith("--")) {
|
|
978
|
+
if (eq < 0)
|
|
979
|
+
explained = false;
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
for (let k = 1; k < arg.length; k++) {
|
|
983
|
+
const ch = arg[k];
|
|
984
|
+
if (ch >= "0" && ch <= "9")
|
|
985
|
+
continue;
|
|
986
|
+
if (model.textLetters.includes(ch))
|
|
987
|
+
return INLINE_PROGRAM_TEXT;
|
|
988
|
+
if (model.fileLetters.includes(ch)) {
|
|
989
|
+
const value = k + 1 < arg.length ? arg.slice(k + 1) : args[++i];
|
|
990
|
+
if (value === "-") {
|
|
991
|
+
stdin = true;
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
named = true;
|
|
995
|
+
if (explained && model.fileOptionEndsOptions)
|
|
996
|
+
break scan;
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
if (model.attachedValueLetters.includes(ch)) {
|
|
1000
|
+
if (model.bannerLetters.includes(ch))
|
|
1001
|
+
banner = true;
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
if (model.bannerLetters.includes(ch)) {
|
|
1005
|
+
banner = true;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (model.interactiveLetters.includes(ch)) {
|
|
1009
|
+
interactive = true;
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (model.separatedValueLetters.includes(ch)) {
|
|
1013
|
+
if (k + 1 >= arg.length)
|
|
1014
|
+
i++;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
if (!model.plainLetters.includes(ch))
|
|
1018
|
+
explained = false;
|
|
1019
|
+
}
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
operands++;
|
|
1023
|
+
if (!named && explained && model.programOperand !== undefined && operands === model.programOperand.at + 1) {
|
|
1024
|
+
if (model.programOperand.isText)
|
|
1025
|
+
return INLINE_PROGRAM_TEXT;
|
|
1026
|
+
named = true;
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (stdin || interactive)
|
|
1031
|
+
return INLINE_PROGRAM_STDIN;
|
|
1032
|
+
if (named || banner)
|
|
1033
|
+
return undefined;
|
|
1034
|
+
return model.stdinWhenUnnamed ? INLINE_PROGRAM_STDIN : undefined;
|
|
1035
|
+
}
|
|
1162
1036
|
function unjudgedProgramTextRefusal(written, clause) {
|
|
1163
1037
|
return `command "${written}" ${clause}, which this walk does not judge ${NOT_AUTO_ALLOWED}`;
|
|
1164
1038
|
}
|
|
1165
|
-
function
|
|
1166
|
-
const
|
|
1167
|
-
if (
|
|
1039
|
+
function argumentAsProgramRefusal(written, name, args) {
|
|
1040
|
+
const slot = SCRIPT_SLOT_VERBS.get(name);
|
|
1041
|
+
if (slot !== undefined && slot(args)) {
|
|
1042
|
+
return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
|
|
1043
|
+
}
|
|
1044
|
+
const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(name));
|
|
1045
|
+
if (interpreter === undefined)
|
|
1168
1046
|
return undefined;
|
|
1169
|
-
|
|
1047
|
+
const clause = inlineProgramClause(interpreter, args);
|
|
1048
|
+
return clause === undefined ? undefined : unjudgedProgramTextRefusal(written, clause);
|
|
1170
1049
|
}
|
|
1171
1050
|
function guardedVerbFormReason(name, toks, raw) {
|
|
1172
1051
|
if (name === "find") {
|
|
@@ -1558,13 +1437,8 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1558
1437
|
for (let si = 0; si < segments.length; si++) {
|
|
1559
1438
|
const toks = tokenizeSegment(segments[si]);
|
|
1560
1439
|
const folded = toks.folded;
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
const controlWord = programBasename(folded[from] ?? "");
|
|
1564
|
-
from++;
|
|
1565
|
-
for (let span = launcherOptionSpan(controlWord, folded, from); span > 0; span = launcherOptionSpan(controlWord, folded, from))
|
|
1566
|
-
from += span;
|
|
1567
|
-
}
|
|
1440
|
+
const pos = readProgramPosition(toks);
|
|
1441
|
+
const from = pos.written ?? folded.length;
|
|
1568
1442
|
const written = folded[from];
|
|
1569
1443
|
if (written === undefined)
|
|
1570
1444
|
return { reason: "empty command" };
|
|
@@ -1582,17 +1456,13 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1582
1456
|
const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
|
|
1583
1457
|
if (pathPrefixed)
|
|
1584
1458
|
nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
|
|
1585
|
-
if (
|
|
1586
|
-
return { reason:
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
if (runs?.head !== undefined && runs.index !== undefined) {
|
|
1593
|
-
const slotRefusal = scriptSlotRefusal(runs.head, runs.head, folded.slice(from + runs.index + 1));
|
|
1594
|
-
if (slotRefusal !== undefined)
|
|
1595
|
-
return { reason: slotRefusal, refusedWhole: true };
|
|
1459
|
+
if (pos.refusal !== undefined)
|
|
1460
|
+
return { reason: programPositionRefusalSentence(pos.refusal), refusedWhole: true };
|
|
1461
|
+
const program = pos.program;
|
|
1462
|
+
if (program !== undefined) {
|
|
1463
|
+
const familyRefusal = argumentAsProgramRefusal(program.basename, program.basename, folded.slice(program.index + 1));
|
|
1464
|
+
if (familyRefusal !== undefined)
|
|
1465
|
+
return { reason: familyRefusal, refusedWhole: true };
|
|
1596
1466
|
}
|
|
1597
1467
|
if (allow.has(name)) {
|
|
1598
1468
|
if (pathPrefixed || from > 0)
|
|
@@ -1604,20 +1474,18 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1604
1474
|
nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
|
|
1605
1475
|
if (SHELL_REENTRY_PROGRAM_RE.test(name))
|
|
1606
1476
|
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text"), refusedWhole: true };
|
|
1607
|
-
if (
|
|
1608
|
-
const
|
|
1609
|
-
if (
|
|
1610
|
-
return { reason:
|
|
1611
|
-
if (unwrapped.kind === "unlisted" && unwrapped.head !== undefined && SHELL_REENTRY_PROGRAM_RE.test(unwrapped.head)) {
|
|
1612
|
-
return { reason: `"${written}" runs "${unwrapped.head}", which runs its argument as shell text this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1477
|
+
if (pos.launchers.length > 0) {
|
|
1478
|
+
const listed = program !== undefined && (allow.has(program.basename) || program.basename === "cd");
|
|
1479
|
+
if (program !== undefined && !listed && SHELL_REENTRY_PROGRAM_RE.test(program.basename)) {
|
|
1480
|
+
return { reason: `"${written}" runs "${program.basename}", which runs its argument as shell text this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1613
1481
|
}
|
|
1614
|
-
if (argvReentryUnjudged(
|
|
1482
|
+
if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS)) {
|
|
1615
1483
|
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 };
|
|
1616
1484
|
}
|
|
1617
|
-
if (
|
|
1485
|
+
if (!listed || program === undefined)
|
|
1618
1486
|
unlistedSegments.add(si);
|
|
1619
1487
|
else
|
|
1620
|
-
reheaded.set(si, { from:
|
|
1488
|
+
reheaded.set(si, { from: program.index, head: program.basename });
|
|
1621
1489
|
continue;
|
|
1622
1490
|
}
|
|
1623
1491
|
unlistedSegments.add(si);
|
|
@@ -1949,44 +1817,21 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
1949
1817
|
const toks = tokenizeSegment(command);
|
|
1950
1818
|
if (toks.folded.length === 0)
|
|
1951
1819
|
return {};
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
return { reason: runs.reason, refusedWhole: true };
|
|
1960
|
-
if (runs.head !== undefined && runs.index !== undefined) {
|
|
1961
|
-
const slotRefusal = scriptSlotRefusal(runs.head, runs.head, toks.folded.slice(runs.index + past + 1));
|
|
1962
|
-
if (slotRefusal !== undefined)
|
|
1963
|
-
return { reason: slotRefusal, refusedWhole: true };
|
|
1820
|
+
const pos = readProgramPosition(toks);
|
|
1821
|
+
if (pos.refusal !== undefined)
|
|
1822
|
+
return { reason: programPositionRefusalSentence(pos.refusal), refusedWhole: true };
|
|
1823
|
+
if (pos.program !== undefined) {
|
|
1824
|
+
const familyRefusal = argumentAsProgramRefusal(pos.program.basename, pos.program.basename, toks.folded.slice(pos.program.index + 1));
|
|
1825
|
+
if (familyRefusal !== undefined)
|
|
1826
|
+
return { reason: familyRefusal, refusedWhole: true };
|
|
1964
1827
|
}
|
|
1965
1828
|
return evaluateReadBoundary([toks], boundary);
|
|
1966
1829
|
}
|
|
1967
1830
|
const NOT_GATED = { gated: false, paths: [] };
|
|
1968
|
-
const ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
1969
1831
|
const UNREADABLE_WHITELIST_WORD = /[$`()*?[\]{}~]/;
|
|
1970
|
-
function whitelistReadsTheSameWords(command, outsideHeredocBodies) {
|
|
1971
|
-
if (/\\\r?\n/.test(command))
|
|
1972
|
-
return false;
|
|
1973
|
-
if (/\\[<>&|;()]/.test(outsideHeredocBodies))
|
|
1974
|
-
return false;
|
|
1975
|
-
return !(outsideHeredocBodies.includes("#") && /["']/.test(outsideHeredocBodies));
|
|
1976
|
-
}
|
|
1977
1832
|
const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
1978
|
-
const COMMAND_LAUNCHERS = new Set([
|
|
1979
|
-
"env", "command", "builtin", "exec", "noglob", "xargs",
|
|
1980
|
-
"sudo", "doas", "pkexec", "su", "runuser", "chroot", "setpriv",
|
|
1981
|
-
"nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
|
|
1982
|
-
"setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run",
|
|
1983
|
-
"strace", "ltrace", "valgrind", "firejail", "bwrap",
|
|
1984
|
-
]);
|
|
1985
1833
|
export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
|
|
1986
1834
|
const LAUNCHER_SCAN_HEAD = "cat";
|
|
1987
|
-
function programBasename(word) {
|
|
1988
|
-
return word.includes("/") ? (word.split("/").pop() ?? word) : word;
|
|
1989
|
-
}
|
|
1990
1835
|
export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
1991
1836
|
const containmentOnly = {
|
|
1992
1837
|
roots: boundary.roots,
|
|
@@ -2141,7 +1986,7 @@ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary
|
|
|
2141
1986
|
if (readSegments.length === 0) {
|
|
2142
1987
|
return { reason: "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed" };
|
|
2143
1988
|
}
|
|
2144
|
-
const bodyHasCd = readSegments.some((seg) => tokensMoveBase(tokenizeSegment(seg)
|
|
1989
|
+
const bodyHasCd = readSegments.some((seg) => tokensMoveBase(tokenizeSegment(seg)));
|
|
2145
1990
|
const modelled = bodyHasCd ? Array.from({ length: beats }, () => readSegments.join("; ")).join("; ") : readSegments.join("; ");
|
|
2146
1991
|
const verdict = classifyCompoundReadonlyDetailed(modelled, allow, boundary, { iterated: bodyHasCd });
|
|
2147
1992
|
if (verdict.reason !== undefined)
|