@sema-agent/core 7.17.7 → 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 +140 -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 +121 -552
- 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.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,36 +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
|
-
const controlWord = programBasename(toks.folded[from] ?? "");
|
|
258
|
-
from++;
|
|
259
|
-
for (let span = launcherOptionSpan(controlWord, toks.folded, from); span > 0; span = launcherOptionSpan(controlWord, toks.folded, from))
|
|
260
|
-
from += span;
|
|
261
|
-
}
|
|
262
|
-
const head = toks.folded[from];
|
|
263
|
-
if (head === undefined)
|
|
177
|
+
function tokensMoveBase(toks) {
|
|
178
|
+
const pos = readProgramPosition(toks);
|
|
179
|
+
if (pos.written === undefined)
|
|
264
180
|
return false;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (head.startsWith("-") && head !== "-")
|
|
268
|
-
return true;
|
|
269
|
-
if (head === "cd")
|
|
181
|
+
const written = toks.folded[pos.written];
|
|
182
|
+
if (toks.raw[pos.written] !== written)
|
|
270
183
|
return true;
|
|
271
|
-
if (
|
|
184
|
+
if (pos.assignments.length > 0)
|
|
272
185
|
return false;
|
|
273
|
-
|
|
274
|
-
|
|
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))
|
|
192
|
+
return false;
|
|
193
|
+
return pos.refusal !== undefined || pos.program?.basename === "cd";
|
|
275
194
|
}
|
|
276
195
|
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
277
|
-
const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
|
|
278
196
|
export function refusedCommandMayRead(command, allow, boundary) {
|
|
279
197
|
return !refusedCommandExempt(command, allow, boundary, 0);
|
|
280
198
|
}
|
|
@@ -282,249 +200,49 @@ const STDIN_ONLY_READER_FLAGS = new Map([
|
|
|
282
200
|
["cat", /^-[AbeEnstTuv]+$/],
|
|
283
201
|
["wc", /^-[clmwL]+$/],
|
|
284
202
|
]);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
else
|
|
302
|
-
body.push(line);
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
out.push(line);
|
|
306
|
-
const header = heredocHeaderOf(line);
|
|
307
|
-
if (header === null)
|
|
308
|
-
return undefined;
|
|
309
|
-
if (header !== undefined) {
|
|
310
|
-
terminator = header.word;
|
|
311
|
-
expanded = !header.quoted;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return terminator === undefined ? { outer: out.join("\n"), expandedBodies } : undefined;
|
|
315
|
-
}
|
|
316
|
-
function heredocHeaderOf(line) {
|
|
317
|
-
let inSingle = false;
|
|
318
|
-
let inDouble = false;
|
|
319
|
-
for (let i = 0; i < line.length; i++) {
|
|
320
|
-
const ch = line[i];
|
|
321
|
-
if (ch === "\\" && !inSingle) {
|
|
322
|
-
i++;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (ch === "'" && !inDouble) {
|
|
326
|
-
inSingle = !inSingle;
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
if (ch === '"' && !inSingle) {
|
|
330
|
-
inDouble = !inDouble;
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (inSingle || inDouble)
|
|
334
|
-
continue;
|
|
335
|
-
if (ch === "#")
|
|
336
|
-
return undefined;
|
|
337
|
-
if (ch === "<" && line[i + 1] === "<") {
|
|
338
|
-
if (line[i + 2] === "<") {
|
|
339
|
-
i += 2;
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
const m = /^<<-?\s*([^\s;|&<>()]+)/.exec(line.slice(i));
|
|
343
|
-
if (m === null)
|
|
344
|
-
return null;
|
|
345
|
-
const word = m[1];
|
|
346
|
-
if (/[$`]/.test(word))
|
|
347
|
-
return null;
|
|
348
|
-
const delimiter = word.replace(/\\(.)/g, "$1").replace(/["']/g, "");
|
|
349
|
-
if (delimiter === "")
|
|
350
|
-
return null;
|
|
351
|
-
return { word: delimiter, quoted: /["'\\]/.test(word) };
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return undefined;
|
|
355
|
-
}
|
|
356
|
-
function splitSubstitutions(text, quotesAreLiteral = false) {
|
|
357
|
-
let outer = "";
|
|
358
|
-
const bodies = [];
|
|
359
|
-
let i = 0;
|
|
360
|
-
let inSingle = false;
|
|
361
|
-
let inDouble = false;
|
|
362
|
-
while (i < text.length) {
|
|
363
|
-
const ch = text[i];
|
|
364
|
-
if (ch === "\\" && !inSingle) {
|
|
365
|
-
outer += text.slice(i, i + 2);
|
|
366
|
-
i += 2;
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
if (ch === "'" && !inDouble && !quotesAreLiteral) {
|
|
370
|
-
inSingle = !inSingle;
|
|
371
|
-
outer += ch;
|
|
372
|
-
i++;
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (ch === '"' && !inSingle && !quotesAreLiteral) {
|
|
376
|
-
inDouble = !inDouble;
|
|
377
|
-
outer += ch;
|
|
378
|
-
i++;
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
if (inSingle) {
|
|
382
|
-
outer += ch;
|
|
383
|
-
i++;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (ch === "`") {
|
|
387
|
-
const close = text.indexOf("`", i + 1);
|
|
388
|
-
if (close < 0)
|
|
389
|
-
return undefined;
|
|
390
|
-
bodies.push(text.slice(i + 1, close));
|
|
391
|
-
outer += SUBSTITUTION_PLACEHOLDER;
|
|
392
|
-
i = close + 1;
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
if ((ch === "$" || ch === "<" || ch === ">") && text[i + 1] === "(") {
|
|
396
|
-
let depth = 0;
|
|
397
|
-
let j = i + 1;
|
|
398
|
-
let quoted;
|
|
399
|
-
for (; j < text.length; j++) {
|
|
400
|
-
const c = text[j];
|
|
401
|
-
if (quoted !== undefined) {
|
|
402
|
-
if (c === "\\" && quoted === '"')
|
|
403
|
-
j++;
|
|
404
|
-
else if (c === quoted)
|
|
405
|
-
quoted = undefined;
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (c === "'" || c === '"') {
|
|
409
|
-
quoted = c;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
if (c === "(")
|
|
413
|
-
depth++;
|
|
414
|
-
else if (c === ")") {
|
|
415
|
-
depth--;
|
|
416
|
-
if (depth === 0)
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (j >= text.length)
|
|
421
|
-
return undefined;
|
|
422
|
-
bodies.push(text.slice(i + 2, j));
|
|
423
|
-
outer += SUBSTITUTION_PLACEHOLDER;
|
|
424
|
-
i = j + 1;
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
outer += ch;
|
|
428
|
-
i++;
|
|
429
|
-
}
|
|
430
|
-
if (inSingle)
|
|
431
|
-
return undefined;
|
|
432
|
-
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
|
+
};
|
|
433
218
|
}
|
|
434
219
|
function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside = false) {
|
|
435
220
|
if (depth > 4)
|
|
436
221
|
return false;
|
|
437
|
-
const
|
|
438
|
-
if (
|
|
439
|
-
return false;
|
|
440
|
-
if (!whitelistReadsTheSameWords(command, stripped.outer))
|
|
441
|
-
return false;
|
|
442
|
-
const split = splitSubstitutions(stripped.outer);
|
|
443
|
-
if (split === undefined)
|
|
222
|
+
const lexed = lexShellText(command, { mode: "lenient" });
|
|
223
|
+
if ("reject" in lexed)
|
|
444
224
|
return false;
|
|
445
|
-
const bodies = [...
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
bodies.push(...inner.bodies);
|
|
451
|
-
}
|
|
452
|
-
const fold = (w) => w.replace(/\\(.)/g, "$1").replace(/["']/g, "");
|
|
453
|
-
const programOf = (w) => {
|
|
454
|
-
const inner = fold(w);
|
|
455
|
-
return inner.includes("/") ? inner.slice(inner.lastIndexOf("/") + 1) : inner;
|
|
456
|
-
};
|
|
457
|
-
const segments = split.outer.split(/\n|;|&&|\|\||\||(?<![>&\d])&(?![>&])/).map((segment) => {
|
|
458
|
-
const toks = [];
|
|
459
|
-
for (const word of segment.trim().split(/\s+/)) {
|
|
460
|
-
let rest = word;
|
|
461
|
-
while (rest !== "") {
|
|
462
|
-
const m = /^(\d*(?:<<<|<<|<|>>|>)(?:&\d*)?|&>>?)(.*)$/.exec(rest);
|
|
463
|
-
if (m !== null) {
|
|
464
|
-
toks.push(m[1].replace(/^\d+/, ""));
|
|
465
|
-
rest = m[2];
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
const at = rest.search(/\d*(?:<<<|<<|<|>>|>)|&>/);
|
|
469
|
-
if (at <= 0) {
|
|
470
|
-
toks.push(rest);
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
toks.push(rest.slice(0, at));
|
|
474
|
-
rest = rest.slice(at);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
const commentAt = toks.findIndex((t) => t.startsWith("#"));
|
|
478
|
-
return (commentAt >= 0 ? toks.slice(0, commentAt) : toks)
|
|
479
|
-
.map((t) => (/^[A-Za-z_][A-Za-z0-9_]*\(\)$/.test(t) ? "{" : t.replace(/^[({]+|[)}]+$/g, "")))
|
|
480
|
-
.filter((t) => t !== "");
|
|
481
|
-
});
|
|
482
|
-
const headIndex = (toks) => {
|
|
483
|
-
const folded = toks.map(fold);
|
|
484
|
-
let i = 0;
|
|
485
|
-
while (i < toks.length) {
|
|
486
|
-
const w = folded[i];
|
|
487
|
-
if (ASSIGNMENT_WORD.test(w) || SHELL_CONTROL_PREFIXES.has(w) || SHELL_BLOCK_WORDS.has(w) || /^[{}]$/.test(w)) {
|
|
488
|
-
i++;
|
|
489
|
-
const controlWord = programBasename(w);
|
|
490
|
-
for (let span = launcherOptionSpan(controlWord, folded, i); span > 0; span = launcherOptionSpan(controlWord, folded, i))
|
|
491
|
-
i += span;
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(w)) {
|
|
495
|
-
i += 2;
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
if (/^(?:>>?|<)&\d*$/.test(w)) {
|
|
499
|
-
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)
|
|
500
230
|
continue;
|
|
501
|
-
|
|
502
|
-
|
|
231
|
+
const inner = substitutionsInHeredocBody(t.body);
|
|
232
|
+
if (inner === undefined)
|
|
233
|
+
return false;
|
|
234
|
+
for (const body of inner)
|
|
235
|
+
bodies.push(body);
|
|
503
236
|
}
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
const runProgramOf = (toks, i) => {
|
|
507
|
-
let head = programOf(toks[i] ?? "");
|
|
508
|
-
let guard = 0;
|
|
509
|
-
while (COMMAND_LAUNCHERS.has(head) && guard++ < 8) {
|
|
510
|
-
let j = i + 1;
|
|
511
|
-
while (j < toks.length && (ASSIGNMENT_WORD.test(fold(toks[j])) || /^\d+[smhd]?$/.test(fold(toks[j]))))
|
|
512
|
-
j++;
|
|
513
|
-
const next = toks[j];
|
|
514
|
-
if (next === undefined || fold(next).startsWith("-"))
|
|
515
|
-
return undefined;
|
|
516
|
-
i = j;
|
|
517
|
-
head = programOf(next);
|
|
518
|
-
}
|
|
519
|
-
return head;
|
|
520
|
-
};
|
|
237
|
+
segments.push(whitelistSegment(segment));
|
|
238
|
+
}
|
|
521
239
|
const baseMoved = baseMovedOutside ||
|
|
522
|
-
segments.some((
|
|
523
|
-
const
|
|
524
|
-
if (
|
|
525
|
-
return
|
|
526
|
-
const
|
|
527
|
-
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));
|
|
528
246
|
});
|
|
529
247
|
for (const body of bodies) {
|
|
530
248
|
const walked = classifyCompoundReadonlyDetailed(body, allow, boundary);
|
|
@@ -535,33 +253,22 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
535
253
|
if (!refusedCommandExempt(body, allow, boundary, depth + 1, baseMoved))
|
|
536
254
|
return false;
|
|
537
255
|
}
|
|
538
|
-
const stdinFileJudgedInside = (
|
|
539
|
-
if (
|
|
256
|
+
const stdinFileJudgedInside = (word) => {
|
|
257
|
+
if (word === undefined || word === "" || word.includes(SUBSTITUTION_PLACEHOLDER) || UNREADABLE_WHITELIST_WORD.test(word))
|
|
540
258
|
return false;
|
|
541
259
|
if (boundary === undefined || baseMoved)
|
|
542
260
|
return false;
|
|
543
|
-
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0],
|
|
261
|
+
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], word, boundary.homeDir);
|
|
544
262
|
if (resolved === undefined || boundary.denyMatch?.(resolved) != null)
|
|
545
263
|
return false;
|
|
546
264
|
return withinAnyRoot(boundary.roots, resolved);
|
|
547
265
|
};
|
|
548
|
-
const unlistedSegmentJudgedInside = (
|
|
266
|
+
const unlistedSegmentJudgedInside = (words, from) => {
|
|
549
267
|
if (boundary === undefined)
|
|
550
268
|
return true;
|
|
551
269
|
let endOfOptions = false;
|
|
552
|
-
for (let j = from; j <
|
|
553
|
-
const
|
|
554
|
-
if (t === "<<<" || t === "<<" || t === "<") {
|
|
555
|
-
j++;
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
if (/^(?:>>?|&>>?)$/.test(t)) {
|
|
559
|
-
j++;
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
if (/^(?:>>?|<)&\d*$/.test(t))
|
|
563
|
-
continue;
|
|
564
|
-
const word = fold(t);
|
|
270
|
+
for (let j = from; j < words.folded.length; j++) {
|
|
271
|
+
const word = words.folded[j];
|
|
565
272
|
if (j > from && !endOfOptions && word === "--") {
|
|
566
273
|
endOfOptions = true;
|
|
567
274
|
continue;
|
|
@@ -580,47 +287,49 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
580
287
|
}
|
|
581
288
|
return true;
|
|
582
289
|
};
|
|
583
|
-
for (const
|
|
584
|
-
for (
|
|
585
|
-
if (
|
|
290
|
+
for (const { words, redirects } of segments) {
|
|
291
|
+
for (const r of redirects)
|
|
292
|
+
if (r.op === "<" && !stdinFileJudgedInside(r.target?.folded))
|
|
586
293
|
return false;
|
|
587
|
-
|
|
588
|
-
if (
|
|
589
|
-
continue;
|
|
590
|
-
const headWord = fold(toks[i]);
|
|
591
|
-
if (headWord.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(headWord))
|
|
294
|
+
const pos = readProgramPosition(words);
|
|
295
|
+
if (pos.refusal !== undefined)
|
|
592
296
|
return false;
|
|
593
|
-
|
|
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)))
|
|
300
|
+
return false;
|
|
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))
|
|
308
|
+
return false;
|
|
309
|
+
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index))
|
|
310
|
+
return false;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (program.name.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(program.name))
|
|
594
314
|
return false;
|
|
595
|
-
|
|
596
|
-
const launcherUnwrap = COMMAND_LAUNCHERS.has(head) ? unwrapLauncher(toks.slice(i).map(fold), allow) : undefined;
|
|
597
|
-
if (launcherUnwrap?.kind === "refused")
|
|
315
|
+
if (argumentAsProgramRefusal(program.basename, program.basename, words.folded.slice(program.index + 1)) !== undefined)
|
|
598
316
|
return false;
|
|
599
|
-
|
|
600
|
-
if (runs.head !== undefined && runs.index !== undefined &&
|
|
601
|
-
argumentAsProgramRefusal(runs.head, runs.head, toks.slice(i + runs.index + 1).map(fold)) !== undefined)
|
|
317
|
+
if (SHELL_REENTRY_PROGRAM_RE.test(program.basename))
|
|
602
318
|
return false;
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
if (wrapped === undefined || /[$`]/.test(wrapped) || SHELL_REENTRY_PROGRAM_RE.test(wrapped))
|
|
319
|
+
if (pos.launchers.length > 0) {
|
|
320
|
+
if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS))
|
|
606
321
|
return false;
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
return false;
|
|
610
|
-
if (unwrapped.kind === "unlisted") {
|
|
611
|
-
if (!unlistedSegmentJudgedInside(toks, i))
|
|
322
|
+
if (!listed(program.basename)) {
|
|
323
|
+
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index))
|
|
612
324
|
return false;
|
|
613
325
|
continue;
|
|
614
326
|
}
|
|
615
|
-
head = unwrapped.head;
|
|
616
|
-
i += unwrapped.index;
|
|
617
327
|
}
|
|
618
|
-
|
|
619
|
-
return false;
|
|
328
|
+
const head = program.basename;
|
|
620
329
|
if (head === "cd" || UNMODELLED_BASE_MOVERS.has(head))
|
|
621
330
|
continue;
|
|
622
331
|
if (!allow.has(head)) {
|
|
623
|
-
if (!unlistedSegmentJudgedInside(
|
|
332
|
+
if (!unlistedSegmentJudgedInside(words, program.index))
|
|
624
333
|
return false;
|
|
625
334
|
continue;
|
|
626
335
|
}
|
|
@@ -629,110 +338,18 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
629
338
|
const flags = STDIN_ONLY_READER_FLAGS.get(head);
|
|
630
339
|
if (flags === undefined)
|
|
631
340
|
return false;
|
|
632
|
-
for (let j =
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
j++;
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
if (/^(?:>>?|&>>?)$/.test(t)) {
|
|
639
|
-
j++;
|
|
640
|
-
continue;
|
|
641
|
-
}
|
|
642
|
-
if (/^(?:>>?|<)&\d*$/.test(t))
|
|
643
|
-
continue;
|
|
644
|
-
if (flags.test(fold(t)))
|
|
645
|
-
continue;
|
|
646
|
-
return false;
|
|
341
|
+
for (let j = program.index + 1; j < words.folded.length; j++) {
|
|
342
|
+
if (!flags.test(words.folded[j]))
|
|
343
|
+
return false;
|
|
647
344
|
}
|
|
648
345
|
}
|
|
649
346
|
return true;
|
|
650
347
|
}
|
|
651
|
-
function unwrapLauncher(folded, allow) {
|
|
652
|
-
let argvFromStdin = ARGV_REENTRY_PROGRAM_RE.test(programBasename(folded[0] ?? ""));
|
|
653
|
-
let lastReentry = argvFromStdin ? 0 : -1;
|
|
654
|
-
for (let i = 1; i < folded.length; i++) {
|
|
655
|
-
const t = folded[i];
|
|
656
|
-
if (ASSIGNMENT_WORD.test(t) || /^\d+[smhd]?$/.test(t))
|
|
657
|
-
continue;
|
|
658
|
-
const wordBasename = programBasename(t);
|
|
659
|
-
if (ARGV_REENTRY_PROGRAM_RE.test(wordBasename)) {
|
|
660
|
-
argvFromStdin = true;
|
|
661
|
-
lastReentry = i;
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
if (COMMAND_LAUNCHERS.has(wordBasename))
|
|
665
|
-
continue;
|
|
666
|
-
if (t.startsWith("-"))
|
|
667
|
-
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}` };
|
|
668
|
-
const head = wordBasename;
|
|
669
|
-
if (SHELL_CONTROL_PREFIXES.has(head) || SHELL_BLOCK_WORDS.has(head))
|
|
670
|
-
return { kind: "refused", reason: `"${folded[0]}" is given shell control syntax ("${head}") where the program it runs would be ${NOT_AUTO_ALLOWED}` };
|
|
671
|
-
return allow.has(head) || head === "cd" ? { kind: "reader", index: i, head, argvFromStdin } : { kind: "unlisted", head, index: i, argvFromStdin };
|
|
672
|
-
}
|
|
673
|
-
return { kind: "unlisted", argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1 };
|
|
674
|
-
}
|
|
675
|
-
const LAUNCHER_OPTION_GRAMMAR = new Map([
|
|
676
|
-
["time", {
|
|
677
|
-
plain: new Set(["-p", "-a", "-v", "-q", "--portability", "--append", "--verbose", "--quiet", "--help", "--version"]),
|
|
678
|
-
valued: new Set(["-f", "-o", "--format", "--output"]),
|
|
679
|
-
}],
|
|
680
|
-
]);
|
|
681
|
-
function launcherOptionSpan(launcher, folded, i) {
|
|
682
|
-
const row = launcher === undefined ? undefined : LAUNCHER_OPTION_GRAMMAR.get(launcher);
|
|
683
|
-
const tok = folded[i];
|
|
684
|
-
if (row === undefined || tok === undefined)
|
|
685
|
-
return 0;
|
|
686
|
-
if (row.plain.has(tok))
|
|
687
|
-
return 1;
|
|
688
|
-
return row.valued.has(tok) && folded[i + 1] !== undefined ? 2 : 0;
|
|
689
|
-
}
|
|
690
|
-
const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
|
|
691
|
-
function argvReentryUnjudged(unwrapped) {
|
|
692
|
-
if (!unwrapped.argvFromStdin)
|
|
693
|
-
return false;
|
|
694
|
-
const runs = unwrapped.head ?? (unwrapped.runsDefaultProgram === true ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
|
|
695
|
-
return runs === undefined || !NO_PATH_OPERAND_COMMANDS.has(runs);
|
|
696
|
-
}
|
|
697
348
|
function tokenizeSegment(segment) {
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
return word.startsWith("~") ? `./${word}` : word;
|
|
703
|
-
}
|
|
704
|
-
function tildeIsLiteral(raw) {
|
|
705
|
-
if (!raw.startsWith("~"))
|
|
706
|
-
return true;
|
|
707
|
-
for (let i = 1; i < raw.length; i++) {
|
|
708
|
-
const ch = raw[i];
|
|
709
|
-
if (ch === '"' || ch === "'")
|
|
710
|
-
return true;
|
|
711
|
-
if (ch === "/")
|
|
712
|
-
return false;
|
|
713
|
-
}
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
|
-
function splitWordsQuoteAware(segment) {
|
|
717
|
-
const s = segment.trim();
|
|
718
|
-
const mask = quoteMask(s);
|
|
719
|
-
if (!mask.balanced)
|
|
720
|
-
return s.split(/\s+/).filter((t) => t.length > 0);
|
|
721
|
-
const out = [];
|
|
722
|
-
let cur = "";
|
|
723
|
-
for (let i = 0; i < s.length; i++) {
|
|
724
|
-
const ch = s[i];
|
|
725
|
-
if (!mask.quoted[i] && /\s/.test(ch)) {
|
|
726
|
-
if (cur.length > 0)
|
|
727
|
-
out.push(cur);
|
|
728
|
-
cur = "";
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
cur += ch;
|
|
732
|
-
}
|
|
733
|
-
if (cur.length > 0)
|
|
734
|
-
out.push(cur);
|
|
735
|
-
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]);
|
|
736
353
|
}
|
|
737
354
|
function hasUnquotedExpansionMetachar(rawToken) {
|
|
738
355
|
let open;
|
|
@@ -1304,20 +921,6 @@ const INTERPRETER_INLINE_PROGRAMS = new Map([
|
|
|
1304
921
|
function interpreterKey(name) {
|
|
1305
922
|
return name.replace(/\d+(?:\.\d+)*$/, "");
|
|
1306
923
|
}
|
|
1307
|
-
function withoutRedirectionWords(args) {
|
|
1308
|
-
const out = [];
|
|
1309
|
-
for (let i = 0; i < args.length; i++) {
|
|
1310
|
-
const t = args[i];
|
|
1311
|
-
if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(t)) {
|
|
1312
|
-
i++;
|
|
1313
|
-
continue;
|
|
1314
|
-
}
|
|
1315
|
-
if (/^(?:>>?|<)&\d*$/.test(t))
|
|
1316
|
-
continue;
|
|
1317
|
-
out.push(t);
|
|
1318
|
-
}
|
|
1319
|
-
return out;
|
|
1320
|
-
}
|
|
1321
924
|
function inlineProgramClause(model, args) {
|
|
1322
925
|
let endOfOptions = false;
|
|
1323
926
|
let banner = false;
|
|
@@ -1441,7 +1044,7 @@ function argumentAsProgramRefusal(written, name, args) {
|
|
|
1441
1044
|
const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(name));
|
|
1442
1045
|
if (interpreter === undefined)
|
|
1443
1046
|
return undefined;
|
|
1444
|
-
const clause = inlineProgramClause(interpreter,
|
|
1047
|
+
const clause = inlineProgramClause(interpreter, args);
|
|
1445
1048
|
return clause === undefined ? undefined : unjudgedProgramTextRefusal(written, clause);
|
|
1446
1049
|
}
|
|
1447
1050
|
function guardedVerbFormReason(name, toks, raw) {
|
|
@@ -1834,13 +1437,8 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1834
1437
|
for (let si = 0; si < segments.length; si++) {
|
|
1835
1438
|
const toks = tokenizeSegment(segments[si]);
|
|
1836
1439
|
const folded = toks.folded;
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
const controlWord = programBasename(folded[from] ?? "");
|
|
1840
|
-
from++;
|
|
1841
|
-
for (let span = launcherOptionSpan(controlWord, folded, from); span > 0; span = launcherOptionSpan(controlWord, folded, from))
|
|
1842
|
-
from += span;
|
|
1843
|
-
}
|
|
1440
|
+
const pos = readProgramPosition(toks);
|
|
1441
|
+
const from = pos.written ?? folded.length;
|
|
1844
1442
|
const written = folded[from];
|
|
1845
1443
|
if (written === undefined)
|
|
1846
1444
|
return { reason: "empty command" };
|
|
@@ -1858,15 +1456,11 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1858
1456
|
const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
|
|
1859
1457
|
if (pathPrefixed)
|
|
1860
1458
|
nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
|
|
1861
|
-
if (
|
|
1862
|
-
return { reason:
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
return { reason: launcherUnwrap.reason, refusedWhole: true };
|
|
1867
|
-
const runs = launcherUnwrap === undefined ? { head: name, index: 0 } : { head: launcherUnwrap.head, index: launcherUnwrap.index };
|
|
1868
|
-
if (runs?.head !== undefined && runs.index !== undefined) {
|
|
1869
|
-
const familyRefusal = argumentAsProgramRefusal(runs.head, runs.head, folded.slice(from + runs.index + 1));
|
|
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));
|
|
1870
1464
|
if (familyRefusal !== undefined)
|
|
1871
1465
|
return { reason: familyRefusal, refusedWhole: true };
|
|
1872
1466
|
}
|
|
@@ -1880,20 +1474,18 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1880
1474
|
nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
|
|
1881
1475
|
if (SHELL_REENTRY_PROGRAM_RE.test(name))
|
|
1882
1476
|
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text"), refusedWhole: true };
|
|
1883
|
-
if (
|
|
1884
|
-
const
|
|
1885
|
-
if (
|
|
1886
|
-
return { reason:
|
|
1887
|
-
if (unwrapped.kind === "unlisted" && unwrapped.head !== undefined && SHELL_REENTRY_PROGRAM_RE.test(unwrapped.head)) {
|
|
1888
|
-
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 };
|
|
1889
1481
|
}
|
|
1890
|
-
if (argvReentryUnjudged(
|
|
1482
|
+
if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS)) {
|
|
1891
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 };
|
|
1892
1484
|
}
|
|
1893
|
-
if (
|
|
1485
|
+
if (!listed || program === undefined)
|
|
1894
1486
|
unlistedSegments.add(si);
|
|
1895
1487
|
else
|
|
1896
|
-
reheaded.set(si, { from:
|
|
1488
|
+
reheaded.set(si, { from: program.index, head: program.basename });
|
|
1897
1489
|
continue;
|
|
1898
1490
|
}
|
|
1899
1491
|
unlistedSegments.add(si);
|
|
@@ -2225,44 +1817,21 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
2225
1817
|
const toks = tokenizeSegment(command);
|
|
2226
1818
|
if (toks.folded.length === 0)
|
|
2227
1819
|
return {};
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
const runs = COMMAND_LAUNCHERS.has(head) ? unwrapLauncher(chain, new Set()) : { kind: "reader", head, index: 0 };
|
|
2234
|
-
if (runs.kind === "refused")
|
|
2235
|
-
return { reason: runs.reason, refusedWhole: true };
|
|
2236
|
-
if (runs.head !== undefined && runs.index !== undefined) {
|
|
2237
|
-
const familyRefusal = argumentAsProgramRefusal(runs.head, runs.head, toks.folded.slice(runs.index + past + 1));
|
|
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));
|
|
2238
1825
|
if (familyRefusal !== undefined)
|
|
2239
1826
|
return { reason: familyRefusal, refusedWhole: true };
|
|
2240
1827
|
}
|
|
2241
1828
|
return evaluateReadBoundary([toks], boundary);
|
|
2242
1829
|
}
|
|
2243
1830
|
const NOT_GATED = { gated: false, paths: [] };
|
|
2244
|
-
const ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
2245
1831
|
const UNREADABLE_WHITELIST_WORD = /[$`()*?[\]{}~]/;
|
|
2246
|
-
function whitelistReadsTheSameWords(command, outsideHeredocBodies) {
|
|
2247
|
-
if (/\\\r?\n/.test(command))
|
|
2248
|
-
return false;
|
|
2249
|
-
if (/\\[<>&|;()]/.test(outsideHeredocBodies))
|
|
2250
|
-
return false;
|
|
2251
|
-
return !(outsideHeredocBodies.includes("#") && /["']/.test(outsideHeredocBodies));
|
|
2252
|
-
}
|
|
2253
1832
|
const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
2254
|
-
const COMMAND_LAUNCHERS = new Set([
|
|
2255
|
-
"env", "command", "builtin", "exec", "noglob", "xargs", "busybox",
|
|
2256
|
-
"sudo", "doas", "pkexec", "su", "runuser", "chroot", "setpriv",
|
|
2257
|
-
"nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
|
|
2258
|
-
"setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run",
|
|
2259
|
-
"strace", "ltrace", "valgrind", "firejail", "bwrap",
|
|
2260
|
-
]);
|
|
2261
1833
|
export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
|
|
2262
1834
|
const LAUNCHER_SCAN_HEAD = "cat";
|
|
2263
|
-
function programBasename(word) {
|
|
2264
|
-
return word.includes("/") ? (word.split("/").pop() ?? word) : word;
|
|
2265
|
-
}
|
|
2266
1835
|
export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
2267
1836
|
const containmentOnly = {
|
|
2268
1837
|
roots: boundary.roots,
|
|
@@ -2417,7 +1986,7 @@ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary
|
|
|
2417
1986
|
if (readSegments.length === 0) {
|
|
2418
1987
|
return { reason: "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed" };
|
|
2419
1988
|
}
|
|
2420
|
-
const bodyHasCd = readSegments.some((seg) => tokensMoveBase(tokenizeSegment(seg)
|
|
1989
|
+
const bodyHasCd = readSegments.some((seg) => tokensMoveBase(tokenizeSegment(seg)));
|
|
2421
1990
|
const modelled = bodyHasCd ? Array.from({ length: beats }, () => readSegments.join("; ")).join("; ") : readSegments.join("; ");
|
|
2422
1991
|
const verdict = classifyCompoundReadonlyDetailed(modelled, allow, boundary, { iterated: bodyHasCd });
|
|
2423
1992
|
if (verdict.reason !== undefined)
|