@sema-agent/core 7.29.0 → 7.30.1
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 +393 -0
- 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/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 +11 -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 +2 -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/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 +1 -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 +56 -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 +1 -1
- 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/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 +1 -1
- 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 +32 -1
- package/dist/core/runner-deps.d.ts +44 -13
- package/dist/core/secret-env.js +2 -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 +5 -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 +14 -0
- 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/lsp/node-lsp-manager.d.ts +3 -0
- package/dist/engine/lsp/node-lsp-manager.js +5 -3
- 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 +38 -54
- package/dist/shell/lex.js +239 -286
- 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 +131 -117
- package/dist/tools/fs/bash-readonly-classifier.js +404 -272
- package/dist/tools/fs/shell-read-check.js +3 -3
- package/dist/tools/fs/shell-read-establish.d.ts +1 -1
- package/dist/tools/fs/shell-read-establish.js +5 -33
- package/dist/tools/fs/shell-read-identity.d.ts +13 -0
- package/dist/tools/fs/shell-read-identity.js +39 -0
- package/dist/tools/fs/shell-read-judge.d.ts +8 -0
- package/dist/tools/fs/shell-read-judge.js +7 -0
- package/dist/tools/fs/shell-read-plan.d.ts +15 -6
- package/dist/tools/fs/shell-read-plan.js +171 -195
- package/dist/tools/fs/shell-read-word.d.ts +64 -0
- package/dist/tools/fs/shell-read-word.js +94 -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.30.0.json +1821 -0
- package/wire-changes/core@7.30.1.json +154 -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,243 @@ 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
|
+
return "parseFailure" in lexed || extglobOpenerIn(lexed, text);
|
|
1514
|
+
}
|
|
1515
|
+
export function extglobOpenerIn(lexed, text) {
|
|
1516
|
+
for (const seg of lexed.segments) {
|
|
1517
|
+
for (const [k, tok] of seg.tokens.entries()) {
|
|
1518
|
+
const next = seg.tokens[k + 1];
|
|
1519
|
+
if (tok.kind === "heredoc" && /[?*+@!]$/.test(tok.delimiter) && next?.kind === "group" && next.op === "(")
|
|
1520
|
+
return true;
|
|
1521
|
+
const t = tok.kind === "word" ? tok : tok.kind === "redirect" ? tok.target : undefined;
|
|
1522
|
+
if (t === undefined)
|
|
1523
|
+
continue;
|
|
1524
|
+
const last = t.pieces[t.pieces.length - 1];
|
|
1525
|
+
if (last === undefined || last.quotedBy !== undefined)
|
|
1526
|
+
continue;
|
|
1527
|
+
if (!/[?*+@!]$/.test(text.slice(last.span[0], last.span[1])))
|
|
1528
|
+
continue;
|
|
1529
|
+
let j = last.span[1];
|
|
1530
|
+
while (text[j] === "\\" && (text[j + 1] === "\n" || (text[j + 1] === "\r" && text[j + 2] === "\n")))
|
|
1531
|
+
j += text[j + 1] === "\r" ? 3 : 2;
|
|
1532
|
+
if (text[j] === "(")
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
function wordHasBraceList(w) {
|
|
1539
|
+
return w.pieces.some((p) => p.expansion?.kind === "brace");
|
|
1540
|
+
}
|
|
1541
|
+
function optionWordCarriesGlob(w) {
|
|
1542
|
+
if (!w.pieces.some(isGlobPiece))
|
|
1543
|
+
return false;
|
|
1544
|
+
const atoms = patternAtoms(w.pieces);
|
|
1545
|
+
if (atoms === undefined)
|
|
1546
|
+
return UNREADABLE_GLOB_IS_A_REBINDING;
|
|
1547
|
+
try {
|
|
1548
|
+
const reversed = atoms.slice().reverse();
|
|
1549
|
+
return closureReaches(atoms, "-") && (closureReaches(reversed, "EMOH") || closureReaches(reversed, "]"));
|
|
1550
|
+
}
|
|
1551
|
+
catch {
|
|
1552
|
+
return UNREADABLE_GLOB_IS_A_REBINDING;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
const UNREADABLE_GLOB_IS_A_REBINDING = true;
|
|
1556
|
+
function escapeRegExp(s) {
|
|
1557
|
+
return s.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
1558
|
+
}
|
|
1559
|
+
function globMayMatchHome(w) {
|
|
1560
|
+
if (!w.pieces.some(isGlobPiece))
|
|
1561
|
+
return false;
|
|
1562
|
+
const atoms = patternAtoms(w.pieces);
|
|
1563
|
+
if (atoms === undefined)
|
|
1564
|
+
return UNREADABLE_GLOB_IS_A_REBINDING;
|
|
1565
|
+
try {
|
|
1566
|
+
if (new RegExp(`^${atoms.join("")}$`).test("HOME"))
|
|
1567
|
+
return true;
|
|
1568
|
+
return closureReaches(atoms, "HOME[") && closureReaches(atoms.slice().reverse(), "]");
|
|
1569
|
+
}
|
|
1570
|
+
catch {
|
|
1571
|
+
return UNREADABLE_GLOB_IS_A_REBINDING;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function patternAtoms(pieces) {
|
|
1575
|
+
const atoms = [];
|
|
1576
|
+
for (const p of pieces) {
|
|
1577
|
+
if (p.literal !== undefined && p.expansion === undefined) {
|
|
1578
|
+
for (const ch of p.literal)
|
|
1579
|
+
atoms.push(caseFoldedChar(ch));
|
|
1545
1580
|
continue;
|
|
1546
1581
|
}
|
|
1547
|
-
if (
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1582
|
+
if (isGlobPiece(p)) {
|
|
1583
|
+
const body = p.expansion.body;
|
|
1584
|
+
if (body === undefined) {
|
|
1585
|
+
atoms.push(RUN_ATOM);
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
const cls = translateBracket(body);
|
|
1589
|
+
if (cls === undefined)
|
|
1590
|
+
return undefined;
|
|
1591
|
+
atoms.push(cls);
|
|
1551
1592
|
continue;
|
|
1552
1593
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1594
|
+
atoms.push(RUN_ATOM);
|
|
1595
|
+
}
|
|
1596
|
+
return atoms;
|
|
1597
|
+
}
|
|
1598
|
+
const RUN_ATOM = ".*";
|
|
1599
|
+
function caseFoldedChar(ch) {
|
|
1600
|
+
const lower = ch.toLowerCase();
|
|
1601
|
+
const upper = ch.toUpperCase();
|
|
1602
|
+
return lower !== upper ? `[${escapeRegExp(lower)}${escapeRegExp(upper)}]` : escapeRegExp(ch);
|
|
1603
|
+
}
|
|
1604
|
+
function closureReaches(atoms, target) {
|
|
1605
|
+
let re = "";
|
|
1606
|
+
for (let i = atoms.length - 1; i >= 0; i--)
|
|
1607
|
+
re = `(?:${atoms[i]}${re})?`;
|
|
1608
|
+
return new RegExp(`^${re}$`).test(target);
|
|
1609
|
+
}
|
|
1610
|
+
function scanBracketBody(body) {
|
|
1611
|
+
const members = [];
|
|
1612
|
+
let computed = false;
|
|
1613
|
+
for (let i = 0; i < body.length; i++) {
|
|
1614
|
+
const c = body[i];
|
|
1615
|
+
if (c === "\\") {
|
|
1616
|
+
if (i + 1 < body.length)
|
|
1617
|
+
members.push({ ch: body[++i], unquoted: false });
|
|
1558
1618
|
continue;
|
|
1559
1619
|
}
|
|
1560
|
-
if (
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1620
|
+
if (c === "$" || c === "`" || c === "{")
|
|
1621
|
+
computed = true;
|
|
1622
|
+
members.push({ ch: c, unquoted: true });
|
|
1623
|
+
}
|
|
1624
|
+
return { members, computed };
|
|
1625
|
+
}
|
|
1626
|
+
function membersOpenClassExpression(members) {
|
|
1627
|
+
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 === "."));
|
|
1628
|
+
}
|
|
1629
|
+
function translateBracket(body) {
|
|
1630
|
+
if (body.includes('"') || body.includes("'"))
|
|
1631
|
+
return undefined;
|
|
1632
|
+
const { members, computed } = scanBracketBody(body);
|
|
1633
|
+
if (computed || membersOpenClassExpression(members))
|
|
1634
|
+
return undefined;
|
|
1635
|
+
let i = 0;
|
|
1636
|
+
let negated = false;
|
|
1637
|
+
if (members[0]?.unquoted === true && (members[0].ch === "!" || members[0].ch === "^")) {
|
|
1638
|
+
negated = true;
|
|
1639
|
+
i = 1;
|
|
1640
|
+
}
|
|
1641
|
+
const first = i;
|
|
1642
|
+
let cls = "";
|
|
1643
|
+
for (; i < members.length; i++) {
|
|
1644
|
+
const { ch, unquoted } = members[i];
|
|
1645
|
+
if (ch === "-" && unquoted && i > first && i < members.length - 1) {
|
|
1646
|
+
if (!negated)
|
|
1647
|
+
return undefined;
|
|
1648
|
+
cls += "-";
|
|
1649
|
+
continue;
|
|
1570
1650
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1651
|
+
const escaped = ch.replace(/[\\\]\[^-]/g, "\\$&");
|
|
1652
|
+
cls += !negated && ch.toLowerCase() !== ch.toUpperCase() ? `${escapeRegExp(ch.toLowerCase())}${escapeRegExp(ch.toUpperCase())}` : escaped;
|
|
1573
1653
|
}
|
|
1574
|
-
return
|
|
1654
|
+
return `[${negated ? "^" : ""}${cls || "\\]"}]`;
|
|
1655
|
+
}
|
|
1656
|
+
const literalWord = (w) => (w !== undefined && w.pieces.every((p) => p.literal !== undefined && p.expansion === undefined) ? w.pieces.map((p) => p.literal).join("") : undefined);
|
|
1657
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1658
|
+
function printfNamesComputed(words) {
|
|
1659
|
+
for (let i = 0; i < words.length; i++) {
|
|
1660
|
+
const head = literalWord(words[i]);
|
|
1661
|
+
if (head === undefined || programBasename(head) !== "printf")
|
|
1662
|
+
continue;
|
|
1663
|
+
for (let j = i + 1; j < words.length; j++) {
|
|
1664
|
+
const lit = literalWord(words[j]);
|
|
1665
|
+
const opt = lit ?? words[j].pieces[0]?.literal ?? "";
|
|
1666
|
+
if (lit === "--" || !opt.startsWith("-v"))
|
|
1667
|
+
break;
|
|
1668
|
+
if (lit === undefined)
|
|
1669
|
+
return true;
|
|
1670
|
+
const name = lit === "-v" ? words[++j] : undefined;
|
|
1671
|
+
if (lit === "-v" && name === undefined)
|
|
1672
|
+
break;
|
|
1673
|
+
if (!IDENTIFIER.test(lit === "-v" ? (literalWord(name) ?? "") : lit.slice(2)))
|
|
1674
|
+
return true;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return false;
|
|
1575
1678
|
}
|
|
1576
1679
|
function segmentCompoundForReadonly(command, homeDir) {
|
|
1577
|
-
const trimmed =
|
|
1680
|
+
const trimmed = command.trim();
|
|
1578
1681
|
if (!trimmed)
|
|
1579
1682
|
return { reject: "empty command" };
|
|
1580
|
-
const
|
|
1581
|
-
|
|
1582
|
-
? redirectionStripped.slice(0, -1)
|
|
1583
|
-
: redirectionStripped;
|
|
1584
|
-
return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep" });
|
|
1683
|
+
const terminatorStripped = trimmed.endsWith(";") ? trimmed.slice(0, -1) : trimmed;
|
|
1684
|
+
return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep", redirection: "data-free", ...(homeDir !== undefined ? { homeDir } : {}) });
|
|
1585
1685
|
}
|
|
1586
1686
|
export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
1587
1687
|
const split = segmentCompoundForReadonly(command, boundary?.homeDir);
|
|
1588
1688
|
if ("reject" in split)
|
|
1589
1689
|
return { reason: split.reject };
|
|
1590
1690
|
const { segments, pipeFed } = split;
|
|
1691
|
+
const homeRebound = boundary?.homeDir !== undefined && homeMayBeRebound(command);
|
|
1591
1692
|
let nameRefusal;
|
|
1592
1693
|
const unlistedSegments = new Set();
|
|
1593
1694
|
const reheaded = new Map();
|
|
1594
1695
|
for (let si = 0; si < segments.length; si++) {
|
|
1595
|
-
const
|
|
1696
|
+
const words = argvOf(split.lexed[si]);
|
|
1697
|
+
const toks = segmentSpellings(words, boundary?.homeDir, homeRebound);
|
|
1596
1698
|
const folded = toks.folded;
|
|
1597
|
-
const pos = readProgramPosition(
|
|
1699
|
+
const pos = readProgramPosition(words);
|
|
1598
1700
|
const from = pos.written ?? folded.length;
|
|
1599
1701
|
const written = folded[from];
|
|
1600
1702
|
if (written === undefined)
|
|
1601
1703
|
return { reason: "empty command" };
|
|
1602
1704
|
if (written === "coproc")
|
|
1603
1705
|
return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command" };
|
|
1604
|
-
if (isBareControlWord(
|
|
1706
|
+
if (isBareControlWord(words, from, SHELL_BLOCK_WORDS)) {
|
|
1605
1707
|
unlistedSegments.add(si);
|
|
1606
1708
|
nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
|
|
1607
1709
|
continue;
|
|
@@ -1682,13 +1784,16 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
1682
1784
|
if (unlistedSegments.has(si))
|
|
1683
1785
|
continue;
|
|
1684
1786
|
const rehead = reheaded.get(si);
|
|
1685
|
-
const
|
|
1787
|
+
const words = argvOf(split.lexed[si]);
|
|
1788
|
+
const spelled = segmentSpellings(words, boundary?.homeDir, homeRebound);
|
|
1789
|
+
const segmentTokens = rehead === undefined ? spelled : reheadSegment(spelled, rehead.head, rehead.from);
|
|
1790
|
+
const argWords = words.slice((rehead?.from ?? 0) + 1);
|
|
1686
1791
|
const toks = segmentTokens.folded;
|
|
1687
1792
|
if (toks.length === 0)
|
|
1688
1793
|
continue;
|
|
1689
1794
|
foldedSegments.push(segmentTokens);
|
|
1690
1795
|
const name = toks[0];
|
|
1691
|
-
const guardReason = guardedVerbFormReason(name, toks,
|
|
1796
|
+
const guardReason = guardedVerbFormReason(name, toks, argWords);
|
|
1692
1797
|
if (guardReason !== undefined) {
|
|
1693
1798
|
shapeRefusal ??= guardReason;
|
|
1694
1799
|
continue;
|
|
@@ -1776,31 +1881,53 @@ export const POLL_LOOP_MAX_BEATS = 120;
|
|
|
1776
1881
|
export const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
|
|
1777
1882
|
const POLL_BODY_HARD_REJECT = /[<>$()`|&{}\n\r\\]/;
|
|
1778
1883
|
const NON_ASCII_WHITESPACE = /[^\S \t]/;
|
|
1779
|
-
const
|
|
1780
|
-
function
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1884
|
+
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)";
|
|
1885
|
+
function pollLoopShapeOf(lexed) {
|
|
1886
|
+
const segments = [...lexed.segments];
|
|
1887
|
+
const last = segments[segments.length - 1];
|
|
1888
|
+
if (last !== undefined && last.tokens.length === 0 && last.connector === ";" && segments.length > 1)
|
|
1889
|
+
segments.pop();
|
|
1890
|
+
if (segments.length < 3)
|
|
1891
|
+
return undefined;
|
|
1892
|
+
const bare = (w, text) => w !== undefined && w.raw === w.folded && w.folded === text;
|
|
1893
|
+
const onlyWords = (s) => s.tokens.every((t) => t.kind === "word");
|
|
1894
|
+
const head = segments[0];
|
|
1895
|
+
if (!onlyWords(head))
|
|
1896
|
+
return undefined;
|
|
1897
|
+
const hw = argvOf(head);
|
|
1898
|
+
if (!bare(hw[0], "for") || !bare(hw[2], "in") || hw.length < 4)
|
|
1899
|
+
return undefined;
|
|
1900
|
+
const name = hw[1];
|
|
1901
|
+
if (name.raw !== name.folded || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name.folded))
|
|
1902
|
+
return undefined;
|
|
1903
|
+
const done = segments[segments.length - 1];
|
|
1904
|
+
const dw = argvOf(done);
|
|
1905
|
+
if (done.connector !== ";" || !onlyWords(done) || dw.length !== 1 || !bare(dw[0], "done"))
|
|
1906
|
+
return undefined;
|
|
1907
|
+
const first = segments[1];
|
|
1908
|
+
if (first.connector !== ";")
|
|
1909
|
+
return undefined;
|
|
1910
|
+
const fw = argvOf(first);
|
|
1911
|
+
if (!bare(fw[0], "do") || fw.length < 2)
|
|
1912
|
+
return undefined;
|
|
1913
|
+
const body = [{ words: fw.slice(1), segment: first }];
|
|
1914
|
+
for (const s of segments.slice(2, -1))
|
|
1915
|
+
body.push({ words: argvOf(s), segment: s });
|
|
1916
|
+
return { varName: name.folded, iterable: hw.slice(3), body };
|
|
1789
1917
|
}
|
|
1790
1918
|
function pollLoopBeatsOf(iterable) {
|
|
1791
|
-
const
|
|
1792
|
-
const
|
|
1919
|
+
const one = iterable.length === 1 && iterable[0].pieces.length === 1 ? iterable[0].pieces[0].expansion : undefined;
|
|
1920
|
+
const seq = one?.kind === "command" ? /^seq[ \t]+(\d{1,7})[ \t]+(\d{1,7})$/.exec(one.body ?? "") : null;
|
|
1921
|
+
const brace = seq === null && one?.kind === "brace" ? /^(\d{1,7})\.\.(\d{1,7})$/.exec(one.body ?? "") : null;
|
|
1793
1922
|
const range = seq ?? brace;
|
|
1794
1923
|
if (range !== null) {
|
|
1795
1924
|
const lo = Number(range[1]);
|
|
1796
1925
|
const hi = Number(range[2]);
|
|
1797
1926
|
return hi >= lo ? hi - lo + 1 : undefined;
|
|
1798
1927
|
}
|
|
1799
|
-
|
|
1800
|
-
return items.every((t) => /^[A-Za-z0-9_.-]{1,64}$/.test(t)) ? items.length : undefined;
|
|
1928
|
+
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
1929
|
}
|
|
1802
|
-
function pollLoopSleepReason(
|
|
1803
|
-
const toks = segment.split(/[ \t]+/);
|
|
1930
|
+
function pollLoopSleepReason(toks) {
|
|
1804
1931
|
if (toks.length !== 2)
|
|
1805
1932
|
return "`sleep` in a poll loop must take exactly one literal numeric argument";
|
|
1806
1933
|
const v = toks[1];
|
|
@@ -1816,42 +1943,47 @@ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary
|
|
|
1816
1943
|
if (NON_ASCII_WHITESPACE.test(command)) {
|
|
1817
1944
|
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
1945
|
}
|
|
1819
|
-
const trimmed =
|
|
1946
|
+
const trimmed = command.trim();
|
|
1820
1947
|
if (!trimmed)
|
|
1821
1948
|
return { reason: "empty command" };
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1949
|
+
const lexed = lexShell(trimmed);
|
|
1950
|
+
if ("parseFailure" in lexed) {
|
|
1951
|
+
const r = lexed.parseFailure.reason;
|
|
1952
|
+
if (r === "unterminated_single_quote" || r === "unterminated_double_quote" || r === "unterminated_ansi_c") {
|
|
1953
|
+
return { reason: "unbalanced quote — the command's structure cannot be read reliably, so it is not auto-allowed" };
|
|
1954
|
+
}
|
|
1955
|
+
return { reason: NOT_A_POLL_LOOP };
|
|
1828
1956
|
}
|
|
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` };
|
|
1957
|
+
const shape = pollLoopShapeOf(lexed);
|
|
1958
|
+
if (shape === undefined)
|
|
1959
|
+
return { reason: NOT_A_POLL_LOOP };
|
|
1960
|
+
if (!/^[a-z]$/.test(shape.varName)) {
|
|
1961
|
+
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
1962
|
}
|
|
1835
|
-
const beats = pollLoopBeatsOf(iterable);
|
|
1963
|
+
const beats = pollLoopBeatsOf(shape.iterable);
|
|
1836
1964
|
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` };
|
|
1965
|
+
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
1966
|
}
|
|
1839
1967
|
if (beats > POLL_LOOP_MAX_BEATS) {
|
|
1840
1968
|
return { reason: `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow` };
|
|
1841
1969
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1970
|
+
const homeRebound = boundary?.homeDir !== undefined && homeMayBeRebound(command);
|
|
1971
|
+
const bodies = shape.body.map((b) => ({ ...b, spelled: segmentSpellings(b.words, boundary?.homeDir, homeRebound).raw }));
|
|
1972
|
+
for (const b of bodies) {
|
|
1973
|
+
if (b.segment.connector !== ";" || b.segment.tokens.some((t) => t.kind !== "word") || b.spelled.some((raw) => POLL_BODY_HARD_REJECT.test(raw))) {
|
|
1974
|
+
return { reason: "the loop body may not contain redirection, pipes, backgrounding, substitution, subshells, braces, escapes, or line breaks" };
|
|
1975
|
+
}
|
|
1844
1976
|
}
|
|
1845
1977
|
const readSegments = [];
|
|
1846
|
-
for (const
|
|
1847
|
-
|
|
1848
|
-
if (segment.length === 0)
|
|
1978
|
+
for (const b of bodies) {
|
|
1979
|
+
if (b.words.length === 0)
|
|
1849
1980
|
return { reason: "empty command in the loop body" };
|
|
1850
|
-
const
|
|
1981
|
+
const segment = b.spelled.join(" ");
|
|
1982
|
+
const parsed = parseLeadingCommandName(segment, { quotedOperatorsAreText: true });
|
|
1851
1983
|
if ("reject" in parsed)
|
|
1852
1984
|
return { reason: parsed.reject };
|
|
1853
1985
|
if (parsed.name === "sleep") {
|
|
1854
|
-
const sleepReason = pollLoopSleepReason(
|
|
1986
|
+
const sleepReason = pollLoopSleepReason(b.spelled);
|
|
1855
1987
|
if (sleepReason !== undefined)
|
|
1856
1988
|
return { reason: sleepReason };
|
|
1857
1989
|
continue;
|