@sema-agent/core 7.8.0 → 7.9.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 +75 -1
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/core/ask-origin.d.ts +16 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/checkpoint-store.d.ts +36 -35
- package/dist/core/effective-path-target.d.ts +43 -0
- package/dist/core/effective-path-target.js +56 -0
- package/dist/core/engine-notice.d.ts +8 -0
- package/dist/core/fs-write-gate-policy.js +2 -1
- package/dist/core/gate-lanes.d.ts +1 -0
- package/dist/core/gate-lanes.js +60 -28
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +66 -32
- package/dist/core/mcp-injection-drop.d.ts +74 -0
- package/dist/core/mcp-injection-drop.js +27 -0
- package/dist/core/permission-rule-consent.d.ts +29 -36
- package/dist/core/permission-rule-consent.js +110 -60
- package/dist/core/permission-rule-model.d.ts +311 -96
- package/dist/core/permission-rule-model.js +309 -50
- package/dist/core/permission-rule-org.d.ts +19 -12
- package/dist/core/permission-rule-org.js +13 -16
- package/dist/core/permission-rule-provider.d.ts +10 -6
- package/dist/core/permission-rule-provider.js +12 -8
- package/dist/core/permission-rule-session.d.ts +7 -6
- package/dist/core/permission-rule-session.js +29 -10
- package/dist/core/permission-rule-store.d.ts +40 -18
- package/dist/core/permission-rule-store.js +68 -42
- package/dist/core/permission-rule-sync.d.ts +9 -3
- package/dist/core/permission-rule-sync.js +29 -25
- package/dist/core/permission-rule-syntax.d.ts +30 -0
- package/dist/core/permission-rule-syntax.js +44 -0
- package/dist/core/permission-rules.d.ts +56 -33
- package/dist/core/permission-rules.js +88 -55
- package/dist/core/persisted-rule-arms.d.ts +61 -0
- package/dist/core/persisted-rule-arms.js +50 -0
- package/dist/core/runner/active-skill-scope.js +2 -1
- package/dist/core/runner/contracts.d.ts +21 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +42 -27
- package/dist/core/runner/permission-rule-lanes.js +25 -21
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-safety-scan.js +8 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-admission.d.ts +53 -0
- package/dist/core/runner/resume-admission.js +83 -0
- package/dist/core/runner/resume-apply.d.ts +50 -0
- package/dist/core/runner/resume-apply.js +184 -0
- package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
- package/dist/core/runner/resume-checkpoint-screen.js +108 -0
- package/dist/core/runner/resume-claim.d.ts +32 -0
- package/dist/core/runner/resume-claim.js +27 -0
- package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
- package/dist/core/runner/resume-internals-and-config.js +50 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
- package/dist/core/runner/resume-policy-outcome.js +127 -0
- package/dist/core/runner/resume-preflight.d.ts +40 -0
- package/dist/core/runner/resume-preflight.js +122 -0
- package/dist/core/runner/resume-review-outcome.d.ts +30 -0
- package/dist/core/runner/resume-review-outcome.js +88 -0
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +85 -734
- package/dist/core/runner/session-rule-policy.js +2 -1
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +5 -5
- package/dist/core/runner/tool-face-overlay.js +22 -3
- package/dist/core/runner/tool-output-projection.d.ts +9 -6
- package/dist/core/runner/tool-output-projection.js +1 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/sensitive-path-policy.js +5 -3
- package/dist/core/shell-lexer.d.ts +47 -0
- package/dist/core/shell-lexer.js +478 -0
- package/dist/core/shell-scan.d.ts +60 -0
- package/dist/core/shell-scan.js +183 -0
- package/dist/core/shell-wrapper-table.d.ts +297 -0
- package/dist/core/shell-wrapper-table.js +58 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/tool-catalog-entries.js +6 -6
- package/dist/core/tool-face.d.ts +80 -4
- package/dist/core/tool-face.js +10 -0
- package/dist/core/tool-policy.d.ts +1 -6
- package/dist/core/tool-registry.d.ts +8 -11
- package/dist/core/tool-registry.js +5 -2
- package/dist/core/tool-roster.d.ts +20 -2
- package/dist/core/tool-roster.js +36 -8
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/stores/file/checkpoint-store.js +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +28 -23
- package/dist/stores/file/permission-rule-store.js +64 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -2
- package/dist/tools/fs/fs-search-tools.js +17 -9
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/search.d.ts +0 -8
- package/dist/tools/fs/search.js +0 -23
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +99 -11
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { evaluatesOperand, SHELL_WRAPPER_TABLE } from "./shell-wrapper-table.js";
|
|
2
|
+
import { arithmeticHidesRun, carriesSubstitution, freshWord, matchOp, readDoubleQuoted, readExpansion, skipBalanced, skipHeredocBodies } from "./shell-scan.js";
|
|
3
|
+
export { SHELL_WRAPPER_TABLE } from "./shell-wrapper-table.js";
|
|
4
|
+
const WRAPPERS = new Map(Object.entries(SHELL_WRAPPER_TABLE));
|
|
5
|
+
const PREFIX_KEYWORDS = new Set(["if", "then", "else", "elif", "while", "until", "do", "!"]);
|
|
6
|
+
const CLOSING_KEYWORDS = new Set(["fi", "done", "esac", "}", "{", "]]"]);
|
|
7
|
+
const OPAQUE_KEYWORDS = new Set(["for", "select", "case", "function", "coproc", "in"]);
|
|
8
|
+
const ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?\+?=/;
|
|
9
|
+
const DURATION_RE = /^\d+(?:\.\d+)?[smhd]?$/;
|
|
10
|
+
const REDIRECT_OPS = ["<<<", "<<-", "<<", "<>", "<&", "<", ">>", ">|", ">&", "&>>", "&>", ">"];
|
|
11
|
+
const CONNECTOR_OPS = ["&&", "||", "|&", "|", ";;&", ";&", ";;", ";", "&"];
|
|
12
|
+
const ALL_OPS = [...CONNECTOR_OPS, ...REDIRECT_OPS];
|
|
13
|
+
export const MAX_SHELL_READ_CHARS = 10_000;
|
|
14
|
+
export function readShellCommand(source) {
|
|
15
|
+
if (source.length > MAX_SHELL_READ_CHARS)
|
|
16
|
+
return { segments: [{ argv: [], peeled: [], unreadable: `the command is longer than ${MAX_SHELL_READ_CHARS} characters — not read` }], connectors: [], grouped: false };
|
|
17
|
+
const segments = [];
|
|
18
|
+
const connectors = [];
|
|
19
|
+
let grouped = false;
|
|
20
|
+
let seg = { words: [] };
|
|
21
|
+
let word = freshWord();
|
|
22
|
+
let pendingRedirect;
|
|
23
|
+
let heredocs = [];
|
|
24
|
+
let badDelimiter = false;
|
|
25
|
+
let afterBoundary = true;
|
|
26
|
+
let lastConnector;
|
|
27
|
+
let tail;
|
|
28
|
+
const n = source.length;
|
|
29
|
+
let i = 0;
|
|
30
|
+
const endWord = () => {
|
|
31
|
+
if (!word.started)
|
|
32
|
+
return;
|
|
33
|
+
if (word.braceExpands)
|
|
34
|
+
word.expands = "many";
|
|
35
|
+
if (word.substitution)
|
|
36
|
+
seg.substitution = true;
|
|
37
|
+
if (pendingRedirect !== undefined) {
|
|
38
|
+
if (pendingRedirect === "<<" || pendingRedirect === "<<-") {
|
|
39
|
+
if (word.expands !== false)
|
|
40
|
+
badDelimiter = true;
|
|
41
|
+
heredocs.push({ delimiter: word.text, quoted: word.raw !== word.text, dash: pendingRedirect === "<<-" });
|
|
42
|
+
}
|
|
43
|
+
pendingRedirect = undefined;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
seg.words.push(word);
|
|
47
|
+
}
|
|
48
|
+
word = freshWord();
|
|
49
|
+
};
|
|
50
|
+
const finishSegment = (connector) => {
|
|
51
|
+
endWord();
|
|
52
|
+
if (pendingRedirect !== undefined) {
|
|
53
|
+
seg.unreadable ??= `the redirection \`${pendingRedirect}\` has no operand — a syntax error`;
|
|
54
|
+
pendingRedirect = undefined;
|
|
55
|
+
}
|
|
56
|
+
let built = buildSegment(seg);
|
|
57
|
+
if (built === "keyworded")
|
|
58
|
+
grouped = true;
|
|
59
|
+
if (built === "empty" || built === "keyworded") {
|
|
60
|
+
const continuation = connector === "\n" && lastConnector !== undefined && lastConnector !== ";" && lastConnector !== "&" && lastConnector !== "\n";
|
|
61
|
+
const legal = built === "keyworded" || afterBoundary || connector === undefined || connector === "\n" || lastConnector === "\n";
|
|
62
|
+
if (!legal)
|
|
63
|
+
built = { argv: [], peeled: [], unreadable: `an empty command before \`${connector}\` — a syntax error` };
|
|
64
|
+
else {
|
|
65
|
+
if (connector !== undefined && connector !== "\n" && segments.length > 0)
|
|
66
|
+
grouped = true;
|
|
67
|
+
if (connector !== undefined && !continuation)
|
|
68
|
+
lastConnector = connector;
|
|
69
|
+
seg = { words: [] };
|
|
70
|
+
afterBoundary = false;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (segments.length > 0)
|
|
75
|
+
connectors.push(lastConnector ?? "\n");
|
|
76
|
+
segments.push(built);
|
|
77
|
+
if (connector !== undefined)
|
|
78
|
+
lastConnector = connector;
|
|
79
|
+
seg = { words: [] };
|
|
80
|
+
afterBoundary = false;
|
|
81
|
+
};
|
|
82
|
+
const boundary = () => { finishSegment(undefined); grouped = true; afterBoundary = true; lastConnector = undefined; };
|
|
83
|
+
while (i < n && tail === undefined) {
|
|
84
|
+
const ch = source[i];
|
|
85
|
+
if (ch === " " || ch === "\t") {
|
|
86
|
+
endWord();
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === "\n") {
|
|
91
|
+
endWord();
|
|
92
|
+
if (badDelimiter) {
|
|
93
|
+
tail = "a here-document delimiter spelled with an expansion";
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (heredocs.length === 0) {
|
|
97
|
+
finishSegment("\n");
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const skipped = skipHeredocBodies(source, i + 1, heredocs);
|
|
102
|
+
heredocs = [];
|
|
103
|
+
if (skipped === undefined) {
|
|
104
|
+
tail = "a here-document body never reaches its delimiter";
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (skipped.substitution)
|
|
108
|
+
seg.substitution = true;
|
|
109
|
+
finishSegment("\n");
|
|
110
|
+
i = skipped.end;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (ch === "#" && !word.started) {
|
|
114
|
+
while (i < n && source[i] !== "\n")
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (ch === "\\") {
|
|
119
|
+
if (i + 1 >= n) {
|
|
120
|
+
tail = "a trailing backslash — the line never ends";
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
if (source[i + 1] === "\n") {
|
|
124
|
+
i += 2;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
word.started = true;
|
|
128
|
+
word.text += source[i + 1];
|
|
129
|
+
word.raw += source.slice(i, i + 2);
|
|
130
|
+
i += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === "'") {
|
|
134
|
+
const close = source.indexOf("'", i + 1);
|
|
135
|
+
if (close < 0) {
|
|
136
|
+
tail = "an unterminated single quote";
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
word.started = true;
|
|
140
|
+
word.text += source.slice(i + 1, close);
|
|
141
|
+
word.raw += source.slice(i, close + 1);
|
|
142
|
+
i = close + 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
let dq = i + 1;
|
|
146
|
+
if (ch === "$")
|
|
147
|
+
while (source[dq] === "\\" && source[dq + 1] === "\n")
|
|
148
|
+
dq += 2;
|
|
149
|
+
if (ch === '"' || (ch === "$" && source[dq] === '"')) {
|
|
150
|
+
const open = ch === "$" ? dq + 1 : i + 1;
|
|
151
|
+
const end = readDoubleQuoted(source, open, word);
|
|
152
|
+
if (end === undefined) {
|
|
153
|
+
tail = "an unterminated double quote";
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
word.raw += source.slice(i, end);
|
|
157
|
+
i = end;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === "$" || ch === "`" || ((ch === "<" || ch === ">") && source[i + 1] === "(")) {
|
|
161
|
+
const end = readExpansion(source, i, word);
|
|
162
|
+
if (end === undefined) {
|
|
163
|
+
tail = "an unterminated substitution";
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (end === i + 1) {
|
|
167
|
+
word.started = true;
|
|
168
|
+
word.text += ch;
|
|
169
|
+
word.raw += ch;
|
|
170
|
+
i = end;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
word.started = true;
|
|
174
|
+
word.expands = ch === "$" || ch === "`" ? "many" : word.expands || "one";
|
|
175
|
+
word.raw += source.slice(i, end);
|
|
176
|
+
i = end;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (ch === "~" && !word.started)
|
|
180
|
+
word.expands = "one";
|
|
181
|
+
const op = matchOp(source, i, ALL_OPS);
|
|
182
|
+
if (op !== undefined) {
|
|
183
|
+
if (CONNECTOR_OPS.includes(op)) {
|
|
184
|
+
if (op === ";;" || op === ";&" || op === ";;&") {
|
|
185
|
+
endWord();
|
|
186
|
+
seg.unreadable ??= "a `case` arm terminator outside a `case` this lane reads";
|
|
187
|
+
i += op.length;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
finishSegment(op);
|
|
191
|
+
i += op.length;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (word.started && word.raw === word.text && (/^\d+$/.test(word.text) || /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(word.text)))
|
|
195
|
+
word = freshWord();
|
|
196
|
+
else
|
|
197
|
+
endWord();
|
|
198
|
+
if (pendingRedirect !== undefined)
|
|
199
|
+
seg.unreadable ??= `the redirection \`${pendingRedirect}\` has no operand — a syntax error`;
|
|
200
|
+
pendingRedirect = op;
|
|
201
|
+
i += op.length;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (ch === "(" && word.started && "@?*+!".includes(word.raw.slice(-1)) && word.raw !== "!") {
|
|
205
|
+
const end = skipBalanced(source, i, "(", ")");
|
|
206
|
+
if (end === undefined) {
|
|
207
|
+
tail = "an unterminated extended pattern (`@(…)`)";
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (carriesSubstitution(source.slice(i + 1, end - 1)))
|
|
211
|
+
word.substitution = true;
|
|
212
|
+
word.expands = "many";
|
|
213
|
+
word.text += source.slice(i, end);
|
|
214
|
+
word.raw += source.slice(i, end);
|
|
215
|
+
i = end;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (ch === "(" && source[i + 1] === "(" && (!word.started || word.raw === "!") && pendingRedirect === undefined && seg.words.every((w) => w.expands === false && (PREFIX_KEYWORDS.has(w.text) || CLOSING_KEYWORDS.has(w.text) || w.text === "time" || w.text === "-p"))) {
|
|
219
|
+
endWord();
|
|
220
|
+
const end = skipBalanced(source, i, "(", ")");
|
|
221
|
+
if (end === undefined) {
|
|
222
|
+
tail = "an unterminated arithmetic command";
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (arithmeticHidesRun(source.slice(i + 2, end - 1)))
|
|
226
|
+
seg.substitution = true;
|
|
227
|
+
i = end;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (ch === "(" || ch === ")") {
|
|
231
|
+
if (ch === "(" && word.started && !word.expands && seg.words.length === 0 && pendingRedirect === undefined) {
|
|
232
|
+
let j = i + 1;
|
|
233
|
+
while (j < n && (source[j] === " " || source[j] === "\t"))
|
|
234
|
+
j++;
|
|
235
|
+
if (source[j] === ")") {
|
|
236
|
+
word = freshWord();
|
|
237
|
+
boundary();
|
|
238
|
+
i = j + 1;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
boundary();
|
|
243
|
+
i++;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (ch === "*" || ch === "?" || (ch === "]" && word.bracketOpen))
|
|
247
|
+
word.expands = "many";
|
|
248
|
+
if (ch === "[")
|
|
249
|
+
word.bracketOpen = true;
|
|
250
|
+
if (ch === "{")
|
|
251
|
+
word.braceOpen = true;
|
|
252
|
+
if (word.braceOpen && (ch === "," || (ch === "." && source[i + 1] === ".")))
|
|
253
|
+
word.braceExpands = true;
|
|
254
|
+
if (ch === "}" && !word.braceExpands)
|
|
255
|
+
word.braceOpen = false;
|
|
256
|
+
word.started = true;
|
|
257
|
+
word.text += ch;
|
|
258
|
+
word.raw += ch;
|
|
259
|
+
i++;
|
|
260
|
+
}
|
|
261
|
+
if (tail !== undefined) {
|
|
262
|
+
endWord();
|
|
263
|
+
seg.unreadable ??= tail;
|
|
264
|
+
finishSegment(undefined);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
if (heredocs.length > 0)
|
|
268
|
+
seg.unreadable ??= "a here-document with no body";
|
|
269
|
+
const dangling = seg.words.length === 0 && !word.started && lastConnector !== undefined && lastConnector !== ";" && lastConnector !== "&" && lastConnector !== "\n";
|
|
270
|
+
if (dangling)
|
|
271
|
+
seg.unreadable ??= `the connector \`${lastConnector}\` has no command after it — a syntax error`;
|
|
272
|
+
finishSegment(undefined);
|
|
273
|
+
}
|
|
274
|
+
return { segments, connectors, grouped };
|
|
275
|
+
}
|
|
276
|
+
const publicWord = ({ text, raw, expands }) => ({ text, raw, expands });
|
|
277
|
+
const SUBSTITUTION_UNREAD = "a command substitution runs a program this reader does not read";
|
|
278
|
+
const MAX_WRAPPER_LAYERS = 8;
|
|
279
|
+
export function isFullyReadable(shape) {
|
|
280
|
+
return shape.segments.every((s) => s.unreadable === undefined && s.peelUnreadable === undefined && [s.argv, ...s.peeled].every((argv) => argv[0]?.expands === false));
|
|
281
|
+
}
|
|
282
|
+
function buildSegment(seg) {
|
|
283
|
+
if (seg.unreadable !== undefined)
|
|
284
|
+
return { argv: seg.words.map(publicWord), peeled: [], unreadable: seg.unreadable };
|
|
285
|
+
const carried = () => (seg.substitution === true ? { argv: [], peeled: [], peelUnreadable: SUBSTITUTION_UNREAD } : keyworded ? "keyworded" : "empty");
|
|
286
|
+
let keyworded = false;
|
|
287
|
+
let k0 = 0;
|
|
288
|
+
while (k0 < seg.words.length && seg.words[k0].expands === false && (PREFIX_KEYWORDS.has(seg.words[k0].text) || CLOSING_KEYWORDS.has(seg.words[k0].text))) {
|
|
289
|
+
k0++;
|
|
290
|
+
keyworded = true;
|
|
291
|
+
}
|
|
292
|
+
const words = k0 === 0 ? seg.words : seg.words.slice(k0);
|
|
293
|
+
const head = words[0];
|
|
294
|
+
if (head === undefined)
|
|
295
|
+
return carried();
|
|
296
|
+
if (evaluatesOperand(words))
|
|
297
|
+
seg.substitution = true;
|
|
298
|
+
if (head.expands === false && (head.text === "[[" || head.text === "["))
|
|
299
|
+
return carried();
|
|
300
|
+
if (head.expands === false && OPAQUE_KEYWORDS.has(head.text))
|
|
301
|
+
return { argv: words.map(publicWord), peeled: [], unreadable: `a \`${head.text}\` control structure — its commands need a syntax tree` };
|
|
302
|
+
const argv = words.map(publicWord);
|
|
303
|
+
const peeled = [];
|
|
304
|
+
let peelUnreadable;
|
|
305
|
+
let k = 0;
|
|
306
|
+
while (k < argv.length && ASSIGNMENT_RE.test(argv[k].raw))
|
|
307
|
+
k++;
|
|
308
|
+
let run = argv.slice(k);
|
|
309
|
+
if (k > 0 && run.length > 0)
|
|
310
|
+
peeled.push(run);
|
|
311
|
+
for (let layer = 0; run.length > 0; layer++) {
|
|
312
|
+
let j = 0;
|
|
313
|
+
while (j < run.length && ((run[j].text === "!" && run[j].expands === false) || ASSIGNMENT_RE.test(run[j].raw)))
|
|
314
|
+
j++;
|
|
315
|
+
if (j > 0) {
|
|
316
|
+
run = run.slice(j);
|
|
317
|
+
if (run.length === 0)
|
|
318
|
+
break;
|
|
319
|
+
peeled.push(run);
|
|
320
|
+
}
|
|
321
|
+
if (run[0].expands !== false) {
|
|
322
|
+
peelUnreadable = "the program name is an expansion";
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
if (layer >= MAX_WRAPPER_LAYERS) {
|
|
326
|
+
peelUnreadable = `more than ${MAX_WRAPPER_LAYERS} wrapper layers`;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
const next = unwrap(run);
|
|
330
|
+
if (typeof next === "string") {
|
|
331
|
+
peelUnreadable = next;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
if (next === run)
|
|
335
|
+
break;
|
|
336
|
+
run = next;
|
|
337
|
+
peeled.push(run);
|
|
338
|
+
}
|
|
339
|
+
if (seg.substitution === true || [argv, ...peeled].some((c) => c[0]?.expands === false && evaluatesOperand(c)))
|
|
340
|
+
peelUnreadable ??= SUBSTITUTION_UNREAD;
|
|
341
|
+
return { argv, peeled, ...(peelUnreadable !== undefined ? { peelUnreadable } : {}) };
|
|
342
|
+
}
|
|
343
|
+
function unwrap(words) {
|
|
344
|
+
const head = words[0];
|
|
345
|
+
const name = head.text.slice(head.text.lastIndexOf("/") + 1);
|
|
346
|
+
const row = WRAPPERS.get(name);
|
|
347
|
+
if (row === undefined)
|
|
348
|
+
return words;
|
|
349
|
+
if (row.viaShell === true)
|
|
350
|
+
return `\`${name}\` evaluates its operands as shell input this reader does not read`;
|
|
351
|
+
if (row.executes !== undefined) {
|
|
352
|
+
const hit = words.slice(1).find((w) => w.expands !== false || row.executes.includes(w.text));
|
|
353
|
+
return hit === undefined ? words : hit.expands !== false ? `an expansion where \`${name}\` reads its arguments` : `\`${name} ${hit.text}\` runs a command this reader does not read`;
|
|
354
|
+
}
|
|
355
|
+
const valueFlags = new Set(row.valueFlags);
|
|
356
|
+
const flags = new Set(row.flags);
|
|
357
|
+
const attached = new Set(row.attached ?? []);
|
|
358
|
+
const commandString = new Set(row.commandString);
|
|
359
|
+
let positionalTaken = row.positional === undefined;
|
|
360
|
+
let durationTaken = row.duration !== true;
|
|
361
|
+
let replaceToken;
|
|
362
|
+
let i = 1;
|
|
363
|
+
const unknown = (t) => `the \`${name}\` option ${t} is not one this reader knows`;
|
|
364
|
+
const takeValue = (flag) => {
|
|
365
|
+
const value = words[i + 1];
|
|
366
|
+
if (name === "xargs" && flag === "-I" && value !== undefined) {
|
|
367
|
+
replaceToken = value.text;
|
|
368
|
+
if (value.expands !== false)
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
i += value === undefined ? 1 : 2;
|
|
372
|
+
return value === undefined || value.expands !== "many";
|
|
373
|
+
};
|
|
374
|
+
const operand = (w) => {
|
|
375
|
+
if (w.expands !== false)
|
|
376
|
+
return `an expansion where \`${name}\` reads its operands`;
|
|
377
|
+
if (!durationTaken) {
|
|
378
|
+
durationTaken = true;
|
|
379
|
+
return DURATION_RE.test(w.text) ? true : `the \`${name}\` duration "${w.text}" cannot be read`;
|
|
380
|
+
}
|
|
381
|
+
if (!positionalTaken) {
|
|
382
|
+
positionalTaken = true;
|
|
383
|
+
return row.positional.test(w.text) ? true : `the \`${name}\` operand "${w.text}" cannot be read`;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
};
|
|
387
|
+
while (i < words.length) {
|
|
388
|
+
const w = words[i];
|
|
389
|
+
if (w.expands !== false)
|
|
390
|
+
return `an expansion where \`${name}\` reads its options`;
|
|
391
|
+
const t = w.text;
|
|
392
|
+
if (t === "--") {
|
|
393
|
+
i++;
|
|
394
|
+
while (i < words.length && (!durationTaken || !positionalTaken)) {
|
|
395
|
+
const r = operand(words[i]);
|
|
396
|
+
if (r !== true)
|
|
397
|
+
return r;
|
|
398
|
+
i++;
|
|
399
|
+
}
|
|
400
|
+
while (row.assignments === true && i < words.length && words[i].expands === false && ASSIGNMENT_RE.test(words[i].text))
|
|
401
|
+
i++;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
if (t === "-" && row.bareDash === true) {
|
|
405
|
+
i++;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (name === "nice" && /^-\d+$/.test(t)) {
|
|
409
|
+
i++;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (t.startsWith("-") && t !== "-") {
|
|
413
|
+
if (name === "command" && /^-[pvV]+$/.test(t) && /[vV]/.test(t))
|
|
414
|
+
return words;
|
|
415
|
+
if (t.startsWith("--")) {
|
|
416
|
+
const eq = t.indexOf("=");
|
|
417
|
+
const flag = eq > 0 ? t.slice(0, eq) : t;
|
|
418
|
+
if (commandString.has(flag))
|
|
419
|
+
return `\`${name} ${flag}\` takes its command as one string`;
|
|
420
|
+
if (eq < 0 && valueFlags.has(flag)) {
|
|
421
|
+
if (!takeValue(flag))
|
|
422
|
+
return `an expansion where \`${name}\` reads its options`;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (!flags.has(flag) && !(eq > 0 && valueFlags.has(flag)))
|
|
426
|
+
return unknown(flag);
|
|
427
|
+
if (name === "xargs" && flag === "--replace")
|
|
428
|
+
replaceToken = eq > 0 ? t.slice(eq + 1) : "{}";
|
|
429
|
+
i++;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
let k = 1;
|
|
433
|
+
for (; k < t.length; k++) {
|
|
434
|
+
const letter = `-${t[k]}`;
|
|
435
|
+
if (commandString.has(letter))
|
|
436
|
+
return `\`${name} ${letter}\` takes its command as one string`;
|
|
437
|
+
if (valueFlags.has(letter)) {
|
|
438
|
+
if (name === "xargs" && letter === "-I" && k < t.length - 1)
|
|
439
|
+
replaceToken = t.slice(k + 1);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
if (attached.has(letter)) {
|
|
443
|
+
if (name === "xargs" && letter === "-i")
|
|
444
|
+
replaceToken = t.length > k + 1 ? t.slice(k + 1) : "{}";
|
|
445
|
+
k = t.length;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
if (!flags.has(letter))
|
|
449
|
+
return unknown(letter);
|
|
450
|
+
}
|
|
451
|
+
if (k === t.length - 1) {
|
|
452
|
+
if (!takeValue(`-${t[k]}`))
|
|
453
|
+
return `an expansion where \`${name}\` reads its options`;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
i++;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (row.assignments === true && ASSIGNMENT_RE.test(t)) {
|
|
460
|
+
i++;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const r = operand(w);
|
|
464
|
+
if (r === false)
|
|
465
|
+
break;
|
|
466
|
+
if (r !== true)
|
|
467
|
+
return r;
|
|
468
|
+
i++;
|
|
469
|
+
}
|
|
470
|
+
if (i >= words.length)
|
|
471
|
+
return words;
|
|
472
|
+
const rest = words.slice(i);
|
|
473
|
+
if (name !== "xargs")
|
|
474
|
+
return rest;
|
|
475
|
+
if (replaceToken === undefined)
|
|
476
|
+
return [...rest, { text: "", raw: "<xargs input>", expands: "many" }];
|
|
477
|
+
return rest.map((w) => (w.text.includes(replaceToken) || w.raw.includes(replaceToken) ? { ...w, expands: "many" } : w));
|
|
478
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The character-level scanners under the tightening reader's shell lexer (`shell-lexer.ts`): a double-
|
|
3
|
+
* quoted body, an expansion (`$…`, a backtick, a process substitution), a balanced `(…)` / `{…}` region,
|
|
4
|
+
* and the here-document bodies. Every scanner answers an INDEX (past what it read) or `undefined` for
|
|
5
|
+
* "never closes", and reports through `word.substitution` / the heredoc result whether it passed a
|
|
6
|
+
* COMMAND SUBSTITUTION — `$(…)`, a backtick, `<(…)`/`>(…)`, anywhere, including inside `${…}` and
|
|
7
|
+
* `$((…))` — because a substitution runs a program the lexer does not read: the lexer marks the segment
|
|
8
|
+
* and the reader answers `unreadable` for every rule the segment does not otherwise reach. Reading the
|
|
9
|
+
* commands INSIDE a substitution was tried and retired: every bash quirk of substitution boundaries
|
|
10
|
+
* (a `)` in a comment, a multi-line body in a heredoc, a `$"…"` string) became a way to hide a run, and
|
|
11
|
+
* the fail-closed word costs an approval prompt, never a bypass. Split from the lexer so the lexer's own
|
|
12
|
+
* ceiling holds the segment/peel logic and this one holds the byte walking.
|
|
13
|
+
*/
|
|
14
|
+
/** A word under construction; `braceOpen`/`braceExpands` track an unquoted `{a,b}` / `{1..3}` pattern, `bracketOpen` a `[…]` class;
|
|
15
|
+
* `substitution` is set when the word carries a command/process substitution anywhere. */
|
|
16
|
+
export interface Word {
|
|
17
|
+
text: string;
|
|
18
|
+
raw: string;
|
|
19
|
+
expands: false | "one" | "many";
|
|
20
|
+
started: boolean;
|
|
21
|
+
braceOpen: boolean;
|
|
22
|
+
braceExpands: boolean;
|
|
23
|
+
bracketOpen: boolean;
|
|
24
|
+
substitution: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface Heredoc {
|
|
27
|
+
delimiter: string;
|
|
28
|
+
quoted: boolean;
|
|
29
|
+
dash: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Is `expr`, the interior of an arithmetic expansion or command, provably free of a hidden run? Only digits,
|
|
32
|
+
* operators and spaces are: a NAME or a `$…` evaluates recursively (`a='arr[$(…)]'; $((a))`), and anything
|
|
33
|
+
* else (`;`, a quote, a bracket) is not arithmetic at all — bash then re-reads `((…))` as nested subshells. */
|
|
34
|
+
export declare const arithmeticHidesRun: (expr: string) => boolean;
|
|
35
|
+
/** Does an expansion interior, an extended-glob interior or a heredoc line carry a run this reader does not
|
|
36
|
+
* read — a substitution opener, or a subscript that evaluates indirectly? Backslash-newline is removed
|
|
37
|
+
* first, as bash removes it before it reads the opener (`$\<newline>(` is `$(`). */
|
|
38
|
+
export declare const carriesSubstitution: (text: string) => boolean;
|
|
39
|
+
export declare const freshWord: () => Word;
|
|
40
|
+
/** Read a `"…"` body into `word` from `i` (just past the opening quote); returns the index past the closing quote. */
|
|
41
|
+
export declare function readDoubleQuoted(s: string, i: number, word: Word): number | undefined;
|
|
42
|
+
/** The index past the expansion starting at `i` (`$…`, a backtick, or a `<(`/`>(` process substitution);
|
|
43
|
+
* `i + 1` when the `$` is literal; `undefined` when it never closes. Sets `word.substitution` when the
|
|
44
|
+
* region is, or contains, a command/process substitution. A `$"…"` string is NOT read here (the lexer
|
|
45
|
+
* reads it as a double-quoted string, which is what it is); inside double quotes `$'` is a literal `$`
|
|
46
|
+
* (ANSI-C quoting exists only outside them). */
|
|
47
|
+
export declare function readExpansion(s: string, i: number, word: Word, inDoubleQuotes?: boolean): number | undefined;
|
|
48
|
+
/** Past the matching `close` for the `open` at `i`, quote-aware (a `)` inside `$(echo ")")` is text).
|
|
49
|
+
* LINEAR: one pass with a small state stack (nesting depth × quote state), no re-entry. */
|
|
50
|
+
export declare function skipBalanced(s: string, i: number, open: string, close: string): number | undefined;
|
|
51
|
+
/** Skip the here-document bodies that begin at `i` (the start of a line), one per delimiter in order,
|
|
52
|
+
* answering the index past them and whether an UNQUOTED body carries a substitution (bash expands such a
|
|
53
|
+
* body: `$(…)` in it runs). An unquoted body joins a line ending in an ODD run of backslashes with the
|
|
54
|
+
* next before the delimiter is compared; `<<-` strips leading tabs. */
|
|
55
|
+
export declare function skipHeredocBodies(s: string, i: number, docs: readonly Heredoc[]): {
|
|
56
|
+
end: number;
|
|
57
|
+
substitution: boolean;
|
|
58
|
+
} | undefined;
|
|
59
|
+
/** The LONGEST operator of `ops` at `i` (`>&` before `>`, `&&` before `&`). */
|
|
60
|
+
export declare function matchOp(s: string, i: number, ops: readonly string[]): string | undefined;
|