@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,183 @@
|
|
|
1
|
+
const SPECIAL_PARAMS = "?$!#@*-0123456789";
|
|
2
|
+
const SUBSTITUTION_RE = /\$\(|`|<\(|>\(|\$\[/;
|
|
3
|
+
export const arithmeticHidesRun = (expr) => /[^\s\d+\-*/%&|^~!<>=?:,()]/.test(expr);
|
|
4
|
+
const SUBSCRIPT_RE = /\$\{[!#]?[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*[A-Za-z_$]|(?:\[[^\]]*\])?:(?![-=+?])[^}]*[A-Za-z_$])/;
|
|
5
|
+
export const carriesSubstitution = (text) => { const t = text.replace(/\\\n/g, ""); return SUBSTITUTION_RE.test(t) || SUBSCRIPT_RE.test(t); };
|
|
6
|
+
const PER_ELEMENT_RE = /^\$@$|^\$\{!?@|\[@\]|^\$\{![^}]*@\}$/;
|
|
7
|
+
export const freshWord = () => ({ text: "", raw: "", expands: false, started: false, braceOpen: false, braceExpands: false, bracketOpen: false, substitution: false });
|
|
8
|
+
export function readDoubleQuoted(s, i, word) {
|
|
9
|
+
word.started = true;
|
|
10
|
+
while (i < s.length) {
|
|
11
|
+
const ch = s[i];
|
|
12
|
+
if (ch === '"')
|
|
13
|
+
return i + 1;
|
|
14
|
+
if (ch === "\\") {
|
|
15
|
+
const next = s[i + 1];
|
|
16
|
+
if (next === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (next === "\n") {
|
|
19
|
+
i += 2;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if ('$`"\\'.includes(next)) {
|
|
23
|
+
word.text += next;
|
|
24
|
+
i += 2;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
word.text += ch;
|
|
28
|
+
i++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === "$" || ch === "`") {
|
|
32
|
+
const end = readExpansion(s, i, word, true);
|
|
33
|
+
if (end === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (end === i + 1) {
|
|
36
|
+
word.text += ch;
|
|
37
|
+
i = end;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (PER_ELEMENT_RE.test(s.slice(i, end).replace(/\\\n/g, "")))
|
|
41
|
+
word.expands = "many";
|
|
42
|
+
else if (word.expands === false)
|
|
43
|
+
word.expands = "one";
|
|
44
|
+
i = end;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
word.text += ch;
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
export function readExpansion(s, i, word, inDoubleQuotes = false) {
|
|
53
|
+
if (s[i] === "`") {
|
|
54
|
+
word.substitution = true;
|
|
55
|
+
for (let j = i + 1; j < s.length; j += s[j] === "\\" ? 2 : 1)
|
|
56
|
+
if (s[j] === "`")
|
|
57
|
+
return j + 1;
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
let o = i + 1;
|
|
61
|
+
while (s[o] === "\\" && s[o + 1] === "\n")
|
|
62
|
+
o += 2;
|
|
63
|
+
const next = s[o];
|
|
64
|
+
if (next === undefined)
|
|
65
|
+
return i + 1;
|
|
66
|
+
if (next === "(") {
|
|
67
|
+
const end = skipBalanced(s, o, "(", ")");
|
|
68
|
+
if (end === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (s[i] !== "$" || s[o + 1] !== "(" || arithmeticHidesRun(s.slice(o + 2, end - 2)))
|
|
71
|
+
word.substitution = true;
|
|
72
|
+
return end;
|
|
73
|
+
}
|
|
74
|
+
if (next === "[") {
|
|
75
|
+
const end = s.indexOf("]", o);
|
|
76
|
+
word.substitution = true;
|
|
77
|
+
return end < 0 ? undefined : end + 1;
|
|
78
|
+
}
|
|
79
|
+
if (next === "{") {
|
|
80
|
+
const end = skipBalanced(s, o, "{", "}");
|
|
81
|
+
if (end !== undefined && carriesSubstitution(s.slice(i, end)))
|
|
82
|
+
word.substitution = true;
|
|
83
|
+
return end;
|
|
84
|
+
}
|
|
85
|
+
if (next === "'" && !inDoubleQuotes) {
|
|
86
|
+
let j = o + 1;
|
|
87
|
+
while (j < s.length && s[j] !== "'")
|
|
88
|
+
j += s[j] === "\\" ? 2 : 1;
|
|
89
|
+
return j < s.length ? j + 1 : undefined;
|
|
90
|
+
}
|
|
91
|
+
if (/[A-Za-z_]/.test(next)) {
|
|
92
|
+
let j = o + 1;
|
|
93
|
+
while (j < s.length && /[A-Za-z0-9_]/.test(s[j]))
|
|
94
|
+
j++;
|
|
95
|
+
return j;
|
|
96
|
+
}
|
|
97
|
+
return SPECIAL_PARAMS.includes(next) ? o + 1 : i + 1;
|
|
98
|
+
}
|
|
99
|
+
export function skipBalanced(s, i, open, close) {
|
|
100
|
+
const stack = [];
|
|
101
|
+
for (let j = i; j < s.length; j++) {
|
|
102
|
+
const ch = s[j];
|
|
103
|
+
const top = stack[stack.length - 1];
|
|
104
|
+
if (ch === "\\") {
|
|
105
|
+
j++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (top?.quote === true) {
|
|
109
|
+
if (ch === '"')
|
|
110
|
+
top.quote = false;
|
|
111
|
+
else if (ch === "$" && (s[j + 1] === "(" || s[j + 1] === "{")) {
|
|
112
|
+
stack.push({ closer: s[j + 1] === "(" ? ")" : "}", quote: false });
|
|
113
|
+
j++;
|
|
114
|
+
}
|
|
115
|
+
else if (ch === "`") {
|
|
116
|
+
const k = s.indexOf("`", j + 1);
|
|
117
|
+
if (k < 0)
|
|
118
|
+
return undefined;
|
|
119
|
+
j = k;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === "'") {
|
|
124
|
+
const k = s.indexOf("'", j + 1);
|
|
125
|
+
if (k < 0)
|
|
126
|
+
return undefined;
|
|
127
|
+
j = k;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (ch === '"') {
|
|
131
|
+
if (top !== undefined)
|
|
132
|
+
top.quote = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === "`") {
|
|
136
|
+
const k = s.indexOf("`", j + 1);
|
|
137
|
+
if (k < 0)
|
|
138
|
+
return undefined;
|
|
139
|
+
j = k;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (ch === open || ch === "(" || ch === "{") {
|
|
143
|
+
stack.push({ closer: ch === "(" ? ")" : "}", quote: false });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (ch === close || ch === ")" || ch === "}") {
|
|
147
|
+
if (stack.pop()?.closer !== ch)
|
|
148
|
+
return undefined;
|
|
149
|
+
if (stack.length === 0)
|
|
150
|
+
return j + 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
export function skipHeredocBodies(s, i, docs) {
|
|
156
|
+
let substitution = false;
|
|
157
|
+
for (const { delimiter, quoted, dash } of docs) {
|
|
158
|
+
for (;;) {
|
|
159
|
+
if (i >= s.length)
|
|
160
|
+
return undefined;
|
|
161
|
+
let eol = s.indexOf("\n", i);
|
|
162
|
+
let line = s.slice(i, eol < 0 ? s.length : eol);
|
|
163
|
+
while (!quoted && eol >= 0 && (/\\+$/.exec(line)?.[0].length ?? 0) % 2 === 1) {
|
|
164
|
+
const next = s.indexOf("\n", eol + 1);
|
|
165
|
+
line = line.slice(0, -1) + s.slice(eol + 1, next < 0 ? s.length : next);
|
|
166
|
+
eol = next;
|
|
167
|
+
}
|
|
168
|
+
i = eol < 0 ? s.length : eol + 1;
|
|
169
|
+
if ((dash ? line.replace(/^\t+/, "") : line) === delimiter)
|
|
170
|
+
break;
|
|
171
|
+
if (!quoted && carriesSubstitution(line))
|
|
172
|
+
substitution = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { end: i, substitution };
|
|
176
|
+
}
|
|
177
|
+
export function matchOp(s, i, ops) {
|
|
178
|
+
let best;
|
|
179
|
+
for (const op of ops)
|
|
180
|
+
if (s.startsWith(op, i) && (best === undefined || op.length > best.length))
|
|
181
|
+
best = op;
|
|
182
|
+
return best;
|
|
183
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wrapper programs the tightening reader ({@link import("./shell-lexer.js").readShellCommand}) peels to
|
|
3
|
+
* reach the program they run — upstream's three tables (its light strip on every arm, its full strip on
|
|
4
|
+
* the deny/ask arms, and its `xargs` clause inlined in the matcher) as ONE closed table. Peeling is
|
|
5
|
+
* ADDITIVE on the reading side: the segment keeps its spelled argv as one run candidate and adds one
|
|
6
|
+
* candidate per peeled layer, so a deny on the wrapper itself (`Bash(sudo:*)`) and a deny on the wrapped
|
|
7
|
+
* program both reach.
|
|
8
|
+
*
|
|
9
|
+
* The option grammar of every row is CLOSED, and that is the whole safety argument of the peel: a
|
|
10
|
+
* wrapper option the row does not name — short or long — stops the peel with an `unreadable` word (the
|
|
11
|
+
* caller asks a person) instead of guessing its arity, because a wrong guess names the option's VALUE
|
|
12
|
+
* or an operand as the program — a phantom run no deny matches. Three adversarial rounds found exactly
|
|
13
|
+
* that shape behind every "unknown is value-less" reading; the closed sets end the family rather than
|
|
14
|
+
* the instance. Growing a row is a data edit with upstream's manual page as its source.
|
|
15
|
+
*
|
|
16
|
+
* Per row: `valueFlags` — options whose value is the NEXT word (`-u root`; the attached `-uroot` and
|
|
17
|
+
* `--user=root` spellings are one word and need no row); `flags` — value-less options, including those
|
|
18
|
+
* whose value may only be ATTACHED (`--default-signal[=SIG]`, `xargs -i[R]`: bare, they take nothing);
|
|
19
|
+
* `attached` — short options whose OPTIONAL value may only be attached (`xargs -i[R]`: the rest of the cluster
|
|
20
|
+
* is the value, the next word never is); `commandString` — options whose value is a whole COMMAND STRING (`env -S 'rm -r x'`), unreadable here;
|
|
21
|
+
* `positional` — an operand that must precede the command (a priority, a cpu mask, a lock file; a word
|
|
22
|
+
* that does not match is unreadable, never the program); `duration` — `timeout`'s duration word;
|
|
23
|
+
* `bareDash` — `env -` (the obsolete spelling of `-i`); `assignments` — the wrapper takes `NAME=value`
|
|
24
|
+
* operands before its command (`env`, `sudo`; read dequoted, `env 'FOO=1' cmd`); `viaShell` — the program EVALUATES its
|
|
25
|
+
* operands as shell input (a command string, a script file, an expression: `watch`, `script`, `eval`,
|
|
26
|
+
* `source`, `bash -c`, `trap`, `let`), so the program behind it is never readable from the words — the
|
|
27
|
+
* string is not split and judged (upstream splits on whitespace; this reader takes the strict word);
|
|
28
|
+
* `executes` — a program that is NOT a wrapper but runs a command named after one of these words ANYWHERE
|
|
29
|
+
* in its argv (`find … -exec rm -r {} +`): such a word, or any expansion among its arguments (it could
|
|
30
|
+
* expand to that word), makes the program behind it unreadable; the program's own run is still read.
|
|
31
|
+
*/
|
|
32
|
+
export declare const SHELL_WRAPPER_TABLE: {
|
|
33
|
+
readonly command: {
|
|
34
|
+
readonly valueFlags: readonly [];
|
|
35
|
+
readonly flags: readonly ["-p"];
|
|
36
|
+
readonly commandString: readonly [];
|
|
37
|
+
};
|
|
38
|
+
readonly builtin: {
|
|
39
|
+
readonly valueFlags: readonly [];
|
|
40
|
+
readonly flags: readonly [];
|
|
41
|
+
readonly commandString: readonly [];
|
|
42
|
+
};
|
|
43
|
+
readonly noglob: {
|
|
44
|
+
readonly valueFlags: readonly [];
|
|
45
|
+
readonly flags: readonly [];
|
|
46
|
+
readonly commandString: readonly [];
|
|
47
|
+
};
|
|
48
|
+
readonly nocorrect: {
|
|
49
|
+
readonly valueFlags: readonly [];
|
|
50
|
+
readonly flags: readonly [];
|
|
51
|
+
readonly commandString: readonly [];
|
|
52
|
+
};
|
|
53
|
+
readonly exec: {
|
|
54
|
+
readonly valueFlags: readonly ["-a"];
|
|
55
|
+
readonly flags: readonly ["-c", "-l"];
|
|
56
|
+
readonly commandString: readonly [];
|
|
57
|
+
};
|
|
58
|
+
readonly env: {
|
|
59
|
+
readonly valueFlags: readonly ["-u", "-C", "-a", "-P", "--unset", "--chdir", "--argv0"];
|
|
60
|
+
readonly flags: readonly ["-i", "-0", "-v", "--ignore-environment", "--null", "--debug", "--list-signal-handling", "--default-signal", "--ignore-signal", "--block-signal", "--help", "--version"];
|
|
61
|
+
readonly commandString: readonly ["-S", "--split-string"];
|
|
62
|
+
readonly bareDash: true;
|
|
63
|
+
readonly assignments: true;
|
|
64
|
+
};
|
|
65
|
+
readonly sudo: {
|
|
66
|
+
readonly valueFlags: readonly ["-u", "-g", "-U", "-C", "-D", "-h", "-p", "-r", "-R", "-t", "-T", "--user", "--group", "--other-user", "--close-from", "--chdir", "--host", "--prompt", "--role", "--chroot", "--type", "--command-timeout", "-a", "--auth-type"];
|
|
67
|
+
readonly flags: readonly ["-A", "-b", "-E", "-H", "-i", "-K", "-k", "-l", "-n", "-P", "-S", "-s", "-V", "-v", "--askpass", "--background", "--preserve-env", "--set-home", "--login", "--remove-timestamp", "--reset-timestamp", "--list", "--non-interactive", "--preserve-groups", "--stdin", "--shell", "--version", "--validate", "--bell", "--help"];
|
|
68
|
+
readonly commandString: readonly [];
|
|
69
|
+
readonly assignments: true;
|
|
70
|
+
};
|
|
71
|
+
readonly doas: {
|
|
72
|
+
readonly valueFlags: readonly ["-a", "-u", "-C"];
|
|
73
|
+
readonly flags: readonly ["-L", "-n", "-s"];
|
|
74
|
+
readonly commandString: readonly [];
|
|
75
|
+
};
|
|
76
|
+
readonly pkexec: {
|
|
77
|
+
readonly valueFlags: readonly ["--user"];
|
|
78
|
+
readonly flags: readonly ["--disable-internal-agent", "--keep-cwd", "--version", "--help"];
|
|
79
|
+
readonly commandString: readonly [];
|
|
80
|
+
};
|
|
81
|
+
readonly nohup: {
|
|
82
|
+
readonly valueFlags: readonly [];
|
|
83
|
+
readonly flags: readonly ["--help", "--version"];
|
|
84
|
+
readonly commandString: readonly [];
|
|
85
|
+
};
|
|
86
|
+
readonly time: {
|
|
87
|
+
readonly valueFlags: readonly ["-o", "--output", "-f", "--format"];
|
|
88
|
+
readonly flags: readonly ["-p", "-a", "-v", "-q", "--append", "--portability", "--verbose", "--quiet", "--help", "--version"];
|
|
89
|
+
readonly commandString: readonly [];
|
|
90
|
+
};
|
|
91
|
+
readonly nice: {
|
|
92
|
+
readonly valueFlags: readonly ["-n", "--adjustment"];
|
|
93
|
+
readonly flags: readonly ["--help", "--version"];
|
|
94
|
+
readonly commandString: readonly [];
|
|
95
|
+
};
|
|
96
|
+
readonly timeout: {
|
|
97
|
+
readonly valueFlags: readonly ["-k", "-s", "--kill-after", "--signal"];
|
|
98
|
+
readonly flags: readonly ["-v", "--foreground", "--preserve-status", "--verbose", "--help", "--version"];
|
|
99
|
+
readonly commandString: readonly [];
|
|
100
|
+
readonly duration: true;
|
|
101
|
+
};
|
|
102
|
+
readonly stdbuf: {
|
|
103
|
+
readonly valueFlags: readonly ["-i", "-o", "-e", "--input", "--output", "--error"];
|
|
104
|
+
readonly flags: readonly ["--help", "--version"];
|
|
105
|
+
readonly commandString: readonly [];
|
|
106
|
+
};
|
|
107
|
+
readonly xargs: {
|
|
108
|
+
readonly valueFlags: readonly ["-n", "-L", "-P", "-I", "-d", "-s", "-a", "-E", "--max-args", "--max-procs", "--delimiter", "--max-chars", "--arg-file", "--process-slot-var"];
|
|
109
|
+
readonly flags: readonly ["-0", "-r", "-t", "-x", "-p", "-o", "--null", "--no-run-if-empty", "--verbose", "--exit", "--interactive", "--open-tty", "--show-limits", "--help", "--version", "--replace", "--eof", "--max-lines"];
|
|
110
|
+
readonly attached: readonly ["-i", "-l"];
|
|
111
|
+
readonly commandString: readonly [];
|
|
112
|
+
};
|
|
113
|
+
readonly watch: {
|
|
114
|
+
readonly valueFlags: readonly [];
|
|
115
|
+
readonly flags: readonly [];
|
|
116
|
+
readonly commandString: readonly [];
|
|
117
|
+
readonly viaShell: true;
|
|
118
|
+
};
|
|
119
|
+
readonly ionice: {
|
|
120
|
+
readonly valueFlags: readonly ["-c", "-n", "-p", "-P", "-u", "--class", "--classdata", "--pid", "--pgid", "--uid"];
|
|
121
|
+
readonly flags: readonly ["-t", "--ignore", "-h", "--help", "-V", "--version"];
|
|
122
|
+
readonly commandString: readonly [];
|
|
123
|
+
};
|
|
124
|
+
readonly setsid: {
|
|
125
|
+
readonly valueFlags: readonly [];
|
|
126
|
+
readonly flags: readonly ["-c", "-f", "-w", "--ctty", "--fork", "--wait", "--help", "--version"];
|
|
127
|
+
readonly commandString: readonly [];
|
|
128
|
+
};
|
|
129
|
+
readonly taskset: {
|
|
130
|
+
readonly valueFlags: readonly [];
|
|
131
|
+
readonly flags: readonly ["-a", "-p", "-c", "--all-tasks", "--pid", "--cpu-list", "--help", "--version"];
|
|
132
|
+
readonly commandString: readonly [];
|
|
133
|
+
readonly positional: RegExp;
|
|
134
|
+
};
|
|
135
|
+
readonly chrt: {
|
|
136
|
+
readonly valueFlags: readonly ["-p", "--pid", "-T", "-P", "-D", "--sched-runtime", "--sched-period", "--sched-deadline"];
|
|
137
|
+
readonly flags: readonly ["-b", "-f", "-i", "-o", "-r", "-d", "-m", "-a", "-R", "-v", "--batch", "--fifo", "--idle", "--other", "--rr", "--deadline", "--max", "--all-tasks", "--reset-on-fork", "--verbose", "--help", "--version"];
|
|
138
|
+
readonly commandString: readonly [];
|
|
139
|
+
readonly positional: RegExp;
|
|
140
|
+
};
|
|
141
|
+
readonly strace: {
|
|
142
|
+
readonly valueFlags: readonly ["-e", "-o", "-p", "-s", "-E", "-P", "-S", "-a", "-b", "-I", "-u", "-X", "-O", "-U", "--output", "--trace", "--expr", "--attach", "--string-limit", "--env", "--trace-path", "--columns", "--user", "--interruptible", "--detach-on", "--const-print-style", "--summary-sort-by", "--summary-syscall-overhead", "--summary-columns"];
|
|
143
|
+
readonly flags: readonly ["-f", "-ff", "-c", "-C", "-d", "-D", "-h", "-i", "-k", "-q", "-qq", "-r", "-t", "-tt", "-ttt", "-T", "-v", "-V", "-w", "-x", "-xx", "-y", "-yy", "-z", "-Z", "-F", "--follow-forks", "--summary-only", "--summary", "--debug", "--help", "--version"];
|
|
144
|
+
readonly commandString: readonly [];
|
|
145
|
+
};
|
|
146
|
+
readonly ltrace: {
|
|
147
|
+
readonly valueFlags: readonly ["-a", "-A", "-e", "-l", "-n", "-o", "-p", "-s", "-u", "-x", "-D", "-F", "--align", "--config", "--debug", "--indent", "--library", "--output", "--string-max", "-w", "--where"];
|
|
148
|
+
readonly flags: readonly ["-c", "-C", "-f", "-i", "-L", "-r", "-S", "-t", "-tt", "-ttt", "-T", "-V", "--demangle", "--help", "--version"];
|
|
149
|
+
readonly commandString: readonly [];
|
|
150
|
+
};
|
|
151
|
+
readonly flock: {
|
|
152
|
+
readonly valueFlags: readonly ["-w", "-E", "--timeout", "--wait", "--conflict-exit-code"];
|
|
153
|
+
readonly flags: readonly ["-s", "-x", "-u", "-n", "-o", "-F", "--shared", "--exclusive", "--unlock", "--nonblock", "--nb", "--close", "--no-fork", "--verbose", "--help", "--version"];
|
|
154
|
+
readonly commandString: readonly ["-c", "--command"];
|
|
155
|
+
readonly positional: RegExp;
|
|
156
|
+
};
|
|
157
|
+
readonly script: {
|
|
158
|
+
readonly valueFlags: readonly [];
|
|
159
|
+
readonly flags: readonly [];
|
|
160
|
+
readonly commandString: readonly ["-c", "--command"];
|
|
161
|
+
readonly viaShell: true;
|
|
162
|
+
};
|
|
163
|
+
readonly unshare: {
|
|
164
|
+
readonly valueFlags: readonly ["-R", "-w", "-S", "-G", "--setuid", "--setgid", "--root", "--wd", "--propagation", "--setgroups", "--monotonic", "--boottime", "--map-user", "--map-group"];
|
|
165
|
+
readonly flags: readonly ["-m", "-u", "-i", "-n", "-p", "-U", "-C", "-T", "-f", "-r", "-c", "--mount", "--uts", "--ipc", "--net", "--pid", "--user", "--cgroup", "--time", "--fork", "--map-root-user", "--map-current-user", "--kill-child", "--mount-proc", "--keep-caps", "--help", "--version"];
|
|
166
|
+
readonly commandString: readonly [];
|
|
167
|
+
};
|
|
168
|
+
readonly nsenter: {
|
|
169
|
+
readonly valueFlags: readonly ["-t", "-S", "-G", "--target", "--setuid", "--setgid"];
|
|
170
|
+
readonly flags: readonly ["-a", "-m", "-u", "-i", "-n", "-p", "-U", "-C", "-T", "-r", "-w", "-F", "-Z", "--all", "--mount", "--uts", "--ipc", "--net", "--pid", "--user", "--cgroup", "--time", "--root", "--wd", "--no-fork", "--preserve-credentials", "--help", "--version"];
|
|
171
|
+
readonly commandString: readonly [];
|
|
172
|
+
};
|
|
173
|
+
readonly eval: {
|
|
174
|
+
readonly valueFlags: readonly [];
|
|
175
|
+
readonly flags: readonly [];
|
|
176
|
+
readonly commandString: readonly [];
|
|
177
|
+
readonly viaShell: true;
|
|
178
|
+
};
|
|
179
|
+
readonly source: {
|
|
180
|
+
readonly valueFlags: readonly [];
|
|
181
|
+
readonly flags: readonly [];
|
|
182
|
+
readonly commandString: readonly [];
|
|
183
|
+
readonly viaShell: true;
|
|
184
|
+
};
|
|
185
|
+
readonly ".": {
|
|
186
|
+
readonly valueFlags: readonly [];
|
|
187
|
+
readonly flags: readonly [];
|
|
188
|
+
readonly commandString: readonly [];
|
|
189
|
+
readonly viaShell: true;
|
|
190
|
+
};
|
|
191
|
+
readonly trap: {
|
|
192
|
+
readonly valueFlags: readonly [];
|
|
193
|
+
readonly flags: readonly [];
|
|
194
|
+
readonly commandString: readonly [];
|
|
195
|
+
readonly viaShell: true;
|
|
196
|
+
};
|
|
197
|
+
readonly let: {
|
|
198
|
+
readonly valueFlags: readonly [];
|
|
199
|
+
readonly flags: readonly [];
|
|
200
|
+
readonly commandString: readonly [];
|
|
201
|
+
readonly viaShell: true;
|
|
202
|
+
};
|
|
203
|
+
readonly sh: {
|
|
204
|
+
readonly valueFlags: readonly [];
|
|
205
|
+
readonly flags: readonly [];
|
|
206
|
+
readonly commandString: readonly [];
|
|
207
|
+
readonly viaShell: true;
|
|
208
|
+
};
|
|
209
|
+
readonly bash: {
|
|
210
|
+
readonly valueFlags: readonly [];
|
|
211
|
+
readonly flags: readonly [];
|
|
212
|
+
readonly commandString: readonly [];
|
|
213
|
+
readonly viaShell: true;
|
|
214
|
+
};
|
|
215
|
+
readonly zsh: {
|
|
216
|
+
readonly valueFlags: readonly [];
|
|
217
|
+
readonly flags: readonly [];
|
|
218
|
+
readonly commandString: readonly [];
|
|
219
|
+
readonly viaShell: true;
|
|
220
|
+
};
|
|
221
|
+
readonly dash: {
|
|
222
|
+
readonly valueFlags: readonly [];
|
|
223
|
+
readonly flags: readonly [];
|
|
224
|
+
readonly commandString: readonly [];
|
|
225
|
+
readonly viaShell: true;
|
|
226
|
+
};
|
|
227
|
+
readonly ksh: {
|
|
228
|
+
readonly valueFlags: readonly [];
|
|
229
|
+
readonly flags: readonly [];
|
|
230
|
+
readonly commandString: readonly [];
|
|
231
|
+
readonly viaShell: true;
|
|
232
|
+
};
|
|
233
|
+
readonly fish: {
|
|
234
|
+
readonly valueFlags: readonly [];
|
|
235
|
+
readonly flags: readonly [];
|
|
236
|
+
readonly commandString: readonly [];
|
|
237
|
+
readonly viaShell: true;
|
|
238
|
+
};
|
|
239
|
+
readonly csh: {
|
|
240
|
+
readonly valueFlags: readonly [];
|
|
241
|
+
readonly flags: readonly [];
|
|
242
|
+
readonly commandString: readonly [];
|
|
243
|
+
readonly viaShell: true;
|
|
244
|
+
};
|
|
245
|
+
readonly tcsh: {
|
|
246
|
+
readonly valueFlags: readonly [];
|
|
247
|
+
readonly flags: readonly [];
|
|
248
|
+
readonly commandString: readonly [];
|
|
249
|
+
readonly viaShell: true;
|
|
250
|
+
};
|
|
251
|
+
readonly find: {
|
|
252
|
+
readonly valueFlags: readonly [];
|
|
253
|
+
readonly flags: readonly [];
|
|
254
|
+
readonly commandString: readonly [];
|
|
255
|
+
readonly executes: readonly ["-exec", "-execdir", "-ok", "-okdir"];
|
|
256
|
+
};
|
|
257
|
+
readonly compgen: {
|
|
258
|
+
readonly valueFlags: readonly [];
|
|
259
|
+
readonly flags: readonly [];
|
|
260
|
+
readonly commandString: readonly [];
|
|
261
|
+
readonly executes: readonly ["-C", "-F"];
|
|
262
|
+
};
|
|
263
|
+
readonly complete: {
|
|
264
|
+
readonly valueFlags: readonly [];
|
|
265
|
+
readonly flags: readonly [];
|
|
266
|
+
readonly commandString: readonly [];
|
|
267
|
+
readonly executes: readonly ["-C", "-F"];
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
export type ShellWrapperName = keyof typeof SHELL_WRAPPER_TABLE;
|
|
271
|
+
/** The shape of one row (see the table's contract). */
|
|
272
|
+
export interface ShellWrapperRow {
|
|
273
|
+
readonly valueFlags: readonly string[];
|
|
274
|
+
readonly flags: readonly string[];
|
|
275
|
+
readonly attached?: readonly string[];
|
|
276
|
+
readonly commandString: readonly string[];
|
|
277
|
+
readonly positional?: RegExp;
|
|
278
|
+
readonly duration?: true;
|
|
279
|
+
readonly bareDash?: true;
|
|
280
|
+
readonly assignments?: true;
|
|
281
|
+
readonly viaShell?: true;
|
|
282
|
+
readonly executes?: readonly string[];
|
|
283
|
+
}
|
|
284
|
+
/** Builtins whose OPERANDS name variables: an operand spelling an array element with a non-numeric
|
|
285
|
+
* subscript (`arr[a]`, quoted, bare, attached `-varr[a]`, or `arr[a]=1`) is evaluated as arithmetic —
|
|
286
|
+
* recursively, like `$((a))` — when the builtin reads or writes it (`printf -v 'arr[a]' …`, `unset arr[a]`,
|
|
287
|
+
* `declare arr[a]=1`, `read arr[a]`, `[[ -v arr[a] ]]`, `test -v arr[a]`), and so is the value of an
|
|
288
|
+
* INTEGER-attribute assignment (`declare -i x=a`). The lexer marks such a run's hidden run at every peel
|
|
289
|
+
* candidate (`command printf -v …`). An operand that is WHOLLY an expansion is not judged here (`unset "$X"`
|
|
290
|
+
* — one more indirection; the price of reading it would be a prompt on every `export FOO="$BAR"`), nor is
|
|
291
|
+
* an arithmetic comparison's operand (`[[ $n -gt 0 ]]` evaluates `$n` — the same price). */
|
|
292
|
+
export declare const VARIABLE_OPERAND_BUILTINS: ReadonlySet<string>;
|
|
293
|
+
/** Does this run of a variable-operand builtin evaluate an operand arithmetically? Linear in the words. */
|
|
294
|
+
export declare function evaluatesOperand(run: readonly {
|
|
295
|
+
readonly text: string;
|
|
296
|
+
readonly raw: string;
|
|
297
|
+
}[]): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const SHELL_WRAPPER_TABLE = {
|
|
2
|
+
command: { valueFlags: [], flags: ["-p"], commandString: [] },
|
|
3
|
+
builtin: { valueFlags: [], flags: [], commandString: [] },
|
|
4
|
+
noglob: { valueFlags: [], flags: [], commandString: [] },
|
|
5
|
+
nocorrect: { valueFlags: [], flags: [], commandString: [] },
|
|
6
|
+
exec: { valueFlags: ["-a"], flags: ["-c", "-l"], commandString: [] },
|
|
7
|
+
env: { valueFlags: ["-u", "-C", "-a", "-P", "--unset", "--chdir", "--argv0"], flags: ["-i", "-0", "-v", "--ignore-environment", "--null", "--debug", "--list-signal-handling", "--default-signal", "--ignore-signal", "--block-signal", "--help", "--version"], commandString: ["-S", "--split-string"], bareDash: true, assignments: true },
|
|
8
|
+
sudo: { valueFlags: ["-u", "-g", "-U", "-C", "-D", "-h", "-p", "-r", "-R", "-t", "-T", "--user", "--group", "--other-user", "--close-from", "--chdir", "--host", "--prompt", "--role", "--chroot", "--type", "--command-timeout", "-a", "--auth-type"], flags: ["-A", "-b", "-E", "-H", "-i", "-K", "-k", "-l", "-n", "-P", "-S", "-s", "-V", "-v", "--askpass", "--background", "--preserve-env", "--set-home", "--login", "--remove-timestamp", "--reset-timestamp", "--list", "--non-interactive", "--preserve-groups", "--stdin", "--shell", "--version", "--validate", "--bell", "--help"], commandString: [], assignments: true },
|
|
9
|
+
doas: { valueFlags: ["-a", "-u", "-C"], flags: ["-L", "-n", "-s"], commandString: [] },
|
|
10
|
+
pkexec: { valueFlags: ["--user"], flags: ["--disable-internal-agent", "--keep-cwd", "--version", "--help"], commandString: [] },
|
|
11
|
+
nohup: { valueFlags: [], flags: ["--help", "--version"], commandString: [] },
|
|
12
|
+
time: { valueFlags: ["-o", "--output", "-f", "--format"], flags: ["-p", "-a", "-v", "-q", "--append", "--portability", "--verbose", "--quiet", "--help", "--version"], commandString: [] },
|
|
13
|
+
nice: { valueFlags: ["-n", "--adjustment"], flags: ["--help", "--version"], commandString: [] },
|
|
14
|
+
timeout: { valueFlags: ["-k", "-s", "--kill-after", "--signal"], flags: ["-v", "--foreground", "--preserve-status", "--verbose", "--help", "--version"], commandString: [], duration: true },
|
|
15
|
+
stdbuf: { valueFlags: ["-i", "-o", "-e", "--input", "--output", "--error"], flags: ["--help", "--version"], commandString: [] },
|
|
16
|
+
xargs: { valueFlags: ["-n", "-L", "-P", "-I", "-d", "-s", "-a", "-E", "--max-args", "--max-procs", "--delimiter", "--max-chars", "--arg-file", "--process-slot-var"], flags: ["-0", "-r", "-t", "-x", "-p", "-o", "--null", "--no-run-if-empty", "--verbose", "--exit", "--interactive", "--open-tty", "--show-limits", "--help", "--version", "--replace", "--eof", "--max-lines"], attached: ["-i", "-l"], commandString: [] },
|
|
17
|
+
watch: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
18
|
+
ionice: { valueFlags: ["-c", "-n", "-p", "-P", "-u", "--class", "--classdata", "--pid", "--pgid", "--uid"], flags: ["-t", "--ignore", "-h", "--help", "-V", "--version"], commandString: [] },
|
|
19
|
+
setsid: { valueFlags: [], flags: ["-c", "-f", "-w", "--ctty", "--fork", "--wait", "--help", "--version"], commandString: [] },
|
|
20
|
+
taskset: { valueFlags: [], flags: ["-a", "-p", "-c", "--all-tasks", "--pid", "--cpu-list", "--help", "--version"], commandString: [], positional: /^(?:0x)?[\da-f]+$|^[\d,:-]+$/i },
|
|
21
|
+
chrt: { valueFlags: ["-p", "--pid", "-T", "-P", "-D", "--sched-runtime", "--sched-period", "--sched-deadline"], flags: ["-b", "-f", "-i", "-o", "-r", "-d", "-m", "-a", "-R", "-v", "--batch", "--fifo", "--idle", "--other", "--rr", "--deadline", "--max", "--all-tasks", "--reset-on-fork", "--verbose", "--help", "--version"], commandString: [], positional: /^\d+$/ },
|
|
22
|
+
strace: { valueFlags: ["-e", "-o", "-p", "-s", "-E", "-P", "-S", "-a", "-b", "-I", "-u", "-X", "-O", "-U", "--output", "--trace", "--expr", "--attach", "--string-limit", "--env", "--trace-path", "--columns", "--user", "--interruptible", "--detach-on", "--const-print-style", "--summary-sort-by", "--summary-syscall-overhead", "--summary-columns"], flags: ["-f", "-ff", "-c", "-C", "-d", "-D", "-h", "-i", "-k", "-q", "-qq", "-r", "-t", "-tt", "-ttt", "-T", "-v", "-V", "-w", "-x", "-xx", "-y", "-yy", "-z", "-Z", "-F", "--follow-forks", "--summary-only", "--summary", "--debug", "--help", "--version"], commandString: [] },
|
|
23
|
+
ltrace: { valueFlags: ["-a", "-A", "-e", "-l", "-n", "-o", "-p", "-s", "-u", "-x", "-D", "-F", "--align", "--config", "--debug", "--indent", "--library", "--output", "--string-max", "-w", "--where"], flags: ["-c", "-C", "-f", "-i", "-L", "-r", "-S", "-t", "-tt", "-ttt", "-T", "-V", "--demangle", "--help", "--version"], commandString: [] },
|
|
24
|
+
flock: { valueFlags: ["-w", "-E", "--timeout", "--wait", "--conflict-exit-code"], flags: ["-s", "-x", "-u", "-n", "-o", "-F", "--shared", "--exclusive", "--unlock", "--nonblock", "--nb", "--close", "--no-fork", "--verbose", "--help", "--version"], commandString: ["-c", "--command"], positional: /^/ },
|
|
25
|
+
script: { valueFlags: [], flags: [], commandString: ["-c", "--command"], viaShell: true },
|
|
26
|
+
unshare: { valueFlags: ["-R", "-w", "-S", "-G", "--setuid", "--setgid", "--root", "--wd", "--propagation", "--setgroups", "--monotonic", "--boottime", "--map-user", "--map-group"], flags: ["-m", "-u", "-i", "-n", "-p", "-U", "-C", "-T", "-f", "-r", "-c", "--mount", "--uts", "--ipc", "--net", "--pid", "--user", "--cgroup", "--time", "--fork", "--map-root-user", "--map-current-user", "--kill-child", "--mount-proc", "--keep-caps", "--help", "--version"], commandString: [] },
|
|
27
|
+
nsenter: { valueFlags: ["-t", "-S", "-G", "--target", "--setuid", "--setgid"], flags: ["-a", "-m", "-u", "-i", "-n", "-p", "-U", "-C", "-T", "-r", "-w", "-F", "-Z", "--all", "--mount", "--uts", "--ipc", "--net", "--pid", "--user", "--cgroup", "--time", "--root", "--wd", "--no-fork", "--preserve-credentials", "--help", "--version"], commandString: [] },
|
|
28
|
+
eval: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
29
|
+
source: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
30
|
+
".": { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
31
|
+
trap: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
32
|
+
let: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
33
|
+
sh: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
34
|
+
bash: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
35
|
+
zsh: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
36
|
+
dash: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
37
|
+
ksh: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
38
|
+
fish: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
39
|
+
csh: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
40
|
+
tcsh: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
41
|
+
find: { valueFlags: [], flags: [], commandString: [], executes: ["-exec", "-execdir", "-ok", "-okdir"] },
|
|
42
|
+
compgen: { valueFlags: [], flags: [], commandString: [], executes: ["-C", "-F"] },
|
|
43
|
+
complete: { valueFlags: [], flags: [], commandString: [], executes: ["-C", "-F"] },
|
|
44
|
+
};
|
|
45
|
+
export const VARIABLE_OPERAND_BUILTINS = new Set(["printf", "unset", "read", "mapfile", "readarray", "declare", "typeset", "local", "export", "readonly", "test", "[", "[["]);
|
|
46
|
+
export function evaluatesOperand(run) {
|
|
47
|
+
if (!VARIABLE_OPERAND_BUILTINS.has(run[0]?.text ?? ""))
|
|
48
|
+
return false;
|
|
49
|
+
const integer = run.some((w) => /^-[A-Za-z]*i/.test(w.text));
|
|
50
|
+
return run.some((w) => namesArrayElement(w.text) || namesArrayElement(w.raw) || (integer && /^[A-Za-z_][A-Za-z0-9_]*\+?=.*[A-Za-z_$]/.test(w.text)));
|
|
51
|
+
}
|
|
52
|
+
function namesArrayElement(word) {
|
|
53
|
+
const open = word.indexOf("[");
|
|
54
|
+
if (open <= 0 || !/[A-Za-z0-9_]/.test(word[open - 1]))
|
|
55
|
+
return false;
|
|
56
|
+
const close = word.indexOf("]", open);
|
|
57
|
+
return close > 0 && /[A-Za-z_$]/.test(word.slice(open + 1, close));
|
|
58
|
+
}
|
|
@@ -5,14 +5,15 @@ const GLOBAL = { kind: "global" };
|
|
|
5
5
|
function add(actor, counter, origin = "user") {
|
|
6
6
|
return { dot: { actor, counter }, origin, createdAt: "2026-01-01T00:00:00.000Z" };
|
|
7
7
|
}
|
|
8
|
-
function rule(text, adds, scope = GLOBAL) {
|
|
8
|
+
function rule(text, adds, scope = GLOBAL, behavior = "allow") {
|
|
9
9
|
const m = /^Bash\((.+?)(:\*)?\)$/.exec(text);
|
|
10
10
|
if (m === null || m[1] === undefined)
|
|
11
11
|
throw new Error(`contract fixture rule "${text}" is not a Bash(...) form`);
|
|
12
|
-
return { rule: text, tool: "Bash", match: m[2] !== undefined ? "prefix" : "exact", command: m[1], scope, adds };
|
|
12
|
+
return { behavior, rule: text, tool: "Bash", match: m[2] !== undefined ? "prefix" : "exact", command: m[1], scope, adds };
|
|
13
13
|
}
|
|
14
|
-
function tomb(text, removed, deletedBy, scope = GLOBAL) {
|
|
14
|
+
function tomb(text, removed, deletedBy, scope = GLOBAL, behavior = "allow") {
|
|
15
15
|
return {
|
|
16
|
+
behavior,
|
|
16
17
|
rule: text,
|
|
17
18
|
scope,
|
|
18
19
|
removedDots: removed.map(([actor, counter]) => ({ actor, counter })),
|
|
@@ -25,10 +26,10 @@ function state(rules = [], tombstones = []) {
|
|
|
25
26
|
function liveDots(s) {
|
|
26
27
|
const out = new Set();
|
|
27
28
|
for (const r of s.rules) {
|
|
28
|
-
const removed = s.tombstones.filter((t) => t.rule === r.rule && sameScope(t.scope, r.scope)).flatMap((t) => t.removedDots);
|
|
29
|
+
const removed = s.tombstones.filter((t) => t.behavior === r.behavior && t.rule === r.rule && sameScope(t.scope, r.scope)).flatMap((t) => t.removedDots);
|
|
29
30
|
for (const a of r.adds) {
|
|
30
31
|
if (!removed.some((d) => d.actor === a.dot.actor && d.counter === a.dot.counter))
|
|
31
|
-
out.add(`${r.rule}|${a.dot.actor}#${a.dot.counter}`);
|
|
32
|
+
out.add(`${r.behavior === "allow" ? "" : r.behavior + ":"}${r.rule}|${a.dot.actor}#${a.dot.counter}`);
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
return out;
|
|
@@ -41,7 +42,7 @@ function sameScope(a, b) {
|
|
|
41
42
|
return b.kind === "project" && a.root === b.root;
|
|
42
43
|
}
|
|
43
44
|
function allDots(s) {
|
|
44
|
-
return new Set(s.rules.flatMap((r) => r.adds.map((a) => `${r.rule}|${a.dot.actor}#${a.dot.counter}`)));
|
|
45
|
+
return new Set(s.rules.flatMap((r) => r.adds.map((a) => `${r.behavior === "allow" ? "" : r.behavior + ":"}${r.rule}|${a.dot.actor}#${a.dot.counter}`)));
|
|
45
46
|
}
|
|
46
47
|
export async function permissionRuleSyncContract(hooks = {}) {
|
|
47
48
|
const join = hooks.join ?? joinRuleStates;
|
|
@@ -141,8 +142,24 @@ export async function permissionRuleSyncContract(hooks = {}) {
|
|
|
141
142
|
assert.deepStrictEqual(new Set(ab.tombstones[0]?.removedDots.map((d) => `${d.actor}#${d.counter}`)), new Set(["a#1", "a#2"]), "the union — a one-sided pick can drop an observed removal");
|
|
142
143
|
assert.deepStrictEqual(ab, join(b, a));
|
|
143
144
|
});
|
|
145
|
+
run("behavior is an identity member: the same text as an allow and as a deny are two rows, each with its own adds and its own tombstone", async () => {
|
|
146
|
+
const both = state([rule("Bash(ls)", [add("a", 1)]), rule("Bash(ls)", [add("a", 2)], GLOBAL, "deny")], [tomb("Bash(ls)", [["a", 1]], ["a", 3])]);
|
|
147
|
+
const merged = join(both, state());
|
|
148
|
+
assert.strictEqual(merged.rules.length, 2, "one text, two behaviors — two identity groups, never folded");
|
|
149
|
+
assert.deepStrictEqual(liveDots(merged), new Set(["deny:Bash(ls)|a#2"]), "the allow tombstone must not reach the deny's add");
|
|
150
|
+
assert.deepStrictEqual(join(state(), both), merged, "commutative across the behavior axis");
|
|
151
|
+
const badWord = state([rule("Bash(pwd)", [add("b", 1)])], [Object.assign(tomb("Bash(pwd)", [["b", 1]], ["b", 2]), { behavior: "maybe" })]);
|
|
152
|
+
const screened = join(badWord, state());
|
|
153
|
+
assert.strictEqual(screened.tombstones.length, 0, "an unreadable behavior word on a tombstone must not land");
|
|
154
|
+
assert.ok(liveDots(screened).has("Bash(pwd)|b#1"), "…and must not cover anything");
|
|
155
|
+
});
|
|
156
|
+
run("closed-set identity: a row carrying a behavior word outside the closed set is refused like invalid text", async () => {
|
|
157
|
+
const forgedWord = Object.assign(rule("Bash(ls)", [add("z", 1)]), { behavior: "sometimes" });
|
|
158
|
+
const merged = join(state([forgedWord]), state());
|
|
159
|
+
assert.strictEqual(allDots(merged).size, 0, "a behavior this engine cannot read must not become an allow (or a deny) by falling through");
|
|
160
|
+
});
|
|
144
161
|
run("metadata re-projection (r2-F2): a record whose stored fields contradict its rule text is refused, both orders", async () => {
|
|
145
|
-
const forged = { rule: "Bash(ls)", tool: "Bash", match: "prefix", command: "rm -rf /", scope: GLOBAL, adds: [add("z", 1)] };
|
|
162
|
+
const forged = { behavior: "allow", rule: "Bash(ls)", tool: "Bash", match: "prefix", command: "rm -rf /", scope: GLOBAL, adds: [add("z", 1)] };
|
|
146
163
|
const clean = state([rule("Bash(git status)", [add("a", 1)])]);
|
|
147
164
|
const ab = join(state([forged]), clean);
|
|
148
165
|
const ba = join(clean, state([forged]));
|
|
@@ -150,9 +167,11 @@ export async function permissionRuleSyncContract(hooks = {}) {
|
|
|
150
167
|
assert.deepStrictEqual(ab, ba, "refusal must preserve commutativity");
|
|
151
168
|
assert.ok(allDots(ab).has("Bash(git status)|a#1"), "refusing one record must not disturb the rest");
|
|
152
169
|
});
|
|
153
|
-
run("single-validator door: an illegal
|
|
170
|
+
run("single-validator door: an illegal ALLOW shape (bare-interpreter prefix) cannot ride a join — while the same text as a DENY is a legitimate wide tightening and lands", async () => {
|
|
154
171
|
const merged = join(state([rule("Bash(node:*)", [add("z", 1)])]), state());
|
|
155
172
|
assert.strictEqual(allDots(merged).size, 0, "Bash(node:*) must be refused by the shared validator inside the join");
|
|
173
|
+
const denied = join(state([rule("Bash(node:*)", [add("z", 1)], GLOBAL, "deny")]), state());
|
|
174
|
+
assert.deepStrictEqual(allDots(denied), new Set(["deny:Bash(node:*)|z#1"]), "the tightening floor admits the interpreter-headed prefix");
|
|
156
175
|
});
|
|
157
176
|
run("durable two-member face (design/382 §4.3): a session-scope row or tombstone never survives a join, and never poisons the round", async () => {
|
|
158
177
|
const sessionScope = { kind: "session", sessionId: "sess-1" };
|
|
@@ -168,8 +187,8 @@ export async function permissionRuleSyncContract(hooks = {}) {
|
|
|
168
187
|
const scopeA = { kind: "project", root: "/x" };
|
|
169
188
|
const scopeB = { kind: "project", root: "/x a" };
|
|
170
189
|
const s = state([rule("Bash(ls)", [add("v", 1)], scopeA), rule("Bash(ls)", [add("w", 1)], scopeB)], [
|
|
171
|
-
{ rule: "Bash(ls)", scope: scopeA, removedDots: [{ actor: "v", counter: 1 }], deletedBy: { actor: "a 1", counter: 2 } },
|
|
172
|
-
{ rule: "Bash(ls)", scope: scopeB, removedDots: [{ actor: "w", counter: 1 }], deletedBy: { actor: "1", counter: 2 } },
|
|
190
|
+
{ behavior: "allow", rule: "Bash(ls)", scope: scopeA, removedDots: [{ actor: "v", counter: 1 }], deletedBy: { actor: "a 1", counter: 2 } },
|
|
191
|
+
{ behavior: "allow", rule: "Bash(ls)", scope: scopeB, removedDots: [{ actor: "w", counter: 1 }], deletedBy: { actor: "1", counter: 2 } },
|
|
173
192
|
]);
|
|
174
193
|
const merged = join(s, state());
|
|
175
194
|
assert.strictEqual(merged.tombstones.length, 2, "two distinct delete intents must both survive — a colliding key folds one away");
|