@sema-agent/core 7.17.7 → 7.19.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 +233 -13
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/agents/teacher.js +1 -0
- package/dist/agents/verify.js +3 -0
- package/dist/brain/anthropic.js +6 -3
- package/dist/brain/open-responses.js +10 -4
- package/dist/brain/openai.js +5 -2
- package/dist/brain/reasoning.d.ts +13 -13
- package/dist/brain/reasoning.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +18 -0
- package/dist/brain/route-adjudicator.js +16 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/brain/stream-shared.d.ts +3 -0
- package/dist/brain/stream-shared.js +1 -0
- package/dist/core/agent-definition.d.ts +10 -0
- package/dist/core/artifact-host.d.ts +41 -4
- package/dist/core/artifact-host.js +18 -2
- package/dist/core/ask-origin.d.ts +16 -0
- package/dist/core/ask-origin.js +8 -0
- package/dist/core/auto-mode-arming.d.ts +6 -0
- package/dist/core/auto-mode-arming.js +5 -5
- package/dist/core/auto-mode-defaults.d.ts +43 -15
- package/dist/core/auto-mode-defaults.js +4 -1
- package/dist/core/auto-mode.d.ts +184 -59
- package/dist/core/auto-mode.js +188 -13
- package/dist/core/checkpoint-store.d.ts +23 -2
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/engine-notice.d.ts +77 -1
- package/dist/core/engine-notice.js +7 -0
- package/dist/core/gate-fold.js +5 -1
- package/dist/core/gate-lanes.js +9 -8
- package/dist/core/gate-outcome.d.ts +42 -2
- package/dist/core/gate-outcome.js +23 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hook-seats.d.ts +66 -0
- package/dist/core/hook-seats.js +37 -0
- package/dist/core/hooks.d.ts +1 -1
- package/dist/core/read-root-candidate.d.ts +12 -0
- package/dist/core/read-root-candidate.js +24 -0
- package/dist/core/roles.d.ts +9 -1
- package/dist/core/roles.js +4 -0
- package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
- package/dist/core/runner/assemble-result.d.ts +2 -1
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/classifier-ladder.d.ts +227 -0
- package/dist/core/runner/classifier-ladder.js +214 -0
- package/dist/core/runner/contracts.d.ts +71 -13
- package/dist/core/runner/decide-continuation.d.ts +1 -3
- package/dist/core/runner/decide-continuation.js +1 -7
- package/dist/core/runner/deferral-census.d.ts +52 -0
- package/dist/core/runner/deferral-census.js +44 -0
- package/dist/core/runner/denial-limit-arms.js +2 -2
- package/dist/core/runner/derived-route-fallback.d.ts +8 -4
- package/dist/core/runner/derived-route-fallback.js +8 -3
- package/dist/core/runner/gate-exit.d.ts +12 -0
- package/dist/core/runner/gate-exit.js +6 -3
- package/dist/core/runner/prepare-artifact.d.ts +10 -2
- package/dist/core/runner/prepare-artifact.js +5 -4
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
- package/dist/core/runner/prepare-config-doors.d.ts +7 -1
- package/dist/core/runner/prepare-config-doors.js +13 -13
- package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
- package/dist/core/runner/prepare-defer-classify.js +8 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
- package/dist/core/runner/prepare-hands-readface.js +60 -24
- package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
- package/dist/core/runner/prepare-inherited-gate.js +10 -2
- package/dist/core/runner/prepare-lsp.d.ts +2 -2
- package/dist/core/runner/prepare-lsp.js +1 -1
- package/dist/core/runner/prepare-policy-chain.js +2 -0
- package/dist/core/runner/prepare-project-context.d.ts +5 -1
- package/dist/core/runner/prepare-project-context.js +7 -4
- package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
- package/dist/core/runner/prepare-prompt-inputs.js +4 -1
- package/dist/core/runner/prepare-run-refs.d.ts +9 -1
- package/dist/core/runner/prepare-run-refs.js +3 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
- package/dist/core/runner/prepare-suspend-saga.js +5 -2
- package/dist/core/runner/prepare-task.js +19 -14
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
- package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
- package/dist/core/runner/prepare-turn-wiring.js +3 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
- package/dist/core/runner/prepare-wiring-manifest.js +5 -2
- package/dist/core/runner/resume-apply.js +3 -3
- package/dist/core/runner/resume-checkpoint-screen.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +4 -1
- package/dist/core/runner/run-identity-wiring.js +6 -2
- package/dist/core/runner/run-leg.d.ts +2 -0
- package/dist/core/runner/run-leg.js +45 -45
- package/dist/core/runner/run-notification-lane.js +55 -27
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +16 -13
- package/dist/core/runner/run-turn-boundary.js +15 -5
- package/dist/core/runner/runtask.d.ts +6 -5
- package/dist/core/runner/runtask.js +26 -14
- package/dist/core/runner/skill-base-dir.d.ts +54 -0
- package/dist/core/runner/skill-base-dir.js +42 -0
- package/dist/core/runner/synthetic-tools.d.ts +83 -48
- package/dist/core/runner/synthetic-tools.js +69 -32
- package/dist/core/runner/tool-defer-gate.d.ts +22 -9
- package/dist/core/runner/tool-defer-gate.js +15 -5
- package/dist/core/runner/tool-disclosure.d.ts +5 -0
- package/dist/core/runner/tool-disclosure.js +2 -0
- package/dist/core/runner/tool-progress-lane.d.ts +42 -0
- package/dist/core/runner/tool-progress-lane.js +61 -0
- package/dist/core/runner/turn-attachments.d.ts +43 -12
- package/dist/core/runner/turn-attachments.js +58 -14
- package/dist/core/runner-deps.d.ts +48 -7
- package/dist/core/sensitive-path-policy.d.ts +58 -17
- package/dist/core/sensitive-path-policy.js +33 -5
- package/dist/core/skill-listing-budget.d.ts +112 -0
- package/dist/core/skill-listing-budget.js +58 -0
- package/dist/core/swappable-deps.d.ts +9 -7
- package/dist/core/swappable-deps.js +7 -3
- package/dist/core/task-event.d.ts +96 -29
- package/dist/core/task-notification.d.ts +142 -16
- package/dist/core/task-notification.js +106 -0
- package/dist/core/task-result.d.ts +9 -1
- package/dist/core/task-spec.d.ts +46 -3
- package/dist/core/tighten-task-spec.js +7 -5
- package/dist/core/tool-model-gate.d.ts +1 -1
- package/dist/core/tool-policy.d.ts +19 -4
- package/dist/core/tool-policy.js +6 -7
- package/dist/core/tool-search-setting.d.ts +78 -0
- package/dist/core/tool-search-setting.js +90 -0
- package/dist/core/tool-spec.d.ts +45 -4
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +46 -0
- package/dist/core/wiring-manifest.js +9 -1
- package/dist/core/write-protect.d.ts +8 -3
- package/dist/core/write-protect.js +8 -5
- package/dist/engine/execution-env/node-execution-env.js +10 -4
- package/dist/engine/harness/agent-harness.d.ts +42 -0
- package/dist/engine/harness/agent-harness.js +61 -29
- package/dist/engine/loop/agent-loop.d.ts +68 -3
- package/dist/engine/loop/agent-loop.js +109 -16
- package/dist/index.d.ts +13 -8
- package/dist/index.js +11 -7
- package/dist/internal/harness.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +3 -0
- package/dist/orchestration/run-workflow-tool.js +6 -1
- package/dist/orchestration/workflow-governance.d.ts +7 -1
- package/dist/orchestration/workflow-governance.js +9 -5
- package/dist/orchestration/workflow-primitives.d.ts +3 -0
- package/dist/orchestration/workflow-primitives.js +6 -1
- package/dist/orchestration/workflow-types.d.ts +48 -13
- package/dist/orchestration/workflow-types.js +1 -1
- package/dist/orchestration/workflow.js +45 -17
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/section-usage.d.ts +26 -0
- package/dist/prompt-assembly/section-usage.js +10 -0
- package/dist/prompt-assembly/types.d.ts +8 -6
- package/dist/prompts/default.d.ts +13 -0
- package/dist/prompts/default.js +16 -8
- package/dist/tools/artifact/artifact-text.d.ts +10 -1
- package/dist/tools/artifact/artifact-text.js +6 -3
- package/dist/tools/artifact/artifact-tool.d.ts +12 -2
- package/dist/tools/artifact/artifact-tool.js +203 -70
- package/dist/tools/artifact/conformance.d.ts +56 -0
- package/dist/tools/artifact/conformance.js +349 -0
- package/dist/tools/artifact/local-stub.js +1 -1
- package/dist/tools/artifact/reject-breaker.d.ts +28 -0
- package/dist/tools/artifact/reject-breaker.js +42 -0
- package/dist/tools/fs/bash-lexer.d.ts +132 -0
- package/dist/tools/fs/bash-lexer.js +563 -0
- package/dist/tools/fs/bash-program-position.d.ts +142 -0
- package/dist/tools/fs/bash-program-position.js +125 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
- package/dist/tools/fs/bash-readonly-classifier.js +180 -558
- package/dist/tools/fs/containment-face.d.ts +89 -0
- package/dist/tools/fs/containment-face.js +81 -0
- package/dist/tools/fs/fs-bash.d.ts +46 -1
- package/dist/tools/fs/fs-bash.js +165 -25
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -3
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +24 -5
- package/dist/tools/fs/index.js +18 -6
- package/dist/tools/fs/read-deny.d.ts +4 -3
- package/dist/tools/fs/repo-map.d.ts +2 -2
- package/dist/tools/fs/safety.d.ts +18 -3
- package/dist/tools/fs/safety.js +11 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +99 -13
- package/wire-changes/core@7.17.7.json +49 -0
- package/wire-changes/core@7.18.0.json +1273 -0
- package/wire-changes/core@7.19.0.json +457 -0
- package/dist/tools/fs/read-face.d.ts +0 -68
- package/dist/tools/fs/read-face.js +0 -47
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
|
|
2
|
-
|
|
1
|
+
import { hasWin32AliasedComponent, isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
|
|
2
|
+
import { lexShellText, literalTildeSpelling, quoteMask, substitutionsInHeredocBody, SUBSTITUTION_PLACEHOLDER, wordsOf, } from "./bash-lexer.js";
|
|
3
|
+
import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
|
|
4
|
+
export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
|
|
3
5
|
export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
4
6
|
"ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
|
|
5
7
|
"grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
|
|
@@ -8,24 +10,6 @@ export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
|
8
10
|
];
|
|
9
11
|
export const BASH_CLASSIFY_DEFAULT_ALLOW = [...BASH_READONLY_DEFAULT_ALLOW, "find", "sed", "cd"];
|
|
10
12
|
const SHELL_OPERATORS = /[;&|<>$()`\n\r\\]/;
|
|
11
|
-
function quoteMask(s) {
|
|
12
|
-
const quoted = new Array(s.length).fill(false);
|
|
13
|
-
let open;
|
|
14
|
-
for (let i = 0; i < s.length; i++) {
|
|
15
|
-
const ch = s[i];
|
|
16
|
-
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
17
|
-
open = ch;
|
|
18
|
-
quoted[i] = true;
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (open !== undefined) {
|
|
22
|
-
quoted[i] = true;
|
|
23
|
-
if (open === ch)
|
|
24
|
-
open = undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return { quoted, balanced: open === undefined };
|
|
28
|
-
}
|
|
29
13
|
function hasBareShellOperator(s, quotedOperatorsAreText) {
|
|
30
14
|
if (!quotedOperatorsAreText)
|
|
31
15
|
return SHELL_OPERATORS.test(s);
|
|
@@ -108,48 +92,11 @@ export function splitShellCompoundSegments(source, options) {
|
|
|
108
92
|
return { reject: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
|
|
109
93
|
}
|
|
110
94
|
const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
for (let i = 0; i < scanned.length; i++) {
|
|
117
|
-
const c = scanned[i];
|
|
118
|
-
if (quoted(i)) {
|
|
119
|
-
cur += c;
|
|
120
|
-
}
|
|
121
|
-
else if (c === "&") {
|
|
122
|
-
if (scanned[i + 1] === "&") {
|
|
123
|
-
segments.push(cur);
|
|
124
|
-
cur = "";
|
|
125
|
-
connectors.push("&&");
|
|
126
|
-
i++;
|
|
127
|
-
}
|
|
128
|
-
else
|
|
129
|
-
return { reject: "`&` backgrounding is not allowed — a backgrounded process outlives the command" };
|
|
130
|
-
}
|
|
131
|
-
else if (c === "|") {
|
|
132
|
-
if (scanned[i + 1] === "|") {
|
|
133
|
-
segments.push(cur);
|
|
134
|
-
cur = "";
|
|
135
|
-
connectors.push("||");
|
|
136
|
-
i++;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
segments.push(cur);
|
|
140
|
-
cur = "";
|
|
141
|
-
connectors.push("|");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
else if (c === ";") {
|
|
145
|
-
segments.push(cur);
|
|
146
|
-
cur = "";
|
|
147
|
-
connectors.push(";");
|
|
148
|
-
}
|
|
149
|
-
else
|
|
150
|
-
cur += c;
|
|
151
|
-
}
|
|
152
|
-
segments.push(cur);
|
|
95
|
+
const lexed = lexShellText(scanned, { mode: "strict" });
|
|
96
|
+
if ("reject" in lexed)
|
|
97
|
+
return lexed;
|
|
98
|
+
const segments = lexed.segments.map((s) => s.text);
|
|
99
|
+
const connectors = lexed.segments.map((s) => s.connector);
|
|
153
100
|
return { segments, pipeFed: connectors.map((k) => k === "|"), connectors };
|
|
154
101
|
}
|
|
155
102
|
const EXEMPT_REDIRECTION = /^(?:2>\/dev\/null|[12]?>&[12])$/;
|
|
@@ -181,24 +128,6 @@ function stripExemptRedirections(command) {
|
|
|
181
128
|
flush();
|
|
182
129
|
return out;
|
|
183
130
|
}
|
|
184
|
-
function foldQuoteRemovalToken(tok) {
|
|
185
|
-
if (!tok.includes('"') && !tok.includes("'"))
|
|
186
|
-
return tok;
|
|
187
|
-
let out = "";
|
|
188
|
-
let open;
|
|
189
|
-
for (const ch of tok) {
|
|
190
|
-
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
191
|
-
open = ch;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (open === ch) {
|
|
195
|
-
open = undefined;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
out += ch;
|
|
199
|
-
}
|
|
200
|
-
return out;
|
|
201
|
-
}
|
|
202
131
|
const POLL_LOOP_CAUSE_MAX_PATHS = 3;
|
|
203
132
|
export function formatOutOfRootReadApprovalOption(directory) {
|
|
204
133
|
const trimmed = directory.replace(/[/\\]+$/, "");
|
|
@@ -206,6 +135,49 @@ export function formatOutOfRootReadApprovalOption(directory) {
|
|
|
206
135
|
const leaf = cut >= 0 ? trimmed.slice(cut + 1) : trimmed;
|
|
207
136
|
return `Yes, allow reading from ${leaf || directory}/ from this project`;
|
|
208
137
|
}
|
|
138
|
+
function parentDirLexically(p) {
|
|
139
|
+
return normalizeAbsPathLexically(`${p}/..`);
|
|
140
|
+
}
|
|
141
|
+
export function deepestCommonDir(a, b) {
|
|
142
|
+
if (a === b)
|
|
143
|
+
return a;
|
|
144
|
+
const shorter = Math.min(a.length, b.length);
|
|
145
|
+
let end = 0;
|
|
146
|
+
let i = 0;
|
|
147
|
+
while (i < shorter && a[i] === b[i]) {
|
|
148
|
+
if (a[i] === "/" || a[i] === "\\")
|
|
149
|
+
end = i;
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
if (i === shorter && (a[shorter] === "/" || a[shorter] === "\\" || b[shorter] === "/" || b[shorter] === "\\"))
|
|
153
|
+
end = shorter;
|
|
154
|
+
const cut = a.slice(0, end);
|
|
155
|
+
const candidate = cut.includes("/") || cut.includes("\\") ? cut : a.slice(0, end + 1);
|
|
156
|
+
if (candidate.length === 0 || !withinAnyRoot([candidate], a) || !withinAnyRoot([candidate], b))
|
|
157
|
+
return undefined;
|
|
158
|
+
return candidate;
|
|
159
|
+
}
|
|
160
|
+
function outOfRootGrantCandidateOf(paths, boundary) {
|
|
161
|
+
if (paths.length === 0)
|
|
162
|
+
return undefined;
|
|
163
|
+
if (paths.some(hasWin32AliasedComponent))
|
|
164
|
+
return undefined;
|
|
165
|
+
let candidate = parentDirLexically(paths[0]);
|
|
166
|
+
for (const path of paths.slice(1)) {
|
|
167
|
+
if (candidate === undefined)
|
|
168
|
+
return undefined;
|
|
169
|
+
candidate = deepestCommonDir(candidate, parentDirLexically(path));
|
|
170
|
+
}
|
|
171
|
+
if (candidate === undefined)
|
|
172
|
+
return undefined;
|
|
173
|
+
if (parentDirLexically(candidate) === candidate)
|
|
174
|
+
return undefined;
|
|
175
|
+
if (boundary.homeDir !== undefined && withinAnyRoot([candidate], boundary.homeDir))
|
|
176
|
+
return undefined;
|
|
177
|
+
if (boundary.roots.some((root) => withinAnyRoot([candidate], root)))
|
|
178
|
+
return undefined;
|
|
179
|
+
return candidate;
|
|
180
|
+
}
|
|
209
181
|
const NO_PATH_OPERAND_COMMANDS = new Set([
|
|
210
182
|
"pwd", "echo", "whoami", "uname", "which", "tr", "basename", "dirname",
|
|
211
183
|
"cal", "uptime", "id", "groups", "nproc", "locale", "free", "true", "false", "seq", "expr", "type", "sleep",
|
|
@@ -245,36 +217,25 @@ function spellingsOf(absolute) {
|
|
|
245
217
|
function reheadSegment(toks, head, from = 0) {
|
|
246
218
|
return { folded: [head, ...toks.folded.slice(from + 1)], raw: [head, ...toks.raw.slice(from + 1)] };
|
|
247
219
|
}
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
const word = toks.folded[i];
|
|
252
|
-
return word !== undefined && toks.raw[i] === word && words.has(word);
|
|
253
|
-
}
|
|
254
|
-
function tokensMoveBase(toks, allow) {
|
|
255
|
-
let from = 0;
|
|
256
|
-
while (from < toks.folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES)) {
|
|
257
|
-
const controlWord = programBasename(toks.folded[from] ?? "");
|
|
258
|
-
from++;
|
|
259
|
-
for (let span = launcherOptionSpan(controlWord, toks.folded, from); span > 0; span = launcherOptionSpan(controlWord, toks.folded, from))
|
|
260
|
-
from += span;
|
|
261
|
-
}
|
|
262
|
-
const head = toks.folded[from];
|
|
263
|
-
if (head === undefined)
|
|
220
|
+
function tokensMoveBase(toks) {
|
|
221
|
+
const pos = readProgramPosition(toks);
|
|
222
|
+
if (pos.written === undefined)
|
|
264
223
|
return false;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (head.startsWith("-") && head !== "-")
|
|
224
|
+
const written = toks.folded[pos.written];
|
|
225
|
+
if (toks.raw[pos.written] !== written)
|
|
268
226
|
return true;
|
|
269
|
-
if (
|
|
270
|
-
return true;
|
|
271
|
-
if (!COMMAND_LAUNCHERS.has(head))
|
|
227
|
+
if (pos.assignments.length > 0)
|
|
272
228
|
return false;
|
|
273
|
-
|
|
274
|
-
|
|
229
|
+
if (pos.launchers.length === 0) {
|
|
230
|
+
if (written.startsWith("-") && written !== "-")
|
|
231
|
+
return true;
|
|
232
|
+
return written === "cd";
|
|
233
|
+
}
|
|
234
|
+
if (!COMMAND_LAUNCHERS.has(written))
|
|
235
|
+
return false;
|
|
236
|
+
return pos.refusal !== undefined || pos.program?.basename === "cd";
|
|
275
237
|
}
|
|
276
238
|
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
277
|
-
const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
|
|
278
239
|
export function refusedCommandMayRead(command, allow, boundary) {
|
|
279
240
|
return !refusedCommandExempt(command, allow, boundary, 0);
|
|
280
241
|
}
|
|
@@ -282,249 +243,49 @@ const STDIN_ONLY_READER_FLAGS = new Map([
|
|
|
282
243
|
["cat", /^-[AbeEnstTuv]+$/],
|
|
283
244
|
["wc", /^-[clmwL]+$/],
|
|
284
245
|
]);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
else
|
|
302
|
-
body.push(line);
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
out.push(line);
|
|
306
|
-
const header = heredocHeaderOf(line);
|
|
307
|
-
if (header === null)
|
|
308
|
-
return undefined;
|
|
309
|
-
if (header !== undefined) {
|
|
310
|
-
terminator = header.word;
|
|
311
|
-
expanded = !header.quoted;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return terminator === undefined ? { outer: out.join("\n"), expandedBodies } : undefined;
|
|
315
|
-
}
|
|
316
|
-
function heredocHeaderOf(line) {
|
|
317
|
-
let inSingle = false;
|
|
318
|
-
let inDouble = false;
|
|
319
|
-
for (let i = 0; i < line.length; i++) {
|
|
320
|
-
const ch = line[i];
|
|
321
|
-
if (ch === "\\" && !inSingle) {
|
|
322
|
-
i++;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (ch === "'" && !inDouble) {
|
|
326
|
-
inSingle = !inSingle;
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
if (ch === '"' && !inSingle) {
|
|
330
|
-
inDouble = !inDouble;
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (inSingle || inDouble)
|
|
334
|
-
continue;
|
|
335
|
-
if (ch === "#")
|
|
336
|
-
return undefined;
|
|
337
|
-
if (ch === "<" && line[i + 1] === "<") {
|
|
338
|
-
if (line[i + 2] === "<") {
|
|
339
|
-
i += 2;
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
const m = /^<<-?\s*([^\s;|&<>()]+)/.exec(line.slice(i));
|
|
343
|
-
if (m === null)
|
|
344
|
-
return null;
|
|
345
|
-
const word = m[1];
|
|
346
|
-
if (/[$`]/.test(word))
|
|
347
|
-
return null;
|
|
348
|
-
const delimiter = word.replace(/\\(.)/g, "$1").replace(/["']/g, "");
|
|
349
|
-
if (delimiter === "")
|
|
350
|
-
return null;
|
|
351
|
-
return { word: delimiter, quoted: /["'\\]/.test(word) };
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return undefined;
|
|
355
|
-
}
|
|
356
|
-
function splitSubstitutions(text, quotesAreLiteral = false) {
|
|
357
|
-
let outer = "";
|
|
358
|
-
const bodies = [];
|
|
359
|
-
let i = 0;
|
|
360
|
-
let inSingle = false;
|
|
361
|
-
let inDouble = false;
|
|
362
|
-
while (i < text.length) {
|
|
363
|
-
const ch = text[i];
|
|
364
|
-
if (ch === "\\" && !inSingle) {
|
|
365
|
-
outer += text.slice(i, i + 2);
|
|
366
|
-
i += 2;
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
if (ch === "'" && !inDouble && !quotesAreLiteral) {
|
|
370
|
-
inSingle = !inSingle;
|
|
371
|
-
outer += ch;
|
|
372
|
-
i++;
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (ch === '"' && !inSingle && !quotesAreLiteral) {
|
|
376
|
-
inDouble = !inDouble;
|
|
377
|
-
outer += ch;
|
|
378
|
-
i++;
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
if (inSingle) {
|
|
382
|
-
outer += ch;
|
|
383
|
-
i++;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (ch === "`") {
|
|
387
|
-
const close = text.indexOf("`", i + 1);
|
|
388
|
-
if (close < 0)
|
|
389
|
-
return undefined;
|
|
390
|
-
bodies.push(text.slice(i + 1, close));
|
|
391
|
-
outer += SUBSTITUTION_PLACEHOLDER;
|
|
392
|
-
i = close + 1;
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
if ((ch === "$" || ch === "<" || ch === ">") && text[i + 1] === "(") {
|
|
396
|
-
let depth = 0;
|
|
397
|
-
let j = i + 1;
|
|
398
|
-
let quoted;
|
|
399
|
-
for (; j < text.length; j++) {
|
|
400
|
-
const c = text[j];
|
|
401
|
-
if (quoted !== undefined) {
|
|
402
|
-
if (c === "\\" && quoted === '"')
|
|
403
|
-
j++;
|
|
404
|
-
else if (c === quoted)
|
|
405
|
-
quoted = undefined;
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (c === "'" || c === '"') {
|
|
409
|
-
quoted = c;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
if (c === "(")
|
|
413
|
-
depth++;
|
|
414
|
-
else if (c === ")") {
|
|
415
|
-
depth--;
|
|
416
|
-
if (depth === 0)
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (j >= text.length)
|
|
421
|
-
return undefined;
|
|
422
|
-
bodies.push(text.slice(i + 2, j));
|
|
423
|
-
outer += SUBSTITUTION_PLACEHOLDER;
|
|
424
|
-
i = j + 1;
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
outer += ch;
|
|
428
|
-
i++;
|
|
429
|
-
}
|
|
430
|
-
if (inSingle)
|
|
431
|
-
return undefined;
|
|
432
|
-
return { outer, bodies };
|
|
246
|
+
function whitelistSegment(segment) {
|
|
247
|
+
const tokens = [...segment.tokens];
|
|
248
|
+
for (let k = 0; k + 2 < tokens.length; k++) {
|
|
249
|
+
const [a, b, c] = [tokens[k], tokens[k + 1], tokens[k + 2]];
|
|
250
|
+
if (a.kind === "word" && b.kind === "group" && b.op === "(" && c.kind === "group" && c.op === ")" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(a.folded))
|
|
251
|
+
tokens.splice(k, 3);
|
|
252
|
+
}
|
|
253
|
+
const words = tokens.filter((t) => t.kind === "word" && !(t.raw === t.folded && (t.folded === "{" || t.folded === "}")));
|
|
254
|
+
let from = 0;
|
|
255
|
+
while (from < words.length && SHELL_BLOCK_WORDS.has(words[from].folded))
|
|
256
|
+
from++;
|
|
257
|
+
return {
|
|
258
|
+
words: { folded: words.slice(from).map((w) => w.folded), raw: words.slice(from).map((w) => w.raw) },
|
|
259
|
+
redirects: tokens.filter((t) => t.kind === "redirect"),
|
|
260
|
+
};
|
|
433
261
|
}
|
|
434
262
|
function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside = false) {
|
|
435
263
|
if (depth > 4)
|
|
436
264
|
return false;
|
|
437
|
-
const
|
|
438
|
-
if (
|
|
439
|
-
return false;
|
|
440
|
-
if (!whitelistReadsTheSameWords(command, stripped.outer))
|
|
441
|
-
return false;
|
|
442
|
-
const split = splitSubstitutions(stripped.outer);
|
|
443
|
-
if (split === undefined)
|
|
265
|
+
const lexed = lexShellText(command, { mode: "lenient" });
|
|
266
|
+
if ("reject" in lexed)
|
|
444
267
|
return false;
|
|
445
|
-
const bodies = [...
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
bodies.push(...inner.bodies);
|
|
451
|
-
}
|
|
452
|
-
const fold = (w) => w.replace(/\\(.)/g, "$1").replace(/["']/g, "");
|
|
453
|
-
const programOf = (w) => {
|
|
454
|
-
const inner = fold(w);
|
|
455
|
-
return inner.includes("/") ? inner.slice(inner.lastIndexOf("/") + 1) : inner;
|
|
456
|
-
};
|
|
457
|
-
const segments = split.outer.split(/\n|;|&&|\|\||\||(?<![>&\d])&(?![>&])/).map((segment) => {
|
|
458
|
-
const toks = [];
|
|
459
|
-
for (const word of segment.trim().split(/\s+/)) {
|
|
460
|
-
let rest = word;
|
|
461
|
-
while (rest !== "") {
|
|
462
|
-
const m = /^(\d*(?:<<<|<<|<|>>|>)(?:&\d*)?|&>>?)(.*)$/.exec(rest);
|
|
463
|
-
if (m !== null) {
|
|
464
|
-
toks.push(m[1].replace(/^\d+/, ""));
|
|
465
|
-
rest = m[2];
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
const at = rest.search(/\d*(?:<<<|<<|<|>>|>)|&>/);
|
|
469
|
-
if (at <= 0) {
|
|
470
|
-
toks.push(rest);
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
toks.push(rest.slice(0, at));
|
|
474
|
-
rest = rest.slice(at);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
const commentAt = toks.findIndex((t) => t.startsWith("#"));
|
|
478
|
-
return (commentAt >= 0 ? toks.slice(0, commentAt) : toks)
|
|
479
|
-
.map((t) => (/^[A-Za-z_][A-Za-z0-9_]*\(\)$/.test(t) ? "{" : t.replace(/^[({]+|[)}]+$/g, "")))
|
|
480
|
-
.filter((t) => t !== "");
|
|
481
|
-
});
|
|
482
|
-
const headIndex = (toks) => {
|
|
483
|
-
const folded = toks.map(fold);
|
|
484
|
-
let i = 0;
|
|
485
|
-
while (i < toks.length) {
|
|
486
|
-
const w = folded[i];
|
|
487
|
-
if (ASSIGNMENT_WORD.test(w) || SHELL_CONTROL_PREFIXES.has(w) || SHELL_BLOCK_WORDS.has(w) || /^[{}]$/.test(w)) {
|
|
488
|
-
i++;
|
|
489
|
-
const controlWord = programBasename(w);
|
|
490
|
-
for (let span = launcherOptionSpan(controlWord, folded, i); span > 0; span = launcherOptionSpan(controlWord, folded, i))
|
|
491
|
-
i += span;
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(w)) {
|
|
495
|
-
i += 2;
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
if (/^(?:>>?|<)&\d*$/.test(w)) {
|
|
499
|
-
i++;
|
|
268
|
+
const bodies = [...lexed.substitutions];
|
|
269
|
+
const segments = [];
|
|
270
|
+
for (const segment of lexed.segments) {
|
|
271
|
+
for (const t of segment.tokens) {
|
|
272
|
+
if (t.kind !== "heredoc" || t.quoted)
|
|
500
273
|
continue;
|
|
501
|
-
|
|
502
|
-
|
|
274
|
+
const inner = substitutionsInHeredocBody(t.body);
|
|
275
|
+
if (inner === undefined)
|
|
276
|
+
return false;
|
|
277
|
+
for (const body of inner)
|
|
278
|
+
bodies.push(body);
|
|
503
279
|
}
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
const runProgramOf = (toks, i) => {
|
|
507
|
-
let head = programOf(toks[i] ?? "");
|
|
508
|
-
let guard = 0;
|
|
509
|
-
while (COMMAND_LAUNCHERS.has(head) && guard++ < 8) {
|
|
510
|
-
let j = i + 1;
|
|
511
|
-
while (j < toks.length && (ASSIGNMENT_WORD.test(fold(toks[j])) || /^\d+[smhd]?$/.test(fold(toks[j]))))
|
|
512
|
-
j++;
|
|
513
|
-
const next = toks[j];
|
|
514
|
-
if (next === undefined || fold(next).startsWith("-"))
|
|
515
|
-
return undefined;
|
|
516
|
-
i = j;
|
|
517
|
-
head = programOf(next);
|
|
518
|
-
}
|
|
519
|
-
return head;
|
|
520
|
-
};
|
|
280
|
+
segments.push(whitelistSegment(segment));
|
|
281
|
+
}
|
|
521
282
|
const baseMoved = baseMovedOutside ||
|
|
522
|
-
segments.some((
|
|
523
|
-
const
|
|
524
|
-
if (
|
|
525
|
-
return
|
|
526
|
-
const
|
|
527
|
-
return
|
|
283
|
+
segments.some(({ words }) => {
|
|
284
|
+
const pos = readProgramPosition(words);
|
|
285
|
+
if (pos.refusal !== undefined)
|
|
286
|
+
return true;
|
|
287
|
+
const runs = pos.program?.basename;
|
|
288
|
+
return runs === "cd" || (runs !== undefined && UNMODELLED_BASE_MOVERS.has(runs));
|
|
528
289
|
});
|
|
529
290
|
for (const body of bodies) {
|
|
530
291
|
const walked = classifyCompoundReadonlyDetailed(body, allow, boundary);
|
|
@@ -535,33 +296,22 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
535
296
|
if (!refusedCommandExempt(body, allow, boundary, depth + 1, baseMoved))
|
|
536
297
|
return false;
|
|
537
298
|
}
|
|
538
|
-
const stdinFileJudgedInside = (
|
|
539
|
-
if (
|
|
299
|
+
const stdinFileJudgedInside = (word) => {
|
|
300
|
+
if (word === undefined || word === "" || word.includes(SUBSTITUTION_PLACEHOLDER) || UNREADABLE_WHITELIST_WORD.test(word))
|
|
540
301
|
return false;
|
|
541
302
|
if (boundary === undefined || baseMoved)
|
|
542
303
|
return false;
|
|
543
|
-
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0],
|
|
304
|
+
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], word, boundary.homeDir);
|
|
544
305
|
if (resolved === undefined || boundary.denyMatch?.(resolved) != null)
|
|
545
306
|
return false;
|
|
546
307
|
return withinAnyRoot(boundary.roots, resolved);
|
|
547
308
|
};
|
|
548
|
-
const unlistedSegmentJudgedInside = (
|
|
309
|
+
const unlistedSegmentJudgedInside = (words, from) => {
|
|
549
310
|
if (boundary === undefined)
|
|
550
311
|
return true;
|
|
551
312
|
let endOfOptions = false;
|
|
552
|
-
for (let j = from; j <
|
|
553
|
-
const
|
|
554
|
-
if (t === "<<<" || t === "<<" || t === "<") {
|
|
555
|
-
j++;
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
if (/^(?:>>?|&>>?)$/.test(t)) {
|
|
559
|
-
j++;
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
if (/^(?:>>?|<)&\d*$/.test(t))
|
|
563
|
-
continue;
|
|
564
|
-
const word = fold(t);
|
|
313
|
+
for (let j = from; j < words.folded.length; j++) {
|
|
314
|
+
const word = words.folded[j];
|
|
565
315
|
if (j > from && !endOfOptions && word === "--") {
|
|
566
316
|
endOfOptions = true;
|
|
567
317
|
continue;
|
|
@@ -580,47 +330,49 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
580
330
|
}
|
|
581
331
|
return true;
|
|
582
332
|
};
|
|
583
|
-
for (const
|
|
584
|
-
for (
|
|
585
|
-
if (
|
|
333
|
+
for (const { words, redirects } of segments) {
|
|
334
|
+
for (const r of redirects)
|
|
335
|
+
if (r.op === "<" && !stdinFileJudgedInside(r.target?.folded))
|
|
586
336
|
return false;
|
|
587
|
-
|
|
588
|
-
if (
|
|
589
|
-
continue;
|
|
590
|
-
const headWord = fold(toks[i]);
|
|
591
|
-
if (headWord.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(headWord))
|
|
337
|
+
const pos = readProgramPosition(words);
|
|
338
|
+
if (pos.refusal !== undefined)
|
|
592
339
|
return false;
|
|
593
|
-
|
|
340
|
+
for (const k of words.folded.keys()) {
|
|
341
|
+
const word = words.folded[k];
|
|
342
|
+
if (ASSIGNMENT_WORD.test(word) && /\$\(|`/.test(word) && (pos.assignments.includes(k) || pos.launchers.some((l) => l.index < k)))
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
const program = pos.program;
|
|
346
|
+
const listed = (name) => allow.has(name) || name === "cd";
|
|
347
|
+
if (program === undefined) {
|
|
348
|
+
if (pos.launchers.length === 0)
|
|
349
|
+
continue;
|
|
350
|
+
if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS))
|
|
351
|
+
return false;
|
|
352
|
+
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index))
|
|
353
|
+
return false;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (program.name.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(program.name))
|
|
594
357
|
return false;
|
|
595
|
-
|
|
596
|
-
const launcherUnwrap = COMMAND_LAUNCHERS.has(head) ? unwrapLauncher(toks.slice(i).map(fold), allow) : undefined;
|
|
597
|
-
if (launcherUnwrap?.kind === "refused")
|
|
358
|
+
if (argumentAsProgramRefusal(program.basename, program.basename, words.folded.slice(program.index + 1)) !== undefined)
|
|
598
359
|
return false;
|
|
599
|
-
|
|
600
|
-
if (runs.head !== undefined && runs.index !== undefined &&
|
|
601
|
-
argumentAsProgramRefusal(runs.head, runs.head, toks.slice(i + runs.index + 1).map(fold)) !== undefined)
|
|
360
|
+
if (SHELL_REENTRY_PROGRAM_RE.test(program.basename))
|
|
602
361
|
return false;
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
if (wrapped === undefined || /[$`]/.test(wrapped) || SHELL_REENTRY_PROGRAM_RE.test(wrapped))
|
|
362
|
+
if (pos.launchers.length > 0) {
|
|
363
|
+
if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS))
|
|
606
364
|
return false;
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
return false;
|
|
610
|
-
if (unwrapped.kind === "unlisted") {
|
|
611
|
-
if (!unlistedSegmentJudgedInside(toks, i))
|
|
365
|
+
if (!listed(program.basename)) {
|
|
366
|
+
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index))
|
|
612
367
|
return false;
|
|
613
368
|
continue;
|
|
614
369
|
}
|
|
615
|
-
head = unwrapped.head;
|
|
616
|
-
i += unwrapped.index;
|
|
617
370
|
}
|
|
618
|
-
|
|
619
|
-
return false;
|
|
371
|
+
const head = program.basename;
|
|
620
372
|
if (head === "cd" || UNMODELLED_BASE_MOVERS.has(head))
|
|
621
373
|
continue;
|
|
622
374
|
if (!allow.has(head)) {
|
|
623
|
-
if (!unlistedSegmentJudgedInside(
|
|
375
|
+
if (!unlistedSegmentJudgedInside(words, program.index))
|
|
624
376
|
return false;
|
|
625
377
|
continue;
|
|
626
378
|
}
|
|
@@ -629,110 +381,18 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
629
381
|
const flags = STDIN_ONLY_READER_FLAGS.get(head);
|
|
630
382
|
if (flags === undefined)
|
|
631
383
|
return false;
|
|
632
|
-
for (let j =
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
j++;
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
if (/^(?:>>?|&>>?)$/.test(t)) {
|
|
639
|
-
j++;
|
|
640
|
-
continue;
|
|
641
|
-
}
|
|
642
|
-
if (/^(?:>>?|<)&\d*$/.test(t))
|
|
643
|
-
continue;
|
|
644
|
-
if (flags.test(fold(t)))
|
|
645
|
-
continue;
|
|
646
|
-
return false;
|
|
384
|
+
for (let j = program.index + 1; j < words.folded.length; j++) {
|
|
385
|
+
if (!flags.test(words.folded[j]))
|
|
386
|
+
return false;
|
|
647
387
|
}
|
|
648
388
|
}
|
|
649
389
|
return true;
|
|
650
390
|
}
|
|
651
|
-
function unwrapLauncher(folded, allow) {
|
|
652
|
-
let argvFromStdin = ARGV_REENTRY_PROGRAM_RE.test(programBasename(folded[0] ?? ""));
|
|
653
|
-
let lastReentry = argvFromStdin ? 0 : -1;
|
|
654
|
-
for (let i = 1; i < folded.length; i++) {
|
|
655
|
-
const t = folded[i];
|
|
656
|
-
if (ASSIGNMENT_WORD.test(t) || /^\d+[smhd]?$/.test(t))
|
|
657
|
-
continue;
|
|
658
|
-
const wordBasename = programBasename(t);
|
|
659
|
-
if (ARGV_REENTRY_PROGRAM_RE.test(wordBasename)) {
|
|
660
|
-
argvFromStdin = true;
|
|
661
|
-
lastReentry = i;
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
if (COMMAND_LAUNCHERS.has(wordBasename))
|
|
665
|
-
continue;
|
|
666
|
-
if (t.startsWith("-"))
|
|
667
|
-
return { kind: "refused", reason: `"${folded[0]}" is given an option before the program it runs — which program runs is not readable without the launcher's option table ${NOT_AUTO_ALLOWED}` };
|
|
668
|
-
const head = wordBasename;
|
|
669
|
-
if (SHELL_CONTROL_PREFIXES.has(head) || SHELL_BLOCK_WORDS.has(head))
|
|
670
|
-
return { kind: "refused", reason: `"${folded[0]}" is given shell control syntax ("${head}") where the program it runs would be ${NOT_AUTO_ALLOWED}` };
|
|
671
|
-
return allow.has(head) || head === "cd" ? { kind: "reader", index: i, head, argvFromStdin } : { kind: "unlisted", head, index: i, argvFromStdin };
|
|
672
|
-
}
|
|
673
|
-
return { kind: "unlisted", argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1 };
|
|
674
|
-
}
|
|
675
|
-
const LAUNCHER_OPTION_GRAMMAR = new Map([
|
|
676
|
-
["time", {
|
|
677
|
-
plain: new Set(["-p", "-a", "-v", "-q", "--portability", "--append", "--verbose", "--quiet", "--help", "--version"]),
|
|
678
|
-
valued: new Set(["-f", "-o", "--format", "--output"]),
|
|
679
|
-
}],
|
|
680
|
-
]);
|
|
681
|
-
function launcherOptionSpan(launcher, folded, i) {
|
|
682
|
-
const row = launcher === undefined ? undefined : LAUNCHER_OPTION_GRAMMAR.get(launcher);
|
|
683
|
-
const tok = folded[i];
|
|
684
|
-
if (row === undefined || tok === undefined)
|
|
685
|
-
return 0;
|
|
686
|
-
if (row.plain.has(tok))
|
|
687
|
-
return 1;
|
|
688
|
-
return row.valued.has(tok) && folded[i + 1] !== undefined ? 2 : 0;
|
|
689
|
-
}
|
|
690
|
-
const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
|
|
691
|
-
function argvReentryUnjudged(unwrapped) {
|
|
692
|
-
if (!unwrapped.argvFromStdin)
|
|
693
|
-
return false;
|
|
694
|
-
const runs = unwrapped.head ?? (unwrapped.runsDefaultProgram === true ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
|
|
695
|
-
return runs === undefined || !NO_PATH_OPERAND_COMMANDS.has(runs);
|
|
696
|
-
}
|
|
697
391
|
function tokenizeSegment(segment) {
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
return word.startsWith("~") ? `./${word}` : word;
|
|
703
|
-
}
|
|
704
|
-
function tildeIsLiteral(raw) {
|
|
705
|
-
if (!raw.startsWith("~"))
|
|
706
|
-
return true;
|
|
707
|
-
for (let i = 1; i < raw.length; i++) {
|
|
708
|
-
const ch = raw[i];
|
|
709
|
-
if (ch === '"' || ch === "'")
|
|
710
|
-
return true;
|
|
711
|
-
if (ch === "/")
|
|
712
|
-
return false;
|
|
713
|
-
}
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
|
-
function splitWordsQuoteAware(segment) {
|
|
717
|
-
const s = segment.trim();
|
|
718
|
-
const mask = quoteMask(s);
|
|
719
|
-
if (!mask.balanced)
|
|
720
|
-
return s.split(/\s+/).filter((t) => t.length > 0);
|
|
721
|
-
const out = [];
|
|
722
|
-
let cur = "";
|
|
723
|
-
for (let i = 0; i < s.length; i++) {
|
|
724
|
-
const ch = s[i];
|
|
725
|
-
if (!mask.quoted[i] && /\s/.test(ch)) {
|
|
726
|
-
if (cur.length > 0)
|
|
727
|
-
out.push(cur);
|
|
728
|
-
cur = "";
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
cur += ch;
|
|
732
|
-
}
|
|
733
|
-
if (cur.length > 0)
|
|
734
|
-
out.push(cur);
|
|
735
|
-
return out;
|
|
392
|
+
const lexed = lexShellText(segment, { mode: "strict", connectors: "text" });
|
|
393
|
+
if ("reject" in lexed)
|
|
394
|
+
return { folded: [], raw: [] };
|
|
395
|
+
return wordsOf(lexed.segments[0]);
|
|
736
396
|
}
|
|
737
397
|
function hasUnquotedExpansionMetachar(rawToken) {
|
|
738
398
|
let open;
|
|
@@ -1304,20 +964,6 @@ const INTERPRETER_INLINE_PROGRAMS = new Map([
|
|
|
1304
964
|
function interpreterKey(name) {
|
|
1305
965
|
return name.replace(/\d+(?:\.\d+)*$/, "");
|
|
1306
966
|
}
|
|
1307
|
-
function withoutRedirectionWords(args) {
|
|
1308
|
-
const out = [];
|
|
1309
|
-
for (let i = 0; i < args.length; i++) {
|
|
1310
|
-
const t = args[i];
|
|
1311
|
-
if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(t)) {
|
|
1312
|
-
i++;
|
|
1313
|
-
continue;
|
|
1314
|
-
}
|
|
1315
|
-
if (/^(?:>>?|<)&\d*$/.test(t))
|
|
1316
|
-
continue;
|
|
1317
|
-
out.push(t);
|
|
1318
|
-
}
|
|
1319
|
-
return out;
|
|
1320
|
-
}
|
|
1321
967
|
function inlineProgramClause(model, args) {
|
|
1322
968
|
let endOfOptions = false;
|
|
1323
969
|
let banner = false;
|
|
@@ -1441,7 +1087,7 @@ function argumentAsProgramRefusal(written, name, args) {
|
|
|
1441
1087
|
const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(name));
|
|
1442
1088
|
if (interpreter === undefined)
|
|
1443
1089
|
return undefined;
|
|
1444
|
-
const clause = inlineProgramClause(interpreter,
|
|
1090
|
+
const clause = inlineProgramClause(interpreter, args);
|
|
1445
1091
|
return clause === undefined ? undefined : unjudgedProgramTextRefusal(written, clause);
|
|
1446
1092
|
}
|
|
1447
1093
|
function guardedVerbFormReason(name, toks, raw) {
|
|
@@ -1834,13 +1480,8 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1834
1480
|
for (let si = 0; si < segments.length; si++) {
|
|
1835
1481
|
const toks = tokenizeSegment(segments[si]);
|
|
1836
1482
|
const folded = toks.folded;
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
const controlWord = programBasename(folded[from] ?? "");
|
|
1840
|
-
from++;
|
|
1841
|
-
for (let span = launcherOptionSpan(controlWord, folded, from); span > 0; span = launcherOptionSpan(controlWord, folded, from))
|
|
1842
|
-
from += span;
|
|
1843
|
-
}
|
|
1483
|
+
const pos = readProgramPosition(toks);
|
|
1484
|
+
const from = pos.written ?? folded.length;
|
|
1844
1485
|
const written = folded[from];
|
|
1845
1486
|
if (written === undefined)
|
|
1846
1487
|
return { reason: "empty command" };
|
|
@@ -1858,15 +1499,11 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1858
1499
|
const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
|
|
1859
1500
|
if (pathPrefixed)
|
|
1860
1501
|
nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
|
|
1861
|
-
if (
|
|
1862
|
-
return { reason:
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
return { reason: launcherUnwrap.reason, refusedWhole: true };
|
|
1867
|
-
const runs = launcherUnwrap === undefined ? { head: name, index: 0 } : { head: launcherUnwrap.head, index: launcherUnwrap.index };
|
|
1868
|
-
if (runs?.head !== undefined && runs.index !== undefined) {
|
|
1869
|
-
const familyRefusal = argumentAsProgramRefusal(runs.head, runs.head, folded.slice(from + runs.index + 1));
|
|
1502
|
+
if (pos.refusal !== undefined)
|
|
1503
|
+
return { reason: programPositionRefusalSentence(pos.refusal), refusedWhole: true };
|
|
1504
|
+
const program = pos.program;
|
|
1505
|
+
if (program !== undefined) {
|
|
1506
|
+
const familyRefusal = argumentAsProgramRefusal(program.basename, program.basename, folded.slice(program.index + 1));
|
|
1870
1507
|
if (familyRefusal !== undefined)
|
|
1871
1508
|
return { reason: familyRefusal, refusedWhole: true };
|
|
1872
1509
|
}
|
|
@@ -1880,20 +1517,18 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1880
1517
|
nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
|
|
1881
1518
|
if (SHELL_REENTRY_PROGRAM_RE.test(name))
|
|
1882
1519
|
return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text"), refusedWhole: true };
|
|
1883
|
-
if (
|
|
1884
|
-
const
|
|
1885
|
-
if (
|
|
1886
|
-
return { reason:
|
|
1887
|
-
if (unwrapped.kind === "unlisted" && unwrapped.head !== undefined && SHELL_REENTRY_PROGRAM_RE.test(unwrapped.head)) {
|
|
1888
|
-
return { reason: `"${written}" runs "${unwrapped.head}", which runs its argument as shell text this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1520
|
+
if (pos.launchers.length > 0) {
|
|
1521
|
+
const listed = program !== undefined && (allow.has(program.basename) || program.basename === "cd");
|
|
1522
|
+
if (program !== undefined && !listed && SHELL_REENTRY_PROGRAM_RE.test(program.basename)) {
|
|
1523
|
+
return { reason: `"${written}" runs "${program.basename}", which runs its argument as shell text this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1889
1524
|
}
|
|
1890
|
-
if (argvReentryUnjudged(
|
|
1525
|
+
if (argvReentryUnjudged(pos, NO_PATH_OPERAND_COMMANDS)) {
|
|
1891
1526
|
return { reason: `"${written}" takes the operands of the program it runs from its standard input, which this walk does not judge ${NOT_AUTO_ALLOWED}`, refusedWhole: true };
|
|
1892
1527
|
}
|
|
1893
|
-
if (
|
|
1528
|
+
if (!listed || program === undefined)
|
|
1894
1529
|
unlistedSegments.add(si);
|
|
1895
1530
|
else
|
|
1896
|
-
reheaded.set(si, { from:
|
|
1531
|
+
reheaded.set(si, { from: program.index, head: program.basename });
|
|
1897
1532
|
continue;
|
|
1898
1533
|
}
|
|
1899
1534
|
unlistedSegments.add(si);
|
|
@@ -2040,10 +1675,10 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
2040
1675
|
shapeRefusal ??= `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}`;
|
|
2041
1676
|
}
|
|
2042
1677
|
if (boundary !== undefined) {
|
|
2043
|
-
const walk = evaluateReadBoundary(foldedSegments, boundary, foldedReach);
|
|
1678
|
+
const walk = evaluateReadBoundary(foldedSegments, boundary, true, foldedReach);
|
|
2044
1679
|
if (shapeRefusal === undefined)
|
|
2045
1680
|
return walk;
|
|
2046
|
-
const { reason: _walkSentence, ...structure } = walk;
|
|
1681
|
+
const { reason: _walkSentence, outOfRootGrantCandidate: _walkGrantCandidate, ...structure } = walk;
|
|
2047
1682
|
return { ...structure, reason: shapeRefusal };
|
|
2048
1683
|
}
|
|
2049
1684
|
if (shapeRefusal !== undefined)
|
|
@@ -2054,7 +1689,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
2054
1689
|
return {};
|
|
2055
1690
|
}
|
|
2056
1691
|
const BASE_CANDIDATE_MAX = 256;
|
|
2057
|
-
function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
1692
|
+
function evaluateReadBoundary(foldedSegments, boundary, mintGrantCandidate, reach) {
|
|
2058
1693
|
const seen = { outside: new Set(), inside: new Set(), undecided: new Set(), recursive: new Set(), unresolved: new Set(), checkedSpelled: new Set(), undecidedSpelled: new Set(), outsideSpelled: new Set() };
|
|
2059
1694
|
const outside = [];
|
|
2060
1695
|
const inside = [];
|
|
@@ -2212,10 +1847,20 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
|
|
|
2212
1847
|
return { ...checkedField, ...undecidedField };
|
|
2213
1848
|
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
2214
1849
|
const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
|
|
1850
|
+
const grantCandidate = (() => {
|
|
1851
|
+
if (!mintGrantCandidate || outOfRootSpellings.length > 0)
|
|
1852
|
+
return undefined;
|
|
1853
|
+
const proposed = outOfRootGrantCandidateOf(outside.map((o) => o.path), boundary);
|
|
1854
|
+
if (proposed === undefined)
|
|
1855
|
+
return undefined;
|
|
1856
|
+
const replay = evaluateReadBoundary(foldedSegments, { ...boundary, roots: [...boundary.roots, proposed] }, false, reach);
|
|
1857
|
+
return replay.reason === undefined && (replay.undecidedPaths ?? []).length === 0 ? proposed : undefined;
|
|
1858
|
+
})();
|
|
2215
1859
|
return {
|
|
2216
1860
|
reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} ${NOT_AUTO_ALLOWED}`,
|
|
2217
1861
|
outOfRootRead: true,
|
|
2218
1862
|
outOfRootPaths: outside.map((o) => o.path),
|
|
1863
|
+
...(grantCandidate !== undefined ? { outOfRootGrantCandidate: grantCandidate } : {}),
|
|
2219
1864
|
...outsideField,
|
|
2220
1865
|
...checkedField,
|
|
2221
1866
|
...undecidedField,
|
|
@@ -2225,44 +1870,21 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
2225
1870
|
const toks = tokenizeSegment(command);
|
|
2226
1871
|
if (toks.folded.length === 0)
|
|
2227
1872
|
return {};
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
const runs = COMMAND_LAUNCHERS.has(head) ? unwrapLauncher(chain, new Set()) : { kind: "reader", head, index: 0 };
|
|
2234
|
-
if (runs.kind === "refused")
|
|
2235
|
-
return { reason: runs.reason, refusedWhole: true };
|
|
2236
|
-
if (runs.head !== undefined && runs.index !== undefined) {
|
|
2237
|
-
const familyRefusal = argumentAsProgramRefusal(runs.head, runs.head, toks.folded.slice(runs.index + past + 1));
|
|
1873
|
+
const pos = readProgramPosition(toks);
|
|
1874
|
+
if (pos.refusal !== undefined)
|
|
1875
|
+
return { reason: programPositionRefusalSentence(pos.refusal), refusedWhole: true };
|
|
1876
|
+
if (pos.program !== undefined) {
|
|
1877
|
+
const familyRefusal = argumentAsProgramRefusal(pos.program.basename, pos.program.basename, toks.folded.slice(pos.program.index + 1));
|
|
2238
1878
|
if (familyRefusal !== undefined)
|
|
2239
1879
|
return { reason: familyRefusal, refusedWhole: true };
|
|
2240
1880
|
}
|
|
2241
|
-
return evaluateReadBoundary([toks], boundary);
|
|
1881
|
+
return evaluateReadBoundary([toks], boundary, true);
|
|
2242
1882
|
}
|
|
2243
1883
|
const NOT_GATED = { gated: false, paths: [] };
|
|
2244
|
-
const ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
2245
1884
|
const UNREADABLE_WHITELIST_WORD = /[$`()*?[\]{}~]/;
|
|
2246
|
-
function whitelistReadsTheSameWords(command, outsideHeredocBodies) {
|
|
2247
|
-
if (/\\\r?\n/.test(command))
|
|
2248
|
-
return false;
|
|
2249
|
-
if (/\\[<>&|;()]/.test(outsideHeredocBodies))
|
|
2250
|
-
return false;
|
|
2251
|
-
return !(outsideHeredocBodies.includes("#") && /["']/.test(outsideHeredocBodies));
|
|
2252
|
-
}
|
|
2253
1885
|
const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
2254
|
-
const COMMAND_LAUNCHERS = new Set([
|
|
2255
|
-
"env", "command", "builtin", "exec", "noglob", "xargs", "busybox",
|
|
2256
|
-
"sudo", "doas", "pkexec", "su", "runuser", "chroot", "setpriv",
|
|
2257
|
-
"nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
|
|
2258
|
-
"setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run",
|
|
2259
|
-
"strace", "ltrace", "valgrind", "firejail", "bwrap",
|
|
2260
|
-
]);
|
|
2261
1886
|
export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
|
|
2262
1887
|
const LAUNCHER_SCAN_HEAD = "cat";
|
|
2263
|
-
function programBasename(word) {
|
|
2264
|
-
return word.includes("/") ? (word.split("/").pop() ?? word) : word;
|
|
2265
|
-
}
|
|
2266
1888
|
export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
2267
1889
|
const containmentOnly = {
|
|
2268
1890
|
roots: boundary.roots,
|
|
@@ -2316,7 +1938,7 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
|
2316
1938
|
return NOT_GATED;
|
|
2317
1939
|
if (baseMoved)
|
|
2318
1940
|
return { gated: true, paths: [] };
|
|
2319
|
-
const verdict = evaluateReadBoundary(readers, containmentOnly, readerReach);
|
|
1941
|
+
const verdict = evaluateReadBoundary(readers, containmentOnly, false, readerReach);
|
|
2320
1942
|
return verdict.outOfRootRead === true ? { gated: true, paths: verdict.outOfRootPaths ?? [] } : NOT_GATED;
|
|
2321
1943
|
};
|
|
2322
1944
|
const direct = judge(command);
|
|
@@ -2417,7 +2039,7 @@ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary
|
|
|
2417
2039
|
if (readSegments.length === 0) {
|
|
2418
2040
|
return { reason: "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed" };
|
|
2419
2041
|
}
|
|
2420
|
-
const bodyHasCd = readSegments.some((seg) => tokensMoveBase(tokenizeSegment(seg)
|
|
2042
|
+
const bodyHasCd = readSegments.some((seg) => tokensMoveBase(tokenizeSegment(seg)));
|
|
2421
2043
|
const modelled = bodyHasCd ? Array.from({ length: beats }, () => readSegments.join("; ")).join("; ") : readSegments.join("; ");
|
|
2422
2044
|
const verdict = classifyCompoundReadonlyDetailed(modelled, allow, boundary, { iterated: bodyHasCd });
|
|
2423
2045
|
if (verdict.reason !== undefined)
|