@sema-agent/core 7.28.1 → 7.30.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 +473 -1
- package/dist/agents/child-model-seat.d.ts +59 -7
- package/dist/agents/child-model-seat.js +33 -7
- package/dist/agents/cross-session-judge.d.ts +27 -5
- package/dist/agents/cross-session-judge.js +23 -12
- package/dist/agents/list-agents-tool.d.ts +31 -6
- package/dist/agents/list-agents-tool.js +66 -14
- package/dist/agents/peer-admission.d.ts +20 -8
- package/dist/agents/peer-admission.js +21 -12
- package/dist/agents/peer-directory.d.ts +29 -0
- package/dist/agents/peer-directory.js +20 -0
- package/dist/agents/peer-held-queue.d.ts +31 -4
- package/dist/agents/peer-held-queue.js +32 -1
- package/dist/agents/peer-session-drain.d.ts +91 -13
- package/dist/agents/peer-session-drain.js +409 -102
- package/dist/agents/read-inbox-tool.d.ts +22 -0
- package/dist/agents/read-inbox-tool.js +39 -0
- package/dist/agents/roster-store.d.ts +2 -4
- package/dist/agents/roster-store.js +1 -1
- package/dist/agents/send-message-tool.d.ts +4 -4
- package/dist/agents/send-message-tool.js +22 -2
- package/dist/agents/subagent.js +62 -44
- package/dist/agents/team-role.d.ts +34 -0
- package/dist/agents/team-role.js +17 -0
- package/dist/agents/verify.js +2 -2
- package/dist/brain/anthropic.js +4 -2
- package/dist/brain/open-responses.js +4 -3
- package/dist/brain/openai.js +2 -1
- package/dist/brain/stream-engine.js +6 -0
- package/dist/brain/stream-shared.d.ts +8 -0
- package/dist/brain/stream-shared.js +8 -0
- package/dist/core/auto-compaction.d.ts +42 -0
- package/dist/core/auto-compaction.js +35 -2
- package/dist/core/child-clamp-seats.d.ts +11 -0
- package/dist/core/child-clamp-seats.js +3 -0
- package/dist/core/context-edit.d.ts +3 -2
- package/dist/core/context-edit.js +6 -5
- package/dist/core/context-guard.d.ts +2 -1
- package/dist/core/context-guard.js +3 -2
- package/dist/core/context-settings.d.ts +103 -0
- package/dist/core/context-settings.js +60 -0
- package/dist/core/engine-notice.d.ts +18 -1
- package/dist/core/frame-settlement.d.ts +97 -0
- package/dist/core/frame-settlement.js +36 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +10 -10
- package/dist/core/mailbox-store.d.ts +41 -7
- package/dist/core/mailbox-store.js +9 -3
- package/dist/core/message-utils.d.ts +13 -0
- package/dist/core/message-utils.js +6 -0
- package/dist/core/parent-spec-seat.d.ts +14 -1
- package/dist/core/parent-spec-seat.js +6 -0
- package/dist/core/permission-rule-model.js +1 -1
- package/dist/core/roles.d.ts +20 -0
- package/dist/core/roles.js +35 -16
- package/dist/core/runner/assemble-result.d.ts +2 -0
- package/dist/core/runner/assemble-result.js +3 -1
- package/dist/core/runner/compaction-call-options.d.ts +26 -2
- package/dist/core/runner/compaction-call-options.js +9 -0
- package/dist/core/runner/compaction-seams.d.ts +4 -3
- package/dist/core/runner/compaction-seams.js +13 -6
- package/dist/core/runner/contracts.d.ts +61 -7
- package/dist/core/runner/decide-continuation.d.ts +2 -1
- package/dist/core/runner/decide-continuation.js +11 -10
- package/dist/core/runner/initial-run-state.js +2 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
- package/dist/core/runner/prepare-config-doors.js +4 -1
- package/dist/core/runner/prepare-context-lane.d.ts +9 -2
- package/dist/core/runner/prepare-context-lane.js +15 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
- package/dist/core/runner/prepare-delegation-surface.js +9 -3
- package/dist/core/runner/prepare-file-history.js +10 -8
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
- package/dist/core/runner/prepare-gate-stations.js +3 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
- package/dist/core/runner/prepare-prompt-inputs.js +2 -1
- package/dist/core/runner/prepare-task.js +22 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
- package/dist/core/runner/prepare-wiring-manifest.js +6 -1
- package/dist/core/runner/response-model-seat.d.ts +16 -0
- package/dist/core/runner/response-model-seat.js +27 -0
- package/dist/core/runner/retired-runner-deps.d.ts +3 -1
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
- package/dist/core/runner/run-compaction-machinery.js +5 -3
- package/dist/core/runner/run-harness-handlers.js +9 -2
- package/dist/core/runner/run-identity-wiring.js +19 -1
- package/dist/core/runner/run-leg.js +1 -1
- package/dist/core/runner/run-notification-lane.js +8 -2
- package/dist/core/runner/run-recovery-lanes.js +9 -5
- package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
- package/dist/core/runner/run-settle-and-teardown.js +6 -10
- package/dist/core/runner/run-stop-and-final-verify.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +5 -5
- package/dist/core/runner/run-turn-boundary.js +32 -5
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +33 -2
- package/dist/core/runner-deps.d.ts +44 -13
- package/dist/core/side-query.d.ts +6 -5
- package/dist/core/side-query.js +3 -1
- package/dist/core/swappable-deps.d.ts +20 -4
- package/dist/core/swappable-deps.js +6 -1
- package/dist/core/task-event.d.ts +13 -0
- package/dist/core/task-notification.d.ts +144 -2
- package/dist/core/task-notification.js +154 -5
- package/dist/core/task-result.d.ts +27 -3
- package/dist/core/task-spec.d.ts +7 -0
- package/dist/core/tool-catalog-entries.js +2 -1
- package/dist/core/tool-conformance.js +1 -0
- package/dist/core/tool-policy.d.ts +9 -4
- package/dist/core/tool-spec.d.ts +14 -0
- package/dist/core/wiring-manifest.d.ts +12 -0
- package/dist/core/wiring-manifest.js +5 -1
- package/dist/engine/harness/agent-harness.d.ts +14 -1
- package/dist/engine/harness/agent-harness.js +28 -3
- package/dist/engine/harness/types.d.ts +3 -1
- package/dist/engine/llm/types.d.ts +8 -7
- package/dist/engine/llm/types.js +1 -1
- package/dist/engine/session/import-validate.js +4 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +7 -5
- package/dist/orchestration/run-workflow-tool.d.ts +4 -4
- package/dist/orchestration/workflow.d.ts +1 -1
- package/dist/orchestration/workflow.js +8 -5
- package/dist/shell/lex.d.ts +30 -51
- package/dist/shell/lex.js +237 -284
- package/dist/stores/cc/mailbox-store.js +3 -2
- package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
- package/dist/stores/file/file-history-store.d.ts +2 -4
- package/dist/stores/file/file-history-store.js +1 -1
- package/dist/stores/file/fs-atomic.d.ts +16 -11
- package/dist/stores/file/fs-atomic.js +17 -6
- package/dist/stores/file/index.d.ts +5 -6
- package/dist/stores/file/index.js +1 -1
- package/dist/stores/file/mailbox-store.d.ts +4 -9
- package/dist/stores/file/mailbox-store.js +29 -30
- package/dist/stores/file/session-policy-store.d.ts +2 -0
- package/dist/stores/file/session-policy-store.js +1 -1
- package/dist/stores/file/session-store.d.ts +11 -7
- package/dist/stores/file/session-store.js +23 -16
- package/dist/stores/file/shared-ledger.js +3 -14
- package/dist/stores/file/tool-result-store.d.ts +2 -0
- package/dist/stores/file/tool-result-store.js +5 -2
- package/dist/stores/file/workflow-journal-store.js +14 -9
- package/dist/tools/fs/bash-program-position.d.ts +15 -24
- package/dist/tools/fs/bash-program-position.js +11 -29
- package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
- package/dist/tools/fs/bash-readonly-classifier.js +400 -272
- package/dist/tools/fs/shell-read-check.js +3 -3
- package/dist/tools/fs/shell-read-establish.js +1 -1
- package/dist/tools/fs/shell-read-plan.d.ts +11 -6
- package/dist/tools/fs/shell-read-plan.js +167 -183
- package/dist/tools/fs/shell-read-word.d.ts +50 -0
- package/dist/tools/fs/shell-read-word.js +59 -0
- package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
- package/dist/tools/fs/shell-reentry-text.js +73 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +58 -8
- package/wire-changes/core@7.29.0.json +185 -0
- package/wire-changes/core@7.30.0.json +1821 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf } from "./safety.js";
|
|
2
|
-
import {
|
|
3
|
-
import { COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord,
|
|
2
|
+
import { PARSE_FAILURE_SENTENCE, UNRESOLVED_SENTENCE, argvOf, lexShell, literalTildeSpelling, } from "../../shell/lex.js";
|
|
3
|
+
import { COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
|
|
4
4
|
export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
5
5
|
"ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
|
|
6
6
|
"grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
|
|
@@ -15,20 +15,33 @@ const SHELL_OPERATORS = /[;&|<>$()`\n\r\\]/;
|
|
|
15
15
|
function hasBareShellOperator(s, quotedOperatorsAreText) {
|
|
16
16
|
if (!quotedOperatorsAreText)
|
|
17
17
|
return SHELL_OPERATORS.test(s);
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
18
|
+
const lexed = lexShell(s);
|
|
19
|
+
if ("parseFailure" in lexed)
|
|
20
20
|
return SHELL_OPERATORS.test(s);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
21
|
+
if (lexed.segments.length > 1)
|
|
22
|
+
return true;
|
|
23
|
+
for (const t of lexed.segments[0].tokens) {
|
|
24
|
+
if (t.kind !== "word")
|
|
25
|
+
return true;
|
|
26
|
+
if (expandsDollar(t) || t.assignment?.form === "compound")
|
|
27
|
+
return true;
|
|
28
|
+
if (t.pieces.some((p) => p.quotedBy === "\\" || (p.quotedBy === undefined && p.literal !== undefined && /[\r\\]/.test(p.literal))))
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
function expandsDollar(w) {
|
|
34
|
+
if (w.quotedSpans.some((q) => q.by === '$"'))
|
|
35
|
+
return true;
|
|
36
|
+
for (const p of w.pieces) {
|
|
37
|
+
const k = p.expansion?.kind;
|
|
38
|
+
if (k === "parameter" || k === "command" || k === "process" || k === "arithmetic")
|
|
39
|
+
return true;
|
|
40
|
+
if (p.literal !== undefined && (p.quotedBy === undefined || p.quotedBy === '"') && /[$`]/.test(p.literal))
|
|
27
41
|
return true;
|
|
28
42
|
}
|
|
29
43
|
return false;
|
|
30
44
|
}
|
|
31
|
-
const SHELL_EXPANDS_IN_DOUBLE_QUOTES = /[$`]/;
|
|
32
45
|
export function parseLeadingCommandName(command, options) {
|
|
33
46
|
const trimmed = command.trim();
|
|
34
47
|
if (!trimmed)
|
|
@@ -38,12 +51,14 @@ export function parseLeadingCommandName(command, options) {
|
|
|
38
51
|
reject: "shell operators (pipes, redirects, ;, &&, command substitution, subshells, variable expansion) are not allowed — run a single simple command",
|
|
39
52
|
};
|
|
40
53
|
}
|
|
41
|
-
const first = trimmed.split(
|
|
54
|
+
const first = trimmed.split(/[ \t]+/)[0];
|
|
42
55
|
if (first.includes("/") && options?.pathPrefixedNameIsText !== true) {
|
|
43
56
|
return { reject: "the command must be a bare name resolved via PATH (no path prefix)" };
|
|
44
57
|
}
|
|
45
58
|
if (first.includes("="))
|
|
46
59
|
return { reject: "leading environment-variable assignments are not allowed" };
|
|
60
|
+
if (first.startsWith("#"))
|
|
61
|
+
return { reject: "empty command" };
|
|
47
62
|
if (/["'{}*?[\]~]/.test(first))
|
|
48
63
|
return { reject: "the command name must be a bare token (no quotes, braces, globs, or ~)" };
|
|
49
64
|
return { name: first };
|
|
@@ -62,96 +77,81 @@ export function carriesShellRedirection(text) {
|
|
|
62
77
|
return SHELL_REDIRECTION_OPERATORS.test(text);
|
|
63
78
|
}
|
|
64
79
|
const SHELL_REDIRECTION_OPERATORS = /[<>]/;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const isSpace = (c) => c === " " || c === "\t";
|
|
70
|
-
for (let i = 0; i < source.length; i++) {
|
|
71
|
-
if (quoted(i) || !SHELL_REDIRECTION_OPERATORS.test(source[i]))
|
|
72
|
-
continue;
|
|
73
|
-
let j = i + 1;
|
|
74
|
-
while (j < source.length && !quoted(j) && (SHELL_REDIRECTION_OPERATORS.test(source[j]) || isSpace(source[j])))
|
|
75
|
-
j++;
|
|
76
|
-
if (j < source.length && !quoted(j) && SHELL_CONNECTOR_CHARS.test(source[j]))
|
|
77
|
-
return true;
|
|
78
|
-
i = j - 1;
|
|
79
|
-
}
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
function rejectedSansRedirection(source) {
|
|
83
|
-
if (redirectionPrecedesConnector(source)) {
|
|
84
|
-
return "a redirection followed by `|`, `&` or `;` is not allowed — the operator's own characters cannot be told apart from a command boundary";
|
|
85
|
-
}
|
|
86
|
-
return hardRejectReason(source, true);
|
|
80
|
+
function isDataFreeRedirection(r) {
|
|
81
|
+
if (r.op === ">" && r.fd === 2 && r.target !== undefined && r.target.raw === "/dev/null")
|
|
82
|
+
return true;
|
|
83
|
+
return r.op === ">&" && r.dup !== undefined && /^[12]$/.test(r.dup) && (r.fd === undefined || r.fd === 1 || r.fd === 2);
|
|
87
84
|
}
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
const BACKGROUND_REFUSAL = "`&` backgrounding is not allowed — a backgrounded process outlives the command";
|
|
86
|
+
const PIPE_BOTH_REFUSAL = "`|&` pipes standard error as well as standard output — not a connector this face reads";
|
|
87
|
+
export function strictResidualRefusal(lexed, source, opts = {}) {
|
|
88
|
+
const tolerate = opts.tolerateRedirection === true;
|
|
89
|
+
const reason = tolerate
|
|
90
90
|
? "command/variable substitution, subshells, escapes, and line breaks are not allowed"
|
|
91
91
|
: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed";
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const inClass = (text) => {
|
|
93
|
+
for (const ch of text)
|
|
94
|
+
if (SHELL_SEGMENT_QUOTE_BLIND_REJECT.test(ch) && !(tolerate && SHELL_REDIRECTION_OPERATORS.test(ch)))
|
|
95
|
+
return true;
|
|
96
|
+
return false;
|
|
97
|
+
};
|
|
98
|
+
if ("parseFailure" in lexed)
|
|
99
|
+
return inClass(source) || SHELL_EXPANSION_CHARS.test(source) ? reason : PARSE_FAILURE_SENTENCE[lexed.parseFailure.reason];
|
|
100
|
+
const words = [];
|
|
101
|
+
for (const s of lexed.segments) {
|
|
102
|
+
if (s.connector === "\n")
|
|
94
103
|
return reason;
|
|
104
|
+
for (const t of s.tokens) {
|
|
105
|
+
if (t.kind === "word") {
|
|
106
|
+
if (inClass(t.raw))
|
|
107
|
+
return reason;
|
|
108
|
+
words.push(t);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (t.kind !== "redirect")
|
|
112
|
+
return reason;
|
|
113
|
+
if (opts.dataFreeRedirections === true && isDataFreeRedirection(t))
|
|
114
|
+
continue;
|
|
115
|
+
if (!tolerate)
|
|
116
|
+
return reason;
|
|
117
|
+
if (t.target !== undefined) {
|
|
118
|
+
if (inClass(t.target.raw))
|
|
119
|
+
return reason;
|
|
120
|
+
words.push(t.target);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
95
123
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!SHELL_EXPANSION_CHARS.test(source[i]))
|
|
101
|
-
continue;
|
|
102
|
-
if (mask.balanced && mask.openedBy[i] === "'")
|
|
103
|
-
continue;
|
|
104
|
-
return reason;
|
|
124
|
+
const home = opts.homeDir !== undefined && !homeMayBeRebound(source) ? opts.homeDir : undefined;
|
|
125
|
+
for (const w of words) {
|
|
126
|
+
if (expandsDollar(w) && !(home !== undefined && homeSpelledForClassify(w, home) !== undefined))
|
|
127
|
+
return reason;
|
|
105
128
|
}
|
|
129
|
+
for (const s of lexed.segments) {
|
|
130
|
+
if (s.connector === "&")
|
|
131
|
+
return BACKGROUND_REFUSAL;
|
|
132
|
+
if (s.pipesStderr)
|
|
133
|
+
return PIPE_BOTH_REFUSAL;
|
|
134
|
+
}
|
|
135
|
+
for (const w of words)
|
|
136
|
+
if (w.unresolved !== undefined)
|
|
137
|
+
return UNRESOLVED_SENTENCE[w.unresolved];
|
|
106
138
|
return undefined;
|
|
107
139
|
}
|
|
108
140
|
export function splitShellCompoundSegments(source, options) {
|
|
109
|
-
if (options?.redirection === "tolerate") {
|
|
110
|
-
const refused = rejectedSansRedirection(source);
|
|
111
|
-
if (refused !== undefined)
|
|
112
|
-
return { reject: refused };
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
const refused = hardRejectReason(source, false);
|
|
116
|
-
if (refused !== undefined)
|
|
117
|
-
return { reject: refused };
|
|
118
|
-
}
|
|
119
141
|
const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
return command;
|
|
134
|
-
let out = "";
|
|
135
|
-
let word = "";
|
|
136
|
-
let wordQuoted = false;
|
|
137
|
-
const flush = () => {
|
|
138
|
-
out += wordQuoted || !EXEMPT_REDIRECTION.test(word) ? word : "";
|
|
139
|
-
word = "";
|
|
140
|
-
wordQuoted = false;
|
|
141
|
-
};
|
|
142
|
-
for (let i = 0; i < command.length; i++) {
|
|
143
|
-
const ch = command[i];
|
|
144
|
-
if (!mask.quoted[i] && (ch === " " || ch === "\t")) {
|
|
145
|
-
flush();
|
|
146
|
-
out += ch;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (mask.quoted[i])
|
|
150
|
-
wordQuoted = true;
|
|
151
|
-
word += ch;
|
|
152
|
-
}
|
|
153
|
-
flush();
|
|
154
|
-
return out;
|
|
142
|
+
if (/[\n\r]/.test(source.slice(scanned.length)))
|
|
143
|
+
return { reject: options?.redirection === "tolerate" ? "command/variable substitution, subshells, escapes, and line breaks are not allowed" : "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
|
|
144
|
+
const lexed = lexShell(scanned);
|
|
145
|
+
const refused = strictResidualRefusal(lexed, scanned, {
|
|
146
|
+
tolerateRedirection: options?.redirection === "tolerate",
|
|
147
|
+
dataFreeRedirections: options?.redirection === "data-free",
|
|
148
|
+
...(options?.homeDir !== undefined ? { homeDir: options.homeDir } : {}),
|
|
149
|
+
});
|
|
150
|
+
if (refused !== undefined)
|
|
151
|
+
return { reject: refused };
|
|
152
|
+
const command = lexed;
|
|
153
|
+
const connectors = command.segments.map((s) => s.connector);
|
|
154
|
+
return { segments: command.segments.map((s) => s.text), pipeFed: connectors.map((k) => k === "|"), connectors, lexed: command.segments };
|
|
155
155
|
}
|
|
156
156
|
export function formatOutOfRootReadApprovalOption(directory) {
|
|
157
157
|
const trimmed = directory.replace(/[/\\]+$/, "");
|
|
@@ -200,56 +200,36 @@ function reheadSegment(toks, head, from = 0) {
|
|
|
200
200
|
return { folded: [head, ...toks.folded.slice(from + 1)], raw: [head, ...toks.raw.slice(from + 1)] };
|
|
201
201
|
}
|
|
202
202
|
export const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
203
|
-
function
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
const close = ansiCQuoteClose(rawToken, i + 1);
|
|
221
|
-
if (close === undefined)
|
|
222
|
-
return true;
|
|
223
|
-
i = close;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
227
|
-
open = ch;
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (open === ch) {
|
|
231
|
-
open = undefined;
|
|
203
|
+
function segmentSpellings(words, homeDir, homeRebound) {
|
|
204
|
+
const folded = [];
|
|
205
|
+
const raw = [];
|
|
206
|
+
for (const w of words) {
|
|
207
|
+
const spelled = homeDir !== undefined && !homeRebound ? homeSpelledForClassify(w, homeDir) : undefined;
|
|
208
|
+
folded.push(spelled ?? w.folded);
|
|
209
|
+
raw.push(spelled ?? w.raw);
|
|
210
|
+
}
|
|
211
|
+
return { folded, raw };
|
|
212
|
+
}
|
|
213
|
+
export function expansionInWord(w) {
|
|
214
|
+
for (const p of w.pieces) {
|
|
215
|
+
const k = p.expansion?.kind;
|
|
216
|
+
if (k === "parameter" || k === "command" || k === "process" || k === "arithmetic" || k === "brace")
|
|
217
|
+
return true;
|
|
218
|
+
if (p.literal === undefined)
|
|
232
219
|
continue;
|
|
233
|
-
}
|
|
234
|
-
if (open === undefined && (ch === "{" || ch === "}" || ch === "$" || ch === "`"))
|
|
220
|
+
if (p.quotedBy === undefined && /[{}$`]/.test(p.literal))
|
|
235
221
|
return true;
|
|
236
|
-
if (
|
|
222
|
+
if (p.quotedBy === '"' && /[$`]/.test(p.literal))
|
|
237
223
|
return true;
|
|
238
224
|
}
|
|
239
225
|
return false;
|
|
240
226
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
for (const
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
if (open === ch) {
|
|
249
|
-
open = undefined;
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
if (open === undefined && (ch === "*" || ch === "?" || ch === "["))
|
|
227
|
+
const isGlobPiece = (p) => p.expansion?.kind === "glob";
|
|
228
|
+
export function globInWord(w) {
|
|
229
|
+
for (const p of w.pieces) {
|
|
230
|
+
if (isGlobPiece(p))
|
|
231
|
+
return true;
|
|
232
|
+
if (p.literal !== undefined && p.quotedBy === undefined && /[*?[]/.test(p.literal))
|
|
253
233
|
return true;
|
|
254
234
|
}
|
|
255
235
|
return false;
|
|
@@ -1298,24 +1278,24 @@ function argumentAsProgramRefusal(written, name, args) {
|
|
|
1298
1278
|
const clause = inlineProgramClause(interpreter, args);
|
|
1299
1279
|
return clause === undefined ? undefined : unjudgedProgramTextRefusal(written, clause);
|
|
1300
1280
|
}
|
|
1301
|
-
function guardedVerbFormReason(name, toks,
|
|
1281
|
+
function guardedVerbFormReason(name, toks, args) {
|
|
1302
1282
|
if (name === "find") {
|
|
1303
1283
|
for (const t of toks) {
|
|
1304
1284
|
if (FIND_DANGEROUS_PREDICATES.has(t)) {
|
|
1305
1285
|
return `\`find ${t}\` executes a command or writes a file — not a read ${NOT_AUTO_ALLOWED}`;
|
|
1306
1286
|
}
|
|
1307
1287
|
}
|
|
1308
|
-
for (const
|
|
1309
|
-
if (
|
|
1310
|
-
return `\`find\` is given the unquoted pattern "${
|
|
1288
|
+
for (const w of args) {
|
|
1289
|
+
if (globInWord(w) || expansionInWord(w)) {
|
|
1290
|
+
return `\`find\` is given the unquoted pattern "${w.raw}", which the shell may expand into other arguments before find runs ${NOT_AUTO_ALLOWED}`;
|
|
1311
1291
|
}
|
|
1312
1292
|
}
|
|
1313
1293
|
return undefined;
|
|
1314
1294
|
}
|
|
1315
1295
|
if (name === "sed") {
|
|
1316
|
-
for (const
|
|
1317
|
-
if (
|
|
1318
|
-
return `\`sed\` is given the unquoted pattern "${
|
|
1296
|
+
for (const w of args) {
|
|
1297
|
+
if (globInWord(w) || expansionInWord(w)) {
|
|
1298
|
+
return `\`sed\` is given the unquoted pattern "${w.raw}", which the shell may expand into other arguments (a file named like a flag becomes a live option) before sed runs ${NOT_AUTO_ALLOWED}`;
|
|
1319
1299
|
}
|
|
1320
1300
|
}
|
|
1321
1301
|
const flags = [];
|
|
@@ -1361,7 +1341,7 @@ function guardedVerbFormReason(name, toks, raw) {
|
|
|
1361
1341
|
return undefined;
|
|
1362
1342
|
}
|
|
1363
1343
|
if (name === "sleep") {
|
|
1364
|
-
return pollLoopSleepReason(toks
|
|
1344
|
+
return pollLoopSleepReason(toks);
|
|
1365
1345
|
}
|
|
1366
1346
|
if (name === "seq") {
|
|
1367
1347
|
for (const t of toks.slice(1)) {
|
|
@@ -1458,51 +1438,13 @@ function isGrepFileStdinLongOption(tok) {
|
|
|
1458
1438
|
return name !== undefined && isLongOptionAbbrevOf(name, "file");
|
|
1459
1439
|
}
|
|
1460
1440
|
const CLUSTER_OPTION_CHAR = /[A-Za-z0-9]/;
|
|
1461
|
-
function tildeExpandsInValue(rawValue) {
|
|
1462
|
-
if (rawValue === undefined)
|
|
1463
|
-
return false;
|
|
1464
|
-
for (let at = 0; at <= rawValue.length;) {
|
|
1465
|
-
if (rawValue[at] === "~") {
|
|
1466
|
-
let quoted = false;
|
|
1467
|
-
for (let k = at + 1; k < rawValue.length && rawValue[k] !== "/" && rawValue[k] !== ":"; k++) {
|
|
1468
|
-
if (rawValue[k] === "'" || rawValue[k] === '"' || rawValue[k] === "\\") {
|
|
1469
|
-
quoted = true;
|
|
1470
|
-
break;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
if (!quoted)
|
|
1474
|
-
return true;
|
|
1475
|
-
}
|
|
1476
|
-
const next = rawValue.indexOf(":", at);
|
|
1477
|
-
if (next < 0)
|
|
1478
|
-
return false;
|
|
1479
|
-
at = next + 1;
|
|
1480
|
-
}
|
|
1481
|
-
return false;
|
|
1482
|
-
}
|
|
1483
|
-
export function assignmentWordPayload(word, raw = word) {
|
|
1484
|
-
const written = writtenAssignmentPrefix(word);
|
|
1485
|
-
if (written === undefined)
|
|
1486
|
-
return undefined;
|
|
1487
|
-
const eq = written.prefix.length - 1;
|
|
1488
|
-
const spelled = word.slice(eq + 1);
|
|
1489
|
-
const expands = tildeExpandsInValue(raw.startsWith(written.prefix) ? raw.slice(written.prefix.length) : undefined);
|
|
1490
|
-
const value = expands ? spelled : literalTildeSpelling(spelled);
|
|
1491
|
-
if (written.appends) {
|
|
1492
|
-
const reason = `"${word}" appends to a value the text does not contain`;
|
|
1493
|
-
return spelled === "" || expands ? { kind: "unresolved", reason } : { kind: "unresolved", reason, value };
|
|
1494
|
-
}
|
|
1495
|
-
if (expands)
|
|
1496
|
-
return { kind: "unresolved", reason: `"${word}" carries a tilde whose expansion the text does not settle` };
|
|
1497
|
-
return spelled === "" ? undefined : { kind: "value", value };
|
|
1498
|
-
}
|
|
1499
1441
|
export const ASSIGNMENT_VALUE_NOT_PATHS = new Set(["make", "gmake", "bmake", "cmake", "ninja", "meson", "docker", "podman"]);
|
|
1500
|
-
export function argvProgramOf(
|
|
1501
|
-
const pos = readProgramPosition(
|
|
1442
|
+
export function argvProgramOf(words) {
|
|
1443
|
+
const pos = readProgramPosition(words);
|
|
1502
1444
|
if (pos.program === undefined)
|
|
1503
1445
|
return undefined;
|
|
1504
1446
|
const at = pos.program.index;
|
|
1505
|
-
return
|
|
1447
|
+
return words[at]?.assignment !== undefined ? undefined : { program: pos.program.basename, from: at };
|
|
1506
1448
|
}
|
|
1507
1449
|
export function attachedOptionPayloads(tok) {
|
|
1508
1450
|
const literal = literalTildeSpelling;
|
|
@@ -1525,83 +1467,239 @@ export function attachedOptionPayloads(tok) {
|
|
|
1525
1467
|
}
|
|
1526
1468
|
return out;
|
|
1527
1469
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1470
|
+
const HOME_EXPANDS_TO_ONE_WORD = /^[A-Za-z0-9_./+@:%=,-]+$/;
|
|
1471
|
+
export function homeExpansionOf(w, home) {
|
|
1472
|
+
const [head, ...rest] = w.pieces;
|
|
1473
|
+
const e = head?.expansion;
|
|
1474
|
+
if (head === undefined || e === undefined || e.kind !== "parameter" || e.name !== "HOME" || (e.body !== undefined && e.body !== "HOME"))
|
|
1475
|
+
return undefined;
|
|
1476
|
+
if (head.quotedBy !== undefined && head.quotedBy !== '"')
|
|
1477
|
+
return undefined;
|
|
1478
|
+
if (head.quotedBy === undefined && !HOME_EXPANDS_TO_ONE_WORD.test(home))
|
|
1479
|
+
return undefined;
|
|
1480
|
+
if (rest.some((p) => p.expansion !== undefined || p.literal === undefined))
|
|
1481
|
+
return undefined;
|
|
1482
|
+
const next = rest[0];
|
|
1483
|
+
if (next !== undefined) {
|
|
1484
|
+
if (head.quotedBy === undefined && !(next.literal.startsWith("/") || next.quotedBy === '"'))
|
|
1485
|
+
return undefined;
|
|
1486
|
+
if (head.quotedBy === '"' && next.quotedBy === '"' && !next.literal.startsWith("/"))
|
|
1487
|
+
return undefined;
|
|
1488
|
+
}
|
|
1489
|
+
return { spelled: `${home}${rest.map((p) => p.literal).join("")}` };
|
|
1490
|
+
}
|
|
1491
|
+
const CLASSIFY_HOME_REREADABLE = /["$`\\\n\r]/;
|
|
1492
|
+
function homeSpelledForClassify(w, home) {
|
|
1493
|
+
return CLASSIFY_HOME_REREADABLE.test(home) ? undefined : homeExpansionOf(w, home)?.spelled;
|
|
1494
|
+
}
|
|
1495
|
+
const HOME_PLAIN_READ = /\$HOME(?![A-Za-z0-9_])|\$\{HOME\}/g;
|
|
1496
|
+
const HOME_NAME = /HOME(?![A-Za-z0-9_])/;
|
|
1497
|
+
export function homeMayBeRebound(text) {
|
|
1498
|
+
const unquoted = text.replace(/\\\r?\n/g, "").replace(HOME_PLAIN_READ, " ").replace(/\$?["']|\\/g, "");
|
|
1499
|
+
if (HOME_NAME.test(unquoted))
|
|
1500
|
+
return true;
|
|
1501
|
+
if (hasUnquotedExtglobOpener(text))
|
|
1502
|
+
return true;
|
|
1503
|
+
if (!unquoted.includes("printf") && !/[*?[{]/.test(unquoted))
|
|
1504
|
+
return false;
|
|
1505
|
+
const lexed = lexShell(text);
|
|
1506
|
+
return !("parseFailure" in lexed) && lexed.segments.some((seg) => {
|
|
1507
|
+
const words = argvOf(seg);
|
|
1508
|
+
return printfNamesComputed(words) || words.some((w) => globMayMatchHome(w) || optionWordCarriesGlob(w) || wordHasBraceList(w));
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
function hasUnquotedExtglobOpener(text) {
|
|
1512
|
+
const lexed = lexShell(text);
|
|
1513
|
+
if ("parseFailure" in lexed)
|
|
1514
|
+
return true;
|
|
1515
|
+
for (const seg of lexed.segments) {
|
|
1516
|
+
for (const tok of seg.tokens) {
|
|
1517
|
+
const t = tok.kind === "word" ? tok : tok.kind === "redirect" ? tok.target : undefined;
|
|
1518
|
+
if (t === undefined)
|
|
1519
|
+
continue;
|
|
1520
|
+
const last = t.pieces[t.pieces.length - 1];
|
|
1521
|
+
if (last === undefined || last.quotedBy !== undefined)
|
|
1522
|
+
continue;
|
|
1523
|
+
if (!/[?*+@!]$/.test(text.slice(last.span[0], last.span[1])))
|
|
1524
|
+
continue;
|
|
1525
|
+
let j = last.span[1];
|
|
1526
|
+
while (text[j] === "\\" && (text[j + 1] === "\n" || (text[j + 1] === "\r" && text[j + 2] === "\n")))
|
|
1527
|
+
j += text[j + 1] === "\r" ? 3 : 2;
|
|
1528
|
+
if (text[j] === "(")
|
|
1529
|
+
return true;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
1534
|
+
function wordHasBraceList(w) {
|
|
1535
|
+
return w.pieces.some((p) => p.expansion?.kind === "brace");
|
|
1536
|
+
}
|
|
1537
|
+
function optionWordCarriesGlob(w) {
|
|
1538
|
+
if (!w.pieces.some(isGlobPiece))
|
|
1539
|
+
return false;
|
|
1540
|
+
const atoms = patternAtoms(w.pieces);
|
|
1541
|
+
if (atoms === undefined)
|
|
1542
|
+
return UNREADABLE_GLOB_IS_A_REBINDING;
|
|
1543
|
+
try {
|
|
1544
|
+
const reversed = atoms.slice().reverse();
|
|
1545
|
+
return closureReaches(atoms, "-") && (closureReaches(reversed, "EMOH") || closureReaches(reversed, "]"));
|
|
1546
|
+
}
|
|
1547
|
+
catch {
|
|
1548
|
+
return UNREADABLE_GLOB_IS_A_REBINDING;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const UNREADABLE_GLOB_IS_A_REBINDING = true;
|
|
1552
|
+
function escapeRegExp(s) {
|
|
1553
|
+
return s.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
1554
|
+
}
|
|
1555
|
+
function globMayMatchHome(w) {
|
|
1556
|
+
if (!w.pieces.some(isGlobPiece))
|
|
1557
|
+
return false;
|
|
1558
|
+
const atoms = patternAtoms(w.pieces);
|
|
1559
|
+
if (atoms === undefined)
|
|
1560
|
+
return UNREADABLE_GLOB_IS_A_REBINDING;
|
|
1561
|
+
try {
|
|
1562
|
+
if (new RegExp(`^${atoms.join("")}$`).test("HOME"))
|
|
1563
|
+
return true;
|
|
1564
|
+
return closureReaches(atoms, "HOME[") && closureReaches(atoms.slice().reverse(), "]");
|
|
1565
|
+
}
|
|
1566
|
+
catch {
|
|
1567
|
+
return UNREADABLE_GLOB_IS_A_REBINDING;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
function patternAtoms(pieces) {
|
|
1571
|
+
const atoms = [];
|
|
1572
|
+
for (const p of pieces) {
|
|
1573
|
+
if (p.literal !== undefined && p.expansion === undefined) {
|
|
1574
|
+
for (const ch of p.literal)
|
|
1575
|
+
atoms.push(caseFoldedChar(ch));
|
|
1545
1576
|
continue;
|
|
1546
1577
|
}
|
|
1547
|
-
if (
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1578
|
+
if (isGlobPiece(p)) {
|
|
1579
|
+
const body = p.expansion.body;
|
|
1580
|
+
if (body === undefined) {
|
|
1581
|
+
atoms.push(RUN_ATOM);
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
const cls = translateBracket(body);
|
|
1585
|
+
if (cls === undefined)
|
|
1586
|
+
return undefined;
|
|
1587
|
+
atoms.push(cls);
|
|
1551
1588
|
continue;
|
|
1552
1589
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1590
|
+
atoms.push(RUN_ATOM);
|
|
1591
|
+
}
|
|
1592
|
+
return atoms;
|
|
1593
|
+
}
|
|
1594
|
+
const RUN_ATOM = ".*";
|
|
1595
|
+
function caseFoldedChar(ch) {
|
|
1596
|
+
const lower = ch.toLowerCase();
|
|
1597
|
+
const upper = ch.toUpperCase();
|
|
1598
|
+
return lower !== upper ? `[${escapeRegExp(lower)}${escapeRegExp(upper)}]` : escapeRegExp(ch);
|
|
1599
|
+
}
|
|
1600
|
+
function closureReaches(atoms, target) {
|
|
1601
|
+
let re = "";
|
|
1602
|
+
for (let i = atoms.length - 1; i >= 0; i--)
|
|
1603
|
+
re = `(?:${atoms[i]}${re})?`;
|
|
1604
|
+
return new RegExp(`^${re}$`).test(target);
|
|
1605
|
+
}
|
|
1606
|
+
function scanBracketBody(body) {
|
|
1607
|
+
const members = [];
|
|
1608
|
+
let computed = false;
|
|
1609
|
+
for (let i = 0; i < body.length; i++) {
|
|
1610
|
+
const c = body[i];
|
|
1611
|
+
if (c === "\\") {
|
|
1612
|
+
if (i + 1 < body.length)
|
|
1613
|
+
members.push({ ch: body[++i], unquoted: false });
|
|
1558
1614
|
continue;
|
|
1559
1615
|
}
|
|
1560
|
-
if (
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1616
|
+
if (c === "$" || c === "`" || c === "{")
|
|
1617
|
+
computed = true;
|
|
1618
|
+
members.push({ ch: c, unquoted: true });
|
|
1619
|
+
}
|
|
1620
|
+
return { members, computed };
|
|
1621
|
+
}
|
|
1622
|
+
function membersOpenClassExpression(members) {
|
|
1623
|
+
return members.some((m, i) => m.unquoted && m.ch === "[" && members[i + 1]?.unquoted === true && (members[i + 1].ch === ":" || members[i + 1].ch === "=" || members[i + 1].ch === "."));
|
|
1624
|
+
}
|
|
1625
|
+
function translateBracket(body) {
|
|
1626
|
+
if (body.includes('"') || body.includes("'"))
|
|
1627
|
+
return undefined;
|
|
1628
|
+
const { members, computed } = scanBracketBody(body);
|
|
1629
|
+
if (computed || membersOpenClassExpression(members))
|
|
1630
|
+
return undefined;
|
|
1631
|
+
let i = 0;
|
|
1632
|
+
let negated = false;
|
|
1633
|
+
if (members[0]?.unquoted === true && (members[0].ch === "!" || members[0].ch === "^")) {
|
|
1634
|
+
negated = true;
|
|
1635
|
+
i = 1;
|
|
1636
|
+
}
|
|
1637
|
+
const first = i;
|
|
1638
|
+
let cls = "";
|
|
1639
|
+
for (; i < members.length; i++) {
|
|
1640
|
+
const { ch, unquoted } = members[i];
|
|
1641
|
+
if (ch === "-" && unquoted && i > first && i < members.length - 1) {
|
|
1642
|
+
if (!negated)
|
|
1643
|
+
return undefined;
|
|
1644
|
+
cls += "-";
|
|
1645
|
+
continue;
|
|
1570
1646
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1647
|
+
const escaped = ch.replace(/[\\\]\[^-]/g, "\\$&");
|
|
1648
|
+
cls += !negated && ch.toLowerCase() !== ch.toUpperCase() ? `${escapeRegExp(ch.toLowerCase())}${escapeRegExp(ch.toUpperCase())}` : escaped;
|
|
1573
1649
|
}
|
|
1574
|
-
return
|
|
1650
|
+
return `[${negated ? "^" : ""}${cls || "\\]"}]`;
|
|
1651
|
+
}
|
|
1652
|
+
const literalWord = (w) => (w !== undefined && w.pieces.every((p) => p.literal !== undefined && p.expansion === undefined) ? w.pieces.map((p) => p.literal).join("") : undefined);
|
|
1653
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1654
|
+
function printfNamesComputed(words) {
|
|
1655
|
+
for (let i = 0; i < words.length; i++) {
|
|
1656
|
+
const head = literalWord(words[i]);
|
|
1657
|
+
if (head === undefined || programBasename(head) !== "printf")
|
|
1658
|
+
continue;
|
|
1659
|
+
for (let j = i + 1; j < words.length; j++) {
|
|
1660
|
+
const lit = literalWord(words[j]);
|
|
1661
|
+
const opt = lit ?? words[j].pieces[0]?.literal ?? "";
|
|
1662
|
+
if (lit === "--" || !opt.startsWith("-v"))
|
|
1663
|
+
break;
|
|
1664
|
+
if (lit === undefined)
|
|
1665
|
+
return true;
|
|
1666
|
+
const name = lit === "-v" ? words[++j] : undefined;
|
|
1667
|
+
if (lit === "-v" && name === undefined)
|
|
1668
|
+
break;
|
|
1669
|
+
if (!IDENTIFIER.test(lit === "-v" ? (literalWord(name) ?? "") : lit.slice(2)))
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return false;
|
|
1575
1674
|
}
|
|
1576
1675
|
function segmentCompoundForReadonly(command, homeDir) {
|
|
1577
|
-
const trimmed =
|
|
1676
|
+
const trimmed = command.trim();
|
|
1578
1677
|
if (!trimmed)
|
|
1579
1678
|
return { reject: "empty command" };
|
|
1580
|
-
const
|
|
1581
|
-
|
|
1582
|
-
? redirectionStripped.slice(0, -1)
|
|
1583
|
-
: redirectionStripped;
|
|
1584
|
-
return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep" });
|
|
1679
|
+
const terminatorStripped = trimmed.endsWith(";") ? trimmed.slice(0, -1) : trimmed;
|
|
1680
|
+
return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep", redirection: "data-free", ...(homeDir !== undefined ? { homeDir } : {}) });
|
|
1585
1681
|
}
|
|
1586
1682
|
export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
1587
1683
|
const split = segmentCompoundForReadonly(command, boundary?.homeDir);
|
|
1588
1684
|
if ("reject" in split)
|
|
1589
1685
|
return { reason: split.reject };
|
|
1590
1686
|
const { segments, pipeFed } = split;
|
|
1687
|
+
const homeRebound = boundary?.homeDir !== undefined && homeMayBeRebound(command);
|
|
1591
1688
|
let nameRefusal;
|
|
1592
1689
|
const unlistedSegments = new Set();
|
|
1593
1690
|
const reheaded = new Map();
|
|
1594
1691
|
for (let si = 0; si < segments.length; si++) {
|
|
1595
|
-
const
|
|
1692
|
+
const words = argvOf(split.lexed[si]);
|
|
1693
|
+
const toks = segmentSpellings(words, boundary?.homeDir, homeRebound);
|
|
1596
1694
|
const folded = toks.folded;
|
|
1597
|
-
const pos = readProgramPosition(
|
|
1695
|
+
const pos = readProgramPosition(words);
|
|
1598
1696
|
const from = pos.written ?? folded.length;
|
|
1599
1697
|
const written = folded[from];
|
|
1600
1698
|
if (written === undefined)
|
|
1601
1699
|
return { reason: "empty command" };
|
|
1602
1700
|
if (written === "coproc")
|
|
1603
1701
|
return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command" };
|
|
1604
|
-
if (isBareControlWord(
|
|
1702
|
+
if (isBareControlWord(words, from, SHELL_BLOCK_WORDS)) {
|
|
1605
1703
|
unlistedSegments.add(si);
|
|
1606
1704
|
nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
|
|
1607
1705
|
continue;
|
|
@@ -1682,13 +1780,16 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
1682
1780
|
if (unlistedSegments.has(si))
|
|
1683
1781
|
continue;
|
|
1684
1782
|
const rehead = reheaded.get(si);
|
|
1685
|
-
const
|
|
1783
|
+
const words = argvOf(split.lexed[si]);
|
|
1784
|
+
const spelled = segmentSpellings(words, boundary?.homeDir, homeRebound);
|
|
1785
|
+
const segmentTokens = rehead === undefined ? spelled : reheadSegment(spelled, rehead.head, rehead.from);
|
|
1786
|
+
const argWords = words.slice((rehead?.from ?? 0) + 1);
|
|
1686
1787
|
const toks = segmentTokens.folded;
|
|
1687
1788
|
if (toks.length === 0)
|
|
1688
1789
|
continue;
|
|
1689
1790
|
foldedSegments.push(segmentTokens);
|
|
1690
1791
|
const name = toks[0];
|
|
1691
|
-
const guardReason = guardedVerbFormReason(name, toks,
|
|
1792
|
+
const guardReason = guardedVerbFormReason(name, toks, argWords);
|
|
1692
1793
|
if (guardReason !== undefined) {
|
|
1693
1794
|
shapeRefusal ??= guardReason;
|
|
1694
1795
|
continue;
|
|
@@ -1776,31 +1877,53 @@ export const POLL_LOOP_MAX_BEATS = 120;
|
|
|
1776
1877
|
export const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
|
|
1777
1878
|
const POLL_BODY_HARD_REJECT = /[<>$()`|&{}\n\r\\]/;
|
|
1778
1879
|
const NON_ASCII_WHITESPACE = /[^\S \t]/;
|
|
1779
|
-
const
|
|
1780
|
-
function
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1880
|
+
const NOT_A_POLL_LOOP = "not a bounded read-only poll loop (`for <v> in <literal bound>; do <read commands>; done` is the only accepted control structure)";
|
|
1881
|
+
function pollLoopShapeOf(lexed) {
|
|
1882
|
+
const segments = [...lexed.segments];
|
|
1883
|
+
const last = segments[segments.length - 1];
|
|
1884
|
+
if (last !== undefined && last.tokens.length === 0 && last.connector === ";" && segments.length > 1)
|
|
1885
|
+
segments.pop();
|
|
1886
|
+
if (segments.length < 3)
|
|
1887
|
+
return undefined;
|
|
1888
|
+
const bare = (w, text) => w !== undefined && w.raw === w.folded && w.folded === text;
|
|
1889
|
+
const onlyWords = (s) => s.tokens.every((t) => t.kind === "word");
|
|
1890
|
+
const head = segments[0];
|
|
1891
|
+
if (!onlyWords(head))
|
|
1892
|
+
return undefined;
|
|
1893
|
+
const hw = argvOf(head);
|
|
1894
|
+
if (!bare(hw[0], "for") || !bare(hw[2], "in") || hw.length < 4)
|
|
1895
|
+
return undefined;
|
|
1896
|
+
const name = hw[1];
|
|
1897
|
+
if (name.raw !== name.folded || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name.folded))
|
|
1898
|
+
return undefined;
|
|
1899
|
+
const done = segments[segments.length - 1];
|
|
1900
|
+
const dw = argvOf(done);
|
|
1901
|
+
if (done.connector !== ";" || !onlyWords(done) || dw.length !== 1 || !bare(dw[0], "done"))
|
|
1902
|
+
return undefined;
|
|
1903
|
+
const first = segments[1];
|
|
1904
|
+
if (first.connector !== ";")
|
|
1905
|
+
return undefined;
|
|
1906
|
+
const fw = argvOf(first);
|
|
1907
|
+
if (!bare(fw[0], "do") || fw.length < 2)
|
|
1908
|
+
return undefined;
|
|
1909
|
+
const body = [{ words: fw.slice(1), segment: first }];
|
|
1910
|
+
for (const s of segments.slice(2, -1))
|
|
1911
|
+
body.push({ words: argvOf(s), segment: s });
|
|
1912
|
+
return { varName: name.folded, iterable: hw.slice(3), body };
|
|
1789
1913
|
}
|
|
1790
1914
|
function pollLoopBeatsOf(iterable) {
|
|
1791
|
-
const
|
|
1792
|
-
const
|
|
1915
|
+
const one = iterable.length === 1 && iterable[0].pieces.length === 1 ? iterable[0].pieces[0].expansion : undefined;
|
|
1916
|
+
const seq = one?.kind === "command" ? /^seq[ \t]+(\d{1,7})[ \t]+(\d{1,7})$/.exec(one.body ?? "") : null;
|
|
1917
|
+
const brace = seq === null && one?.kind === "brace" ? /^(\d{1,7})\.\.(\d{1,7})$/.exec(one.body ?? "") : null;
|
|
1793
1918
|
const range = seq ?? brace;
|
|
1794
1919
|
if (range !== null) {
|
|
1795
1920
|
const lo = Number(range[1]);
|
|
1796
1921
|
const hi = Number(range[2]);
|
|
1797
1922
|
return hi >= lo ? hi - lo + 1 : undefined;
|
|
1798
1923
|
}
|
|
1799
|
-
|
|
1800
|
-
return items.every((t) => /^[A-Za-z0-9_.-]{1,64}$/.test(t)) ? items.length : undefined;
|
|
1924
|
+
return iterable.every((w) => w.raw === w.folded && w.expansions.length === 0 && /^[A-Za-z0-9_.-]{1,64}$/.test(w.raw)) ? iterable.length : undefined;
|
|
1801
1925
|
}
|
|
1802
|
-
function pollLoopSleepReason(
|
|
1803
|
-
const toks = segment.split(/[ \t]+/);
|
|
1926
|
+
function pollLoopSleepReason(toks) {
|
|
1804
1927
|
if (toks.length !== 2)
|
|
1805
1928
|
return "`sleep` in a poll loop must take exactly one literal numeric argument";
|
|
1806
1929
|
const v = toks[1];
|
|
@@ -1816,42 +1939,47 @@ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary
|
|
|
1816
1939
|
if (NON_ASCII_WHITESPACE.test(command)) {
|
|
1817
1940
|
return { reason: "the command contains a non-ASCII or control whitespace character (only space and tab are allowed) — its word boundaries cannot be read the way bash would split them, so it is not auto-allowed" };
|
|
1818
1941
|
}
|
|
1819
|
-
const trimmed =
|
|
1942
|
+
const trimmed = command.trim();
|
|
1820
1943
|
if (!trimmed)
|
|
1821
1944
|
return { reason: "empty command" };
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1945
|
+
const lexed = lexShell(trimmed);
|
|
1946
|
+
if ("parseFailure" in lexed) {
|
|
1947
|
+
const r = lexed.parseFailure.reason;
|
|
1948
|
+
if (r === "unterminated_single_quote" || r === "unterminated_double_quote" || r === "unterminated_ansi_c") {
|
|
1949
|
+
return { reason: "unbalanced quote — the command's structure cannot be read reliably, so it is not auto-allowed" };
|
|
1950
|
+
}
|
|
1951
|
+
return { reason: NOT_A_POLL_LOOP };
|
|
1828
1952
|
}
|
|
1829
|
-
const
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
if (!/^[a-z]$/.test(varName)) {
|
|
1833
|
-
return { reason: `the loop variable "${varName}" must be a single lowercase letter — a name like PATH/IFS/LD_PRELOAD would change how the body's commands resolve` };
|
|
1953
|
+
const shape = pollLoopShapeOf(lexed);
|
|
1954
|
+
if (shape === undefined)
|
|
1955
|
+
return { reason: NOT_A_POLL_LOOP };
|
|
1956
|
+
if (!/^[a-z]$/.test(shape.varName)) {
|
|
1957
|
+
return { reason: `the loop variable "${shape.varName}" must be a single lowercase letter — a name like PATH/IFS/LD_PRELOAD would change how the body's commands resolve` };
|
|
1834
1958
|
}
|
|
1835
|
-
const beats = pollLoopBeatsOf(iterable);
|
|
1959
|
+
const beats = pollLoopBeatsOf(shape.iterable);
|
|
1836
1960
|
if (beats === undefined) {
|
|
1837
|
-
return { reason: `the loop bound "${iterable}" is not a recognized literal — only \`$(seq <int> <int>)\`, \`{<int>..<int>}\` (ascending) or a literal word list is accepted` };
|
|
1961
|
+
return { reason: `the loop bound "${shape.iterable.map((w) => w.raw).join(" ")}" is not a recognized literal — only \`$(seq <int> <int>)\`, \`{<int>..<int>}\` (ascending) or a literal word list is accepted` };
|
|
1838
1962
|
}
|
|
1839
1963
|
if (beats > POLL_LOOP_MAX_BEATS) {
|
|
1840
1964
|
return { reason: `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow` };
|
|
1841
1965
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1966
|
+
const homeRebound = boundary?.homeDir !== undefined && homeMayBeRebound(command);
|
|
1967
|
+
const bodies = shape.body.map((b) => ({ ...b, spelled: segmentSpellings(b.words, boundary?.homeDir, homeRebound).raw }));
|
|
1968
|
+
for (const b of bodies) {
|
|
1969
|
+
if (b.segment.connector !== ";" || b.segment.tokens.some((t) => t.kind !== "word") || b.spelled.some((raw) => POLL_BODY_HARD_REJECT.test(raw))) {
|
|
1970
|
+
return { reason: "the loop body may not contain redirection, pipes, backgrounding, substitution, subshells, braces, escapes, or line breaks" };
|
|
1971
|
+
}
|
|
1844
1972
|
}
|
|
1845
1973
|
const readSegments = [];
|
|
1846
|
-
for (const
|
|
1847
|
-
|
|
1848
|
-
if (segment.length === 0)
|
|
1974
|
+
for (const b of bodies) {
|
|
1975
|
+
if (b.words.length === 0)
|
|
1849
1976
|
return { reason: "empty command in the loop body" };
|
|
1850
|
-
const
|
|
1977
|
+
const segment = b.spelled.join(" ");
|
|
1978
|
+
const parsed = parseLeadingCommandName(segment, { quotedOperatorsAreText: true });
|
|
1851
1979
|
if ("reject" in parsed)
|
|
1852
1980
|
return { reason: parsed.reject };
|
|
1853
1981
|
if (parsed.name === "sleep") {
|
|
1854
|
-
const sleepReason = pollLoopSleepReason(
|
|
1982
|
+
const sleepReason = pollLoopSleepReason(b.spelled);
|
|
1855
1983
|
if (sleepReason !== undefined)
|
|
1856
1984
|
return { reason: sleepReason };
|
|
1857
1985
|
continue;
|