@sema-agent/core 7.44.0 → 7.46.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 +418 -1
- package/dist/agents/extra-tool-names.d.ts +65 -0
- package/dist/agents/extra-tool-names.js +124 -0
- package/dist/agents/subagent.d.ts +17 -5
- package/dist/agents/subagent.js +65 -38
- package/dist/agents/tool-filter.d.ts +82 -5
- package/dist/agents/tool-filter.js +134 -4
- package/dist/core/agent-config-dir.d.ts +20 -0
- package/dist/core/agent-config-dir.js +49 -0
- package/dist/core/agent-definition.d.ts +2 -2
- package/dist/core/allow-entry.d.ts +30 -4
- package/dist/core/allow-entry.js +32 -5
- package/dist/core/ask-origin.d.ts +23 -4
- package/dist/core/ask-origin.js +11 -5
- package/dist/core/checkpoint-store.d.ts +41 -4
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/effective-path-target.d.ts +143 -10
- package/dist/core/effective-path-target.js +113 -9
- package/dist/core/engine-notice.d.ts +25 -3
- package/dist/core/engine-notice.js +12 -6
- package/dist/core/gate-fold.js +3 -0
- package/dist/core/gate-lanes.js +11 -3
- package/dist/core/gate-outcome.d.ts +5 -0
- package/dist/core/gate-outcome.js +2 -0
- package/dist/core/governance-codes.d.ts +3 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +5 -0
- package/dist/core/memory-engine/content-origin.d.ts +0 -34
- package/dist/core/memory-engine/content-origin.js +9 -4
- package/dist/core/memory-engine/layout.d.ts +5 -1
- package/dist/core/memory-engine/layout.js +1 -0
- package/dist/core/path-rule-binding.d.ts +65 -0
- package/dist/core/path-rule-binding.js +99 -0
- package/dist/core/permission-rule-model.d.ts +47 -18
- package/dist/core/permission-rule-model.js +38 -42
- package/dist/core/permission-rule-provider.d.ts +7 -0
- package/dist/core/permission-rule-provider.js +10 -3
- package/dist/core/permission-rule-store.d.ts +8 -0
- package/dist/core/permission-rule-store.js +6 -2
- package/dist/core/permission-rule-sync.js +2 -2
- package/dist/core/permission-rules.d.ts +22 -36
- package/dist/core/permission-rules.js +50 -40
- package/dist/core/real-approval.d.ts +9 -6
- package/dist/core/real-approval.js +5 -5
- package/dist/core/runner/child-tree.d.ts +110 -0
- package/dist/core/runner/child-tree.js +107 -0
- package/dist/core/runner/construction-doors.d.ts +5 -0
- package/dist/core/runner/construction-doors.js +6 -0
- package/dist/core/runner/contracts.d.ts +54 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
- package/dist/core/runner/permission-rule-lanes.js +18 -12
- package/dist/core/runner/persisted-path-rows.d.ts +31 -7
- package/dist/core/runner/persisted-path-rows.js +31 -12
- package/dist/core/runner/prepare-ask-lane.d.ts +6 -0
- package/dist/core/runner/prepare-ask-lane.js +4 -2
- package/dist/core/runner/prepare-config-doors.js +3 -1
- package/dist/core/runner/prepare-execution-env.d.ts +5 -2
- package/dist/core/runner/prepare-execution-env.js +11 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
- package/dist/core/runner/prepare-gate-stations.js +3 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
- package/dist/core/runner/prepare-hands-readface.js +6 -6
- package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
- package/dist/core/runner/prepare-inherited-gate.js +8 -2
- package/dist/core/runner/prepare-listings.d.ts +4 -4
- package/dist/core/runner/prepare-listings.js +2 -22
- package/dist/core/runner/prepare-park-ask.d.ts +5 -0
- package/dist/core/runner/prepare-park-ask.js +6 -4
- package/dist/core/runner/prepare-policy-chain.d.ts +16 -2
- package/dist/core/runner/prepare-policy-chain.js +14 -5
- package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
- package/dist/core/runner/prepare-suspend-saga.js +7 -1
- package/dist/core/runner/prepare-task.js +41 -18
- package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
- package/dist/core/runner/prepare-turn-wiring.js +2 -1
- package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
- package/dist/core/runner/prepare-workspace-restore.js +1 -0
- package/dist/core/runner/resume-apply.js +17 -3
- package/dist/core/runner/resume-checkpoint-screen.js +13 -0
- package/dist/core/runner/resume-policy-outcome.js +2 -1
- package/dist/core/runner/run-leg.js +5 -1
- package/dist/core/runner/run-notification-lane.js +2 -2
- package/dist/core/runner/runtask.d.ts +2 -0
- package/dist/core/runner/runtask.js +4 -2
- package/dist/core/runner/stream-steer-verb.js +2 -2
- package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
- package/dist/core/runner/sub-agent-deny-lists.js +121 -0
- package/dist/core/runner/synthetic-tools.d.ts +1 -1
- package/dist/core/runner/synthetic-tools.js +3 -2
- package/dist/core/runner/write-protection-station.d.ts +3 -2
- package/dist/core/runner/write-protection-station.js +1 -0
- package/dist/core/runner-deps.d.ts +14 -0
- package/dist/core/sensitive-path-policy.d.ts +8 -0
- package/dist/core/sensitive-path-policy.js +18 -2
- package/dist/core/session-policy-store.d.ts +6 -2
- package/dist/core/session-policy-store.js +2 -1
- package/dist/core/shell-wrapper-projection.d.ts +11 -0
- package/dist/core/shell-wrapper-projection.js +56 -0
- package/dist/core/shell-wrapper-table.d.ts +144 -34
- package/dist/core/shell-wrapper-table.js +40 -20
- package/dist/core/skill-grant.d.ts +10 -2
- package/dist/core/skill-grant.js +20 -4
- package/dist/core/synthetic-read-verdict.d.ts +3 -0
- package/dist/core/synthetic-read-verdict.js +3 -1
- package/dist/core/task-notification.d.ts +12 -1
- package/dist/core/task-notification.js +13 -1
- package/dist/core/task-spec.d.ts +5 -3
- package/dist/core/tool-catalog-entries.d.ts +21 -0
- package/dist/core/tool-catalog-entries.js +9 -0
- package/dist/core/tool-policy.d.ts +45 -12
- package/dist/core/tool-policy.js +18 -20
- package/dist/core/tool-spec.d.ts +23 -4
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.js +1 -1
- package/dist/core/write-protect.d.ts +4 -0
- package/dist/core/write-protect.js +30 -9
- package/dist/index.d.ts +10 -6
- package/dist/index.js +9 -5
- package/dist/orchestration/run-spec.js +8 -7
- package/dist/orchestration/workflow.js +3 -2
- package/dist/shell/lex.d.ts +7 -0
- package/dist/shell/lex.js +17 -0
- package/dist/tools/fs/bash-program-position.d.ts +8 -87
- package/dist/tools/fs/bash-program-position.js +33 -69
- package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
- package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
- package/dist/tools/fs/index.d.ts +3 -2
- package/dist/tools/fs/program-read-model.d.ts +694 -0
- package/dist/tools/fs/program-read-model.js +1252 -0
- package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
- package/dist/tools/fs/program-read-model.rows.js +613 -0
- package/dist/tools/fs/read-deny-home.d.ts +19 -1
- package/dist/tools/fs/read-deny-home.js +62 -24
- package/dist/tools/fs/read-deny.d.ts +54 -14
- package/dist/tools/fs/read-deny.js +104 -50
- package/dist/tools/fs/removal-targets.d.ts +83 -0
- package/dist/tools/fs/removal-targets.js +236 -0
- package/dist/tools/fs/safety.d.ts +3 -0
- package/dist/tools/fs/safety.js +10 -3
- package/dist/tools/fs/shell-read-argv.d.ts +85 -0
- package/dist/tools/fs/shell-read-argv.js +386 -0
- package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
- package/dist/tools/fs/shell-read-bindings.js +746 -0
- package/dist/tools/fs/shell-read-check.js +23 -15
- package/dist/tools/fs/shell-read-establish.js +79 -5
- package/dist/tools/fs/shell-read-judge.d.ts +11 -1
- package/dist/tools/fs/shell-read-judge.js +9 -1
- package/dist/tools/fs/shell-read-plan.d.ts +31 -7
- package/dist/tools/fs/shell-read-plan.js +299 -393
- package/dist/tools/fs/shell-read-word.d.ts +29 -26
- package/dist/tools/fs/shell-read-word.js +30 -38
- package/dist/tools/fs/shell-read-write-targets.d.ts +15 -37
- package/dist/tools/fs/shell-read-write-targets.js +83 -136
- package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
- package/dist/tools/fs/shell-reentry-text.js +90 -53
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +49 -3
- package/wire-changes/core@7.45.0.json +1084 -0
- package/wire-changes/core@7.46.0.json +1522 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { planShellReads } from "./shell-read-plan.js";
|
|
2
|
+
import { literalTextOf } from "../../shell/lex.js";
|
|
3
|
+
import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
|
|
4
|
+
import { isAbsolutePathForm, isWinFormPath, normalizeAbsPathLexically } from "./safety.js";
|
|
5
|
+
import { reentryText } from "./shell-reentry-text.js";
|
|
6
|
+
import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
7
|
+
import { programArgs } from "./shell-read-write-targets.js";
|
|
8
|
+
import { implementationsOf, programRow, runsShellText, selectedOptions, writeReadings } from "./program-read-model.js";
|
|
9
|
+
import { inlineUntrusted } from "../../core/untrusted-text.js";
|
|
10
|
+
export const SHELL_REMOVAL_COMMANDS = Object.freeze(["rm", "rmdir"]);
|
|
11
|
+
const REMOVAL_COMMAND_SET = new Set(SHELL_REMOVAL_COMMANDS);
|
|
12
|
+
function removalKey(path) {
|
|
13
|
+
let k = normalizeAbsPathLexically(path.normalize("NFC")).replace(/\\/g, "/").toLowerCase();
|
|
14
|
+
k = k.replace(/(?:\/+\*+)+\/*$/, "");
|
|
15
|
+
const drive = driveOf(path, k);
|
|
16
|
+
let rest = k.slice(drive.length);
|
|
17
|
+
if (rest === "")
|
|
18
|
+
rest = "/";
|
|
19
|
+
if (rest.length > 1)
|
|
20
|
+
rest = rest.replace(/\/+$/, "");
|
|
21
|
+
return drive + rest.replace(/^\/private\/(etc|var|tmp|home)(?=\/|$)/, "/$1");
|
|
22
|
+
}
|
|
23
|
+
function driveOf(path, key) {
|
|
24
|
+
return isWinFormPath(path) && key.length >= 2 && key[1] === ":" ? key.slice(0, 2) : "";
|
|
25
|
+
}
|
|
26
|
+
export function dangerousRemovalOf(path, seat) {
|
|
27
|
+
if (!isAbsolutePathForm(path))
|
|
28
|
+
return undefined;
|
|
29
|
+
const k = removalKey(path);
|
|
30
|
+
const rest = k.slice(driveOf(path, k).length);
|
|
31
|
+
if (rest === "/" || /^\/\/[^/]+\/[^/]+$/.test(k))
|
|
32
|
+
return "filesystem_root";
|
|
33
|
+
if (seat.homes.some((h) => isAbsolutePathForm(h) && removalKey(h) === k))
|
|
34
|
+
return "home";
|
|
35
|
+
const prefix = k.endsWith("/") ? k : `${k}/`;
|
|
36
|
+
if (seat.workspace.some((w) => isAbsolutePathForm(w) && (removalKey(w) === k || removalKey(w).startsWith(prefix))))
|
|
37
|
+
return "workspace";
|
|
38
|
+
if (/^\/[^/]+$/.test(rest))
|
|
39
|
+
return "top_level";
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
function tildeNamesAHome(spelled) {
|
|
43
|
+
const m = /^~([^/]*)(\/.*)?$/.exec(spelled);
|
|
44
|
+
if (m === null)
|
|
45
|
+
return false;
|
|
46
|
+
const key = removalKey(`/~home${m[2] ?? ""}`);
|
|
47
|
+
return key === "/~home" || !key.startsWith("/~home/");
|
|
48
|
+
}
|
|
49
|
+
function removalKindOf(read, seat) {
|
|
50
|
+
const floors = read.established === "unestablishable" && read.candidate.bases === "unmodelled" ? read.spellings : [];
|
|
51
|
+
for (const view of [read.target, read.origin, ...floors]) {
|
|
52
|
+
const kind = view === undefined ? undefined : dangerousRemovalOf(view, seat);
|
|
53
|
+
if (kind !== undefined)
|
|
54
|
+
return kind;
|
|
55
|
+
}
|
|
56
|
+
return read.established === "unestablishable" && tildeNamesAHome(read.candidate.spelled) ? "home" : undefined;
|
|
57
|
+
}
|
|
58
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
59
|
+
const NAME_BINDERS = new Set(["unset", "local", "declare", "typeset", "readonly", "export", "read"]);
|
|
60
|
+
function bindingsOf(plan) {
|
|
61
|
+
const out = new Map();
|
|
62
|
+
const bind = (name, value) => {
|
|
63
|
+
out.set(name, [...(out.get(name) ?? []), value]);
|
|
64
|
+
};
|
|
65
|
+
for (const segment of plan.segments) {
|
|
66
|
+
for (const w of segment.argv)
|
|
67
|
+
if (w.assignment !== undefined)
|
|
68
|
+
bind(w.assignment.name, w.assignment.settlesWholeName ? literalTextOf(w.assignment.value.pieces) : undefined);
|
|
69
|
+
const [head, name] = segment.argv;
|
|
70
|
+
if ((head?.folded === "for" || head?.folded === "select") && name !== undefined && NAME.test(name.folded)) {
|
|
71
|
+
const at = segment.argv.findIndex((w, i) => i > 1 && w.folded === "in");
|
|
72
|
+
const words = at < 0 ? [undefined] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces));
|
|
73
|
+
for (const v of words.length > 0 ? words : [undefined])
|
|
74
|
+
bind(name.folded, v);
|
|
75
|
+
}
|
|
76
|
+
if (head !== undefined && NAME_BINDERS.has(head.folded))
|
|
77
|
+
for (const w of segment.argv.slice(1))
|
|
78
|
+
if (w.assignment === undefined && NAME.test(w.folded))
|
|
79
|
+
bind(w.folded, undefined);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
const BOUND_SPELLINGS_MAX = 64;
|
|
84
|
+
function boundSpellingsOf(word, bindings) {
|
|
85
|
+
const named = word.pieces.filter((q) => q.literal === undefined);
|
|
86
|
+
if (named.length === 0 || !named.some((q) => q.expansion?.kind === "parameter" && q.expansion.name !== undefined && bindings.has(q.expansion.name)))
|
|
87
|
+
return undefined;
|
|
88
|
+
let spellings = [""];
|
|
89
|
+
for (const q of word.pieces) {
|
|
90
|
+
if (q.literal !== undefined) {
|
|
91
|
+
spellings = spellings.map((sp) => sp + q.literal);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const name = q.expansion?.kind === "parameter" && (q.expansion.body === undefined || q.expansion.body === q.expansion.name) ? q.expansion.name : undefined;
|
|
95
|
+
const values = name !== undefined ? bindings.get(name) : undefined;
|
|
96
|
+
if (name === undefined || values === undefined)
|
|
97
|
+
return { unreadable: q.expansion?.name ?? word.raw };
|
|
98
|
+
if (values.some((v) => v === undefined || (q.quotedBy === undefined && /\s/.test(v))))
|
|
99
|
+
return { unreadable: name };
|
|
100
|
+
spellings = spellings.flatMap((sp) => values.map((v) => sp + v));
|
|
101
|
+
if (spellings.length > BOUND_SPELLINGS_MAX)
|
|
102
|
+
return { unreadable: name };
|
|
103
|
+
}
|
|
104
|
+
return { spellings };
|
|
105
|
+
}
|
|
106
|
+
function recursiveForce(row, args) {
|
|
107
|
+
return implementationsOf(row).some((view) => {
|
|
108
|
+
const selected = selectedOptions(view, args, "one-implementation");
|
|
109
|
+
return selected.some((o) => o.selects === "traversal") && selected.some((o) => o.mode === "forced");
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function removalFindingsOf(established, seat) {
|
|
113
|
+
const out = [];
|
|
114
|
+
const bindings = bindingsOf(established.plan);
|
|
115
|
+
for (const segment of established.plan.segments) {
|
|
116
|
+
const program = segment.verb?.basename;
|
|
117
|
+
const row = program !== undefined && REMOVAL_COMMAND_SET.has(program) ? programRow(program) : undefined;
|
|
118
|
+
if (program !== undefined && row !== undefined) {
|
|
119
|
+
const args = programArgs(segment);
|
|
120
|
+
const spelled = args.map((w) => w.spelled);
|
|
121
|
+
const removed = new Set(writeReadings(row, spelled).flatMap((r) => r.written.map((w) => w.at)));
|
|
122
|
+
const options = new Set(args.filter((_, at) => !removed.has(at)).map((w) => w.raw));
|
|
123
|
+
for (const read of established.reads.get(segment) ?? []) {
|
|
124
|
+
if (read.candidate.face !== "write" || options.has(read.candidate.raw))
|
|
125
|
+
continue;
|
|
126
|
+
const kind = removalKindOf(read, seat);
|
|
127
|
+
if (kind !== undefined) {
|
|
128
|
+
out.push({ guard: "dangerous_delete", kind, program, operand: read.candidate.raw, target: read.target ?? read.candidate.floorSpelled ?? read.candidate.spelled });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (read.established !== "unestablishable")
|
|
132
|
+
continue;
|
|
133
|
+
const word = segment.argv.find((w) => w.raw === read.candidate.raw);
|
|
134
|
+
const bound = word === undefined ? undefined : boundSpellingsOf(word, bindings);
|
|
135
|
+
if (bound === undefined)
|
|
136
|
+
continue;
|
|
137
|
+
if ("unreadable" in bound) {
|
|
138
|
+
if (recursiveForce(row, spelled))
|
|
139
|
+
out.push({ guard: "unverifiable_delete", program, operand: read.candidate.raw, name: bound.unreadable });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const bases = segment.bases === "unmodelled" ? (read.candidate.floorBases ?? []) : segment.bases;
|
|
143
|
+
for (const spelled of bound.spellings) {
|
|
144
|
+
const views = isAbsolutePathForm(spelled) ? [spelled] : bases.map((base) => `${base}/${spelled}`);
|
|
145
|
+
for (const view of views) {
|
|
146
|
+
const hit = dangerousRemovalOf(view, seat);
|
|
147
|
+
if (hit === undefined)
|
|
148
|
+
continue;
|
|
149
|
+
out.push({ guard: "dangerous_delete", kind: hit, program, operand: read.candidate.raw, target: normalizeAbsPathLexically(view) });
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const script = runtimeScriptOf(segment);
|
|
156
|
+
if (script !== undefined && program !== undefined)
|
|
157
|
+
out.push({ guard: "dangerous_delete", kind: "runtime_script", program, operand: script, target: script });
|
|
158
|
+
for (const nested of segment.nested) {
|
|
159
|
+
const sub = established.nested.get(nested);
|
|
160
|
+
if (sub !== undefined)
|
|
161
|
+
out.push(...removalFindingsOf(sub, seat));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function runtimeScriptOf(segment) {
|
|
167
|
+
const program = segment.verb;
|
|
168
|
+
if (segment.reentry !== "shell" || program === undefined || !runsShellText(program.basename))
|
|
169
|
+
return undefined;
|
|
170
|
+
const at = segment.argv.findIndex((w) => w.folded === program.name);
|
|
171
|
+
const MARK = "\u0000";
|
|
172
|
+
const words = at < 0 ? [] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces) ?? `${MARK}${w.raw}`);
|
|
173
|
+
const texts = [reentryText(program.basename, words), ...(segment.stdin ?? []).map((s) => reentryText(program.basename, words, s.literal ?? `${MARK}${s.from}`))];
|
|
174
|
+
const text = texts.find((t) => t?.includes(MARK) === true);
|
|
175
|
+
return text === undefined ? undefined : text.split(MARK).join("");
|
|
176
|
+
}
|
|
177
|
+
export function planRunsRemoval(plan) {
|
|
178
|
+
return plan.segments.some((s) => (s.verb !== undefined && REMOVAL_COMMAND_SET.has(s.verb.basename)) || runtimeScriptOf(s) !== undefined || s.nested.some(planRunsRemoval));
|
|
179
|
+
}
|
|
180
|
+
const DANGEROUS_REMOVAL_SENTENCE = {
|
|
181
|
+
filesystem_root: (p, t) => `\`${p}\` would remove the filesystem root (${t})`,
|
|
182
|
+
home: (p, t) => `\`${p}\` would remove a home directory (${t})`,
|
|
183
|
+
workspace: (p, t) => `\`${p}\` would remove a workspace directory — the task root, the working directory or an additional working directory — or one of its ancestors (${t})`,
|
|
184
|
+
top_level: (p, t) => `\`${p}\` would remove a top-level system directory (${t})`,
|
|
185
|
+
runtime_script: (p, t) => `\`${p}\` runs a script built only when the command runs (${t}), and it may remove the filesystem root, a home directory or the task's directories before anyone can read it`,
|
|
186
|
+
};
|
|
187
|
+
function verdictOf(findings) {
|
|
188
|
+
for (const f of findings)
|
|
189
|
+
if (f.guard === "dangerous_delete")
|
|
190
|
+
return { guard: f.guard, text: DANGEROUS_REMOVAL_SENTENCE[f.kind](f.program, inlineUntrusted(f.target, 200)) };
|
|
191
|
+
for (const f of findings)
|
|
192
|
+
if (f.guard === "unverifiable_delete")
|
|
193
|
+
return { guard: f.guard, text: `\`${f.program}\` recursive+force target ${inlineUntrusted(f.operand, 200)} takes ${f.name}'s value, which this command sets in a form the guard cannot read (an expansion, an unset or bare declaration, a \`read\`, a loop over unread words) — the deleted path cannot be verified from this command` };
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const REMOVAL_RESOLVE_TIMEOUT_MS = 10_000;
|
|
197
|
+
export function removalReadingOf(command, req, seat) {
|
|
198
|
+
const homeDir = seat?.env?.homeDir;
|
|
199
|
+
const cwd = req.cwd ?? req.root ?? "";
|
|
200
|
+
const roots = req.root !== undefined ? [req.root] : cwd !== "" ? [cwd] : [];
|
|
201
|
+
const boundary = { roots, ...(cwd !== "" ? { cwd } : {}), ...(homeDir !== undefined ? { homeDir } : {}) };
|
|
202
|
+
const plan = planShellReads(command, { readers: new Set(bashClassifyDefaultAllow()), cwd, roots, ...(homeDir !== undefined ? { homeDir } : {}) });
|
|
203
|
+
if (!planRunsRemoval(plan))
|
|
204
|
+
return undefined;
|
|
205
|
+
return {
|
|
206
|
+
async judge(signal) {
|
|
207
|
+
const env = seat?.env;
|
|
208
|
+
const resolved = env !== undefined && homeDir !== undefined ? await resolvedOrUndefined(env.canonicalPath(homeDir, signal), signal) : undefined;
|
|
209
|
+
const removalSeat = {
|
|
210
|
+
homes: [...(homeDir !== undefined ? [homeDir] : []), ...(resolved !== undefined && resolved.ok ? [resolved.value] : [])],
|
|
211
|
+
workspace: [...(req.root !== undefined ? [req.root] : []), ...(req.cwd !== undefined ? [req.cwd] : []), ...(seat?.additionalRoots?.() ?? [])],
|
|
212
|
+
};
|
|
213
|
+
const lexical = removalFindingsOf(establishShellReadsLexically(plan, boundary), removalSeat);
|
|
214
|
+
if (lexical.some((f) => f.guard === "dangerous_delete") || env === undefined || roots.length === 0)
|
|
215
|
+
return verdictOf(lexical);
|
|
216
|
+
const established = await resolvedOrUndefined(createShellReadEstablisher(env, roots, { rootsCanonical: true }).establish(plan, boundary, signal), signal);
|
|
217
|
+
return verdictOf(established === undefined ? lexical : [...removalFindingsOf(established, removalSeat), ...lexical]);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async function resolvedOrUndefined(leg, signal) {
|
|
222
|
+
let timer;
|
|
223
|
+
try {
|
|
224
|
+
return await Promise.race([
|
|
225
|
+
leg.catch(() => undefined),
|
|
226
|
+
new Promise((resolve) => {
|
|
227
|
+
timer = setTimeout(() => resolve(undefined), REMOVAL_RESOLVE_TIMEOUT_MS);
|
|
228
|
+
signal?.addEventListener("abort", () => resolve(undefined), { once: true });
|
|
229
|
+
}),
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
if (timer !== undefined)
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -129,6 +129,9 @@ export interface FsViolation {
|
|
|
129
129
|
* lands it outside every POSIX/drive root, i.e. a location the working directory does not contain.
|
|
130
130
|
*/
|
|
131
131
|
export declare function isAbsolutePathForm(p: string): boolean;
|
|
132
|
+
/** Does this absolute spelling name the opening process's STANDARD INPUT (`/dev/stdin`, `/dev/fd/0`, `/proc/<pid>/fd/0`),
|
|
133
|
+
* folded lexically first (`/dev/./stdin`, `/dev//fd/0`)? */
|
|
134
|
+
export declare function namesStandardInput(path: string): boolean;
|
|
132
135
|
/**
|
|
133
136
|
* Windows-FAMILY spelling of a path (extracted here so {@link canonicalizeTarget} judges
|
|
134
137
|
* the UNC family by the same rule): drive-rooted (`C:\…`/`C:/…`), UNC-backslash (`\\host\share`), or a
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readDenySentence } from "./read-deny.js";
|
|
|
3
3
|
import { homeViewsOf } from "./read-deny-home.js";
|
|
4
4
|
import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
|
|
5
5
|
import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
|
|
6
|
+
import { readPathViews } from "../../core/effective-path-target.js";
|
|
6
7
|
import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
|
|
7
8
|
import { thrownText } from "../../core/thrown-value.js";
|
|
8
9
|
export function fileArgPath(args) {
|
|
@@ -41,6 +42,10 @@ const BLOCKED_DEVICE_PATHS = new Set([
|
|
|
41
42
|
function isProcStdioFd(key) {
|
|
42
43
|
return key.startsWith("/proc/") && (key.endsWith("/fd/0") || key.endsWith("/fd/1") || key.endsWith("/fd/2"));
|
|
43
44
|
}
|
|
45
|
+
export function namesStandardInput(path) {
|
|
46
|
+
const k = path.startsWith("/") ? normalizeAbsPathLexically(path) : "";
|
|
47
|
+
return k === "/dev/stdin" || k === "/dev/fd/0" || (k.startsWith("/proc/") && k.endsWith("/fd/0"));
|
|
48
|
+
}
|
|
44
49
|
const PROC_SENSITIVE_SUFFIXES = ["/environ", "/cmdline", "/auxv", "/maps", "/mem", "/stat"];
|
|
45
50
|
function isProcSensitiveFile(key) {
|
|
46
51
|
return key.startsWith("/proc/") && PROC_SENSITIVE_SUFFIXES.some((suf) => key.endsWith(suf));
|
|
@@ -457,7 +462,8 @@ function within(rootCanonical, p) {
|
|
|
457
462
|
return pN.identity.startsWith(rootN.identity.endsWith(SEP) ? rootN.identity : rootN.identity + SEP);
|
|
458
463
|
}
|
|
459
464
|
export async function resolveKey(env, rootCanonical, path, signal, baseCwd, additionalRoots, exactFileReadExemption, readDeny, containmentFace) {
|
|
460
|
-
const
|
|
465
|
+
const views = await readPathViews(env, path, baseCwd ?? rootCanonical, { root: rootCanonical, ...(baseCwd !== undefined ? { cwd: baseCwd } : {}) }, signal, { hops: readDeny !== undefined });
|
|
466
|
+
const canon = views.canonical ?? (await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootCanonical }), baseCwd));
|
|
461
467
|
if (!canon.ok)
|
|
462
468
|
return { ok: false, violation: { code: "invalid", message: canon.message } };
|
|
463
469
|
const key = canon.key;
|
|
@@ -466,8 +472,9 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
|
|
|
466
472
|
return { ok: false, violation: { code: "invalid", message: `path "${path}" is a blocked device/special file (reading it can hang the process); refused.` } };
|
|
467
473
|
}
|
|
468
474
|
if (readDeny !== undefined) {
|
|
469
|
-
const judged = { aliasResolved: canon.aliasResolved === true };
|
|
470
|
-
|
|
475
|
+
const judged = { aliasResolved: canon.aliasResolved === true, hops: views.hops };
|
|
476
|
+
const lexical = views.lexical?.kind === "path" ? spellingOfIdentity(views.lexical.identity, pathFamilyOf({ root: rootCanonical })) : undefined;
|
|
477
|
+
let hit = readDeny.matchTarget(key, lexical, judged);
|
|
471
478
|
if (hit === null) {
|
|
472
479
|
const homeView = await homeViewsOf(env, readDeny, signal).of(key);
|
|
473
480
|
if (homeView !== undefined)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The candidates ONE program's words hand the read station — the program read model's rows read word by word: a
|
|
3
|
+
* listed reader's operands and option-value slots, an unlisted program's words (its own row read where it has one,
|
|
4
|
+
* read the speculative way where it has none), a subcommand's words by the subcommand's row, a module's program text,
|
|
5
|
+
* an assignment word's value. `shell-read-plan.ts` threads the segments and their bases; this module is what one
|
|
6
|
+
* segment's argv offers.
|
|
7
|
+
*/
|
|
8
|
+
import { type LexedWord } from "../../shell/lex.js";
|
|
9
|
+
import { type Read } from "./shell-read-word.js";
|
|
10
|
+
import { type BindingTable, type BoundValue } from "./shell-read-bindings.js";
|
|
11
|
+
import { subcommandOf, type ArgRole, type ProgramRow } from "./program-read-model.js";
|
|
12
|
+
import type { ReadCandidate, ReadCandidateProvenance } from "./shell-read-plan.js";
|
|
13
|
+
/** A candidate before the threading fills its bases. `alternatives`: the other spellings its word may expand to (a name
|
|
14
|
+
* bound to several values) — one candidate each once the segment's reads are read. */
|
|
15
|
+
export type Bare = Omit<ReadCandidate, "bases"> & {
|
|
16
|
+
readonly alternatives?: readonly string[];
|
|
17
|
+
readonly floors?: readonly BoundValue[];
|
|
18
|
+
};
|
|
19
|
+
/** What reading a segment's words found beside its candidates. */
|
|
20
|
+
export interface WordContext {
|
|
21
|
+
/** The names at this segment. */
|
|
22
|
+
readonly table: BindingTable;
|
|
23
|
+
/** Shell text the arithmetic evaluator runs (a substitution in a subscript it evaluates): the segment re-enters it. */
|
|
24
|
+
readonly reentered: string[];
|
|
25
|
+
/** Program text the station does not read — inline text in a language that may open files, or a program read from
|
|
26
|
+
* standard input: why the segment's reads are not established. */
|
|
27
|
+
programText?: string;
|
|
28
|
+
/** Some program text was found in a slot (whether or not it may open files). */
|
|
29
|
+
programTextSeen: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** The subcommand whose row says how ITS words name paths (operands or a traversal of its own), or `undefined`: a
|
|
32
|
+
* subcommand whose row states only its grammar — its message slots, read by {@link messageWords} — leaves its words to
|
|
33
|
+
* the program's own reading. */
|
|
34
|
+
export declare function pathSubcommandOf(row: ProgramRow | undefined, args: readonly string[]): ReturnType<typeof subcommandOf>;
|
|
35
|
+
/** The sentence of inline program text the station does not read. */
|
|
36
|
+
export declare const programTextSentence: (language: string) => string;
|
|
37
|
+
/** One candidate without its bases (filled by the threading pass): the word's spelling — or `spelled`, a caller's
|
|
38
|
+
* reading of PART of it (an option payload, a `key=value` value) — with the word's own expansion facts. A word whose
|
|
39
|
+
* names may hold several values offers each as its whole spelling; a PART of such a word is not followed through them
|
|
40
|
+
* (it is unresolvable). */
|
|
41
|
+
export declare function bare(r: Read, provenance: ReadCandidateProvenance, spelled?: string): Bare;
|
|
42
|
+
/** The reads one value of a declared role hands over: a candidate per file it names, or — a value the program expands
|
|
43
|
+
* past the listed bound — the word itself, unresolvable. A mount source is a read the daemon makes of the whole tree
|
|
44
|
+
* under it (a read and a traversal root); an object path
|
|
45
|
+
* `<rev>:<path>` offers the word as a path and the `<path>` it spells to the deny judge; program text is never a path
|
|
46
|
+
* ({@link programTextReads}). `alsoOperand`: the walk read the option past the first operand, where the program may
|
|
47
|
+
* have stopped reading options — a path role is still offered (more candidates), but no program text is read there:
|
|
48
|
+
* the word is the operands' (the script's own argument), as the program's delivery reading says. */
|
|
49
|
+
export declare function roleReads(r: Read, provenance: ReadCandidateProvenance, value: string, role: ArgRole, globs: "expanded" | "literal", ctx: WordContext, alsoOperand?: boolean): Bare[];
|
|
50
|
+
/**
|
|
51
|
+
* The candidates a `NAME=value` word hands the judge, both facts about its VALUE (the word is a path to nobody):
|
|
52
|
+
* what path it NAMES, and what the text cannot SETTLE — placed as an operand is wherever an environment value
|
|
53
|
+
* names ONE path (a leading tilde against the declared home; a `:` joins a second). Where the text runs nothing able
|
|
54
|
+
* to open a file, {@link planShellReads} drops them: the value names a path to nobody.
|
|
55
|
+
*
|
|
56
|
+
* Bash applies its assignment-word expansion to such a word even when it is an ARGUMENT to a simple command, so
|
|
57
|
+
* the program receives `NAME=<expanded value>`, and a program that reads its value as a path (`dd if=…`, a build
|
|
58
|
+
* or tool variable, a launcher's environment word) opens what the value names. Two facts of the lexer's
|
|
59
|
+
* {@link AssignmentFact} are read: a tilde the shell EXPANDS in the value (at its start and after each `:`, where
|
|
60
|
+
* unquoted — `value.expansions`) leaves the path unsettled, and the append form (`NAME+=value`) names `<whatever
|
|
61
|
+
* NAME held>` + value, so the whole value is not in the text — but the text it appends IS, and an unset or empty
|
|
62
|
+
* NAME makes it the whole value, so it is a candidate too. A value that is a substitution is the nested plan
|
|
63
|
+
* its body already is, never a candidate of its own text (its placeholder is not a path).
|
|
64
|
+
*/
|
|
65
|
+
export declare function pushAssignment(reads: Bare[], w: LexedWord, provenance: ReadCandidateProvenance): void;
|
|
66
|
+
/** Every word of a segment whose program's grammar this parser does not model, from `from` onward: option
|
|
67
|
+
* payloads and path-shaped option words included, the launcher chain's own words excluded, a program declared
|
|
68
|
+
* to open no path operand handing only the environment words in front of it. A program with NO row is read the
|
|
69
|
+
* speculative way: every word is still handed over, but the filesystem decides it
|
|
70
|
+
* ({@link ReadCandidate.speculative}). */
|
|
71
|
+
export declare function unlistedWords(reads: Bare[], toks: readonly Read[], from: number, launcherIndices: ReadonlySet<number>, provenance: ReadCandidateProvenance, ctx: WordContext, program?: {
|
|
72
|
+
readonly index: number;
|
|
73
|
+
readonly basename: string;
|
|
74
|
+
}): void;
|
|
75
|
+
/** The positional operands and declared option-value slots of a listed reader — the program read model's argv walk,
|
|
76
|
+
* one candidate per word the program may open, plus the traversal roots of a recursive form. A subcommand's words
|
|
77
|
+
* are read by its own row; the words a module takes as its program text are program text. */
|
|
78
|
+
export declare function listedVerbCandidates(name: string, argReads: readonly Read[], reads: Bare[], ctx: WordContext): void;
|
|
79
|
+
/** The subscript of a variable-name word (`a[…]`), as the arithmetic text that is evaluated; the empty text for a plain
|
|
80
|
+
* name. */
|
|
81
|
+
export declare function subscriptOnly(word: string): string;
|
|
82
|
+
/** An assignment word's arithmetic: its own subscript (`a[$(…)]=1`, quoted or not — not for a builtin that takes the
|
|
83
|
+
* name `asWritten`), and its value where the name carries the integer attribute (`declare -i n; n='a[$(…)]'`, bash's
|
|
84
|
+
* own `OPTIND` / `RANDOM` / …). */
|
|
85
|
+
export declare function assignmentArithmetic(w: LexedWord, integer: boolean, ctx: WordContext, asWritten?: boolean): void;
|