@deftai/directive-core 0.106.0 → 0.107.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/dist/check/gate-lists.js +2 -0
- package/dist/design-critique/exclusive-chip.d.ts +33 -0
- package/dist/design-critique/exclusive-chip.js +68 -0
- package/dist/design-critique/parent-audit.d.ts +59 -0
- package/dist/design-critique/parent-audit.js +121 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +35 -0
- package/dist/hooks/dest-form.d.ts +46 -0
- package/dist/hooks/dest-form.js +575 -0
- package/dist/hooks/dispatcher.d.ts +17 -1
- package/dist/hooks/dispatcher.js +82 -11
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/intake/github-auth-modes-cli.js +14 -2
- package/dist/intake/github-auth-modes.d.ts +54 -9
- package/dist/intake/github-auth-modes.js +326 -123
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
- package/dist/lifecycle/completed-write-guard.d.ts +48 -0
- package/dist/lifecycle/completed-write-guard.js +373 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/capture.d.ts +16 -0
- package/dist/literal-acceptance/capture.js +75 -7
- package/dist/literal-acceptance/evaluate.js +25 -3
- package/dist/literal-acceptance/index.d.ts +2 -2
- package/dist/literal-acceptance/index.js +2 -2
- package/dist/literal-acceptance/run.js +9 -3
- package/dist/literal-acceptance/types.d.ts +6 -0
- package/dist/literal-acceptance/types.js +6 -0
- package/dist/policy/runtime-authority.d.ts +18 -0
- package/dist/policy/runtime-authority.js +11 -0
- package/dist/policy/write-fence.js +3 -0
- package/dist/preflight-cache/evaluate.js +9 -3
- package/dist/product-first-done-gate/evaluate.js +36 -5
- package/dist/scm/design-critique-chip.d.ts +37 -0
- package/dist/scm/design-critique-chip.js +171 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +3 -0
- package/dist/scm/main.js +15 -1
- package/dist/scm/readiness-cli.d.ts +2 -0
- package/dist/scm/readiness-cli.js +42 -1
- package/dist/scm/readiness.d.ts +2 -1
- package/dist/scm/readiness.js +7 -1
- package/dist/scope/decompose.js +2 -1
- package/dist/scope/delivery-evidence.d.ts +27 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/lifecycle-write.d.ts +24 -0
- package/dist/scope/lifecycle-write.js +64 -0
- package/dist/scope/main.js +3 -1
- package/dist/scope/transition.js +90 -11
- package/dist/scope-provenance/evaluate.js +5 -1
- package/dist/session/git.d.ts +18 -0
- package/dist/session/git.js +81 -1
- package/dist/session/occupancy.d.ts +2 -1
- package/dist/session/occupancy.js +11 -4
- package/dist/session/session-start.d.ts +12 -0
- package/dist/session/session-start.js +17 -0
- package/dist/session/verify-session-ritual.d.ts +22 -3
- package/dist/session/verify-session-ritual.js +29 -4
- package/dist/swarm/launch.d.ts +69 -0
- package/dist/swarm/launch.js +191 -1
- package/dist/triage/actions/index.d.ts +4 -0
- package/dist/triage/actions/index.js +6 -0
- package/dist/triage/evaluate/evaluate.d.ts +11 -0
- package/dist/triage/evaluate/evaluate.js +169 -0
- package/dist/triage/evaluate/github.d.ts +16 -0
- package/dist/triage/evaluate/github.js +153 -0
- package/dist/triage/evaluate/index.d.ts +9 -0
- package/dist/triage/evaluate/index.js +8 -0
- package/dist/triage/evaluate/paths.d.ts +7 -0
- package/dist/triage/evaluate/paths.js +25 -0
- package/dist/triage/evaluate/sink.d.ts +5 -0
- package/dist/triage/evaluate/sink.js +52 -0
- package/dist/triage/evaluate/types.d.ts +93 -0
- package/dist/triage/evaluate/types.js +13 -0
- package/dist/triage/evaluate/validity.d.ts +8 -0
- package/dist/triage/evaluate/validity.js +87 -0
- package/dist/triage/evaluate/value.d.ts +12 -0
- package/dist/triage/evaluate/value.js +33 -0
- package/dist/triage/evaluate/wip-census.d.ts +5 -0
- package/dist/triage/evaluate/wip-census.js +39 -0
- package/dist/triage/evaluate/worktrees.d.ts +8 -0
- package/dist/triage/evaluate/worktrees.js +68 -0
- package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
- package/dist/triage/evaluate/xbrief-refs.js +60 -0
- package/dist/triage/help/registry-data.d.ts +21 -7
- package/dist/triage/help/registry-data.js +53 -5
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/value/feedback-file.d.ts +17 -2
- package/dist/value/feedback-file.js +80 -6
- package/dist/vbrief-reconcile/labels.d.ts +1 -0
- package/dist/vbrief-reconcile/labels.js +30 -0
- package/dist/vbrief-validation/story-quality.d.ts +7 -0
- package/dist/vbrief-validation/story-quality.js +8 -1
- package/dist/xbrief-migrate/agents-header.js +69 -7
- package/package.json +3 -3
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product dest-form classifier for in-repo Shell mutations (#3438).
|
|
3
|
+
*
|
|
4
|
+
* NEW harvest — do not reuse `classifyShellAuthzOps` / `listShellOps` as-is.
|
|
5
|
+
* Those return [] for `git checkout --` / `rm` of ordinary product paths.
|
|
6
|
+
*
|
|
7
|
+
* ! Resolves a target for ONE shape only: a single simple command. Everything
|
|
8
|
+
* else is recognized and fail-closed. Reconstructing a target from shell state
|
|
9
|
+
* was implemented and withdrawn — cwd depends on operator precedence, on exit
|
|
10
|
+
* status, on subshell boundaries, and on git config, and every resolution rule
|
|
11
|
+
* added produced its own fence bypass. Do not add it back; widen the recognized
|
|
12
|
+
* *forms* instead, and see `content/contracts/path-write-fence.md`.
|
|
13
|
+
*
|
|
14
|
+
* Residual known-open (recognition, not resolution): `python -c`,
|
|
15
|
+
* `cmd /c copy`, obfuscated `bash -c`.
|
|
16
|
+
*
|
|
17
|
+
* Commit-time active-scope is out of this slice (#3438), and ⊗ do not weaken
|
|
18
|
+
* Edit to match Shell. Keep this sentence on ONE line: acceptance clause 4 is
|
|
19
|
+
* verified by a literal token search, so wrapping it silently fails the gate.
|
|
20
|
+
*/
|
|
21
|
+
import { record, toolInputRecord } from "./classify/payload.js";
|
|
22
|
+
/** Injected write target for expansion dests so the fence cannot match a literal glob. */
|
|
23
|
+
export const SHELL_DEST_EXPANSION_SENTINEL = "__shell_dest_expansion__";
|
|
24
|
+
/** Globs / braces on a token. `$` and backtick never reach here unescaped — `hasUnsupportedSyntax` already fail-closed those. */
|
|
25
|
+
const EXPANSION_DEST = /[*?[\]{}]/;
|
|
26
|
+
const WRAP_BINS = new Set(["sudo", "env", "command"]);
|
|
27
|
+
/** Wrapper flags that consume the next token. Unlisted `-` flags are boolean. */
|
|
28
|
+
const WRAP_VALUE_OPTS = {
|
|
29
|
+
sudo: new Set(["-u", "-g", "-C", "-p", "-r", "-t", "-T", "-D"]),
|
|
30
|
+
env: new Set(["-u", "-C", "-S", "--chdir", "--split-string"]),
|
|
31
|
+
command: new Set(),
|
|
32
|
+
};
|
|
33
|
+
/** Verbs this classifier recognizes, for scanning past a wrapper's options. */
|
|
34
|
+
const RECOGNIZED_BINS = new Set(["rm", "rm.exe", "rmdir", "rmdir.exe", "git", "git.exe"]);
|
|
35
|
+
function isRecognizedBin(token) {
|
|
36
|
+
if (token === undefined)
|
|
37
|
+
return false;
|
|
38
|
+
return RECOGNIZED_BINS.has(token.replace(/^[({!]+/, "").toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
const GIT_GLOBAL_VALUE_OPTS = new Set([
|
|
41
|
+
"-C",
|
|
42
|
+
"-c",
|
|
43
|
+
"--git-dir",
|
|
44
|
+
"--work-tree",
|
|
45
|
+
"--namespace",
|
|
46
|
+
"--config-env",
|
|
47
|
+
"--super-prefix",
|
|
48
|
+
"--list-cmds",
|
|
49
|
+
]);
|
|
50
|
+
const RESTORE_VALUE_OPTS = new Set(["--source", "-s", "--conflict"]);
|
|
51
|
+
/**
|
|
52
|
+
* Characters a preceding backslash escapes in an unquoted word. A backslash
|
|
53
|
+
* before anything else is retained, so Windows dests keep their separators.
|
|
54
|
+
*/
|
|
55
|
+
const SHELL_ESCAPABLE = new Set([
|
|
56
|
+
" ",
|
|
57
|
+
"\t",
|
|
58
|
+
"\n",
|
|
59
|
+
"\r",
|
|
60
|
+
'"',
|
|
61
|
+
"'",
|
|
62
|
+
"\\",
|
|
63
|
+
"$",
|
|
64
|
+
"`",
|
|
65
|
+
"&",
|
|
66
|
+
"|",
|
|
67
|
+
";",
|
|
68
|
+
"(",
|
|
69
|
+
")",
|
|
70
|
+
"<",
|
|
71
|
+
">",
|
|
72
|
+
"*",
|
|
73
|
+
"?",
|
|
74
|
+
"[",
|
|
75
|
+
"]",
|
|
76
|
+
"{",
|
|
77
|
+
"}",
|
|
78
|
+
"~",
|
|
79
|
+
"#",
|
|
80
|
+
]);
|
|
81
|
+
/**
|
|
82
|
+
* Inject a dest path so `inspectMutationGates` sees the same write target as Edit/Write.
|
|
83
|
+
* Preserves original payload fields (command, posture markers).
|
|
84
|
+
*/
|
|
85
|
+
export function payloadWithInjectedWriteTarget(payload, destPath) {
|
|
86
|
+
const rec = record(payload) ?? {};
|
|
87
|
+
const toolInput = toolInputRecord(rec);
|
|
88
|
+
return {
|
|
89
|
+
...rec,
|
|
90
|
+
tool_input: {
|
|
91
|
+
...(toolInput ?? {}),
|
|
92
|
+
file_path: destPath,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Classify recognized product dest-forms in a shell command (#3438 v1 list).
|
|
98
|
+
* Empty when the command is not `git checkout -- <paths>`, `git restore`, or `rm`/`rmdir`.
|
|
99
|
+
*/
|
|
100
|
+
export function destFormHasExpansion(path) {
|
|
101
|
+
return EXPANSION_DEST.test(path);
|
|
102
|
+
}
|
|
103
|
+
export function classifyProductDestForms(command) {
|
|
104
|
+
const cmd = command.trim();
|
|
105
|
+
if (cmd.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
const segments = splitCommandSegments(cmd);
|
|
108
|
+
// A single simple command is the only shape whose target this classifier can
|
|
109
|
+
// PROVE. Anything compound gets recognized but never resolved: reconstructing
|
|
110
|
+
// a target from shell state was tried and abandoned (#3438) — cwd depends on
|
|
111
|
+
// operator precedence, on exit status (`cd x || …`), on subshell boundaries,
|
|
112
|
+
// and on git context options, so each reconstruction rule grew its own
|
|
113
|
+
// bypass.
|
|
114
|
+
//
|
|
115
|
+
// ⊗ Recognition is NOT total either, and this is not a security boundary.
|
|
116
|
+
// Only four verbs are recognized, and a non-literal verb (`\rm`, `rm${IFS}x`)
|
|
117
|
+
// defeats the tokenizer, so the fail-closed branch below only fires when the
|
|
118
|
+
// verb is still legible. Unrecognized mutators (`git reset --hard`,
|
|
119
|
+
// `git clean`, `git checkout` without `--`, `mv`, `sed -i`, `>` redirection)
|
|
120
|
+
// and interpreters (`bash -c`, `python -c`) are fail-OPEN. See the threat
|
|
121
|
+
// model in content/contracts/path-write-fence.md: this is a guardrail for
|
|
122
|
+
// careless agents, not a boundary against adversarial ones.
|
|
123
|
+
if (segments.length === 1 && !hasUnsupportedSyntax(cmd)) {
|
|
124
|
+
return classifySimpleDestForms(segments[0] ?? "");
|
|
125
|
+
}
|
|
126
|
+
// Compound: report one fail-closed dest per recognized kind. No path is
|
|
127
|
+
// claimed, because any path claimed here would be a guess.
|
|
128
|
+
const kinds = new Set();
|
|
129
|
+
for (const seg of segments) {
|
|
130
|
+
for (const dest of classifyDestFormSegment(seg))
|
|
131
|
+
kinds.add(dest.kind);
|
|
132
|
+
}
|
|
133
|
+
return [...kinds].map((kind) => ({
|
|
134
|
+
kind,
|
|
135
|
+
path: SHELL_DEST_EXPANSION_SENTINEL,
|
|
136
|
+
expansion: true,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
/** Classify one command with no operators, no grouping, and no substitution. */
|
|
140
|
+
function classifySimpleDestForms(segment) {
|
|
141
|
+
const found = [];
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
for (const dest of classifyDestFormSegment(segment)) {
|
|
144
|
+
const path = dest.path;
|
|
145
|
+
const expansion =
|
|
146
|
+
// `unsafe` from the classifier (git context options relocate the tree).
|
|
147
|
+
dest.expansion === true ||
|
|
148
|
+
// A RETAINED backslash (one the tokenizer did not consume as an escape)
|
|
149
|
+
// is dialect-ambiguous. On win32 it is a path separator, so a Windows
|
|
150
|
+
// absolute dest means what it looks like; under a POSIX shell -- including
|
|
151
|
+
// Git Bash ON win32 -- the same word drops its separators and targets
|
|
152
|
+
// something else entirely. The payload does not say which shell will run
|
|
153
|
+
// the string, so the target cannot be proved either way. Fail closed (#3624).
|
|
154
|
+
path.includes("\\") ||
|
|
155
|
+
// `~` expands to $HOME only at the START of a word, so a trailing `~`
|
|
156
|
+
// (`foo.ts~`) is an ordinary path and must not be swept up here.
|
|
157
|
+
path.startsWith("~") ||
|
|
158
|
+
destFormHasExpansion(path);
|
|
159
|
+
const key = `${dest.kind}\0${path}`;
|
|
160
|
+
if (seen.has(key))
|
|
161
|
+
continue;
|
|
162
|
+
seen.add(key);
|
|
163
|
+
found.push(expansion ? { kind: dest.kind, path, expansion: true } : { kind: dest.kind, path });
|
|
164
|
+
}
|
|
165
|
+
return found;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Unquoted syntax whose effect on the target this classifier does not model:
|
|
169
|
+
* subshell grouping, brace groups, and command substitution. Presence of any of
|
|
170
|
+
* it makes the command non-simple, so it is recognized and fail-closed rather
|
|
171
|
+
* than resolved (#3438).
|
|
172
|
+
*/
|
|
173
|
+
function hasUnsupportedSyntax(command) {
|
|
174
|
+
let quote = null;
|
|
175
|
+
for (let i = 0; i < command.length; i++) {
|
|
176
|
+
const c = command[i];
|
|
177
|
+
if (c === undefined)
|
|
178
|
+
break;
|
|
179
|
+
if (quote !== null) {
|
|
180
|
+
// Double-quoted `\$` / `\`` are literals; skip the escape so we do not
|
|
181
|
+
// treat them as substitution (false-deny under `shellDestForms: enforce`).
|
|
182
|
+
if (quote === '"' && c === "\\" && i + 1 < command.length) {
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// A single-quoted string is fully literal; unescaped `"$` / backtick still expand.
|
|
187
|
+
if (c === quote)
|
|
188
|
+
quote = null;
|
|
189
|
+
else if (quote === '"' && (c === "$" || c === "`"))
|
|
190
|
+
return true;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (c === "\\" && i + 1 < command.length) {
|
|
194
|
+
i++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (c === "'" || c === '"') {
|
|
198
|
+
quote = c;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (c === "(" || c === ")" || c === "{" || c === "}" || c === "`" || c === "$")
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Split on unquoted `&&`, `||`, `|`, `&`, `;`, and newline. Segment TEXT only —
|
|
208
|
+
* deliberately no cwd or precedence metadata. A command that splits into more
|
|
209
|
+
* than one segment is not simple, and the only thing done with the pieces is to
|
|
210
|
+
* recognize which dest-form kinds appear in them.
|
|
211
|
+
*/
|
|
212
|
+
function splitCommandSegments(command) {
|
|
213
|
+
const segments = [];
|
|
214
|
+
let cur = "";
|
|
215
|
+
let quote = null;
|
|
216
|
+
const flush = () => {
|
|
217
|
+
segments.push(cur);
|
|
218
|
+
cur = "";
|
|
219
|
+
};
|
|
220
|
+
for (let i = 0; i < command.length; i++) {
|
|
221
|
+
const c = command[i];
|
|
222
|
+
if (c === undefined)
|
|
223
|
+
break;
|
|
224
|
+
if (quote !== null) {
|
|
225
|
+
if (c === quote)
|
|
226
|
+
quote = null;
|
|
227
|
+
cur += c;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (c === "\\" && i + 1 < command.length) {
|
|
231
|
+
cur += c;
|
|
232
|
+
cur += command[i + 1] ?? "";
|
|
233
|
+
i++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (c === "'" || c === '"') {
|
|
237
|
+
quote = c;
|
|
238
|
+
cur += c;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if ((c === "&" && command[i + 1] === "&") || (c === "|" && command[i + 1] === "|")) {
|
|
242
|
+
flush();
|
|
243
|
+
i++;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (c === "|" || c === "&" || c === ";" || c === "\n" || c === "\r") {
|
|
247
|
+
flush();
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
cur += c;
|
|
251
|
+
}
|
|
252
|
+
flush();
|
|
253
|
+
return segments.filter((seg) => seg.trim().length > 0);
|
|
254
|
+
}
|
|
255
|
+
function tokenizeSegment(segment) {
|
|
256
|
+
const tokens = [];
|
|
257
|
+
let cur = "";
|
|
258
|
+
let quote = null;
|
|
259
|
+
const push = () => {
|
|
260
|
+
if (cur.length > 0) {
|
|
261
|
+
tokens.push(cur);
|
|
262
|
+
cur = "";
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
for (let i = 0; i < segment.length; i++) {
|
|
266
|
+
const c = segment[i];
|
|
267
|
+
if (c === undefined)
|
|
268
|
+
break;
|
|
269
|
+
if (quote !== null) {
|
|
270
|
+
if (c === quote) {
|
|
271
|
+
quote = null;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (quote === '"' && c === "\\" && i + 1 < segment.length) {
|
|
275
|
+
cur += segment[i + 1] ?? "";
|
|
276
|
+
i++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
cur += c;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (c === "\\" && i + 1 < segment.length) {
|
|
283
|
+
const next = segment[i + 1] ?? "";
|
|
284
|
+
// `rm protected\ file` is ONE path. But this module also accepts Windows
|
|
285
|
+
// dests (`C:\Repos\file.ts`), where a backslash is a separator, not an
|
|
286
|
+
// escape — so only consume it before a character that needs escaping and
|
|
287
|
+
// otherwise keep it verbatim (#3438).
|
|
288
|
+
if (SHELL_ESCAPABLE.has(next)) {
|
|
289
|
+
cur += next;
|
|
290
|
+
i++;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
cur += c;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (c === "'" || c === '"') {
|
|
297
|
+
quote = c;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (c === " " || c === "\t") {
|
|
301
|
+
push();
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
cur += c;
|
|
305
|
+
}
|
|
306
|
+
push();
|
|
307
|
+
return tokens;
|
|
308
|
+
}
|
|
309
|
+
function isEnvAssign(token) {
|
|
310
|
+
const eq = token.indexOf("=");
|
|
311
|
+
if (eq <= 0)
|
|
312
|
+
return false;
|
|
313
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(token.slice(0, eq));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Environment assignments that can relocate git's work tree.
|
|
317
|
+
*
|
|
318
|
+
* `GIT_WORK_TREE=` / `GIT_DIR=` do it directly. `GIT_CONFIG_KEY_n=core.worktree`
|
|
319
|
+
* does it through the env-config protocol, and `GIT_CONFIG*` file overrides can
|
|
320
|
+
* carry `core.worktree` in a file this classifier never reads — all fail closed
|
|
321
|
+
* rather than resolving a prefix (#3438).
|
|
322
|
+
*/
|
|
323
|
+
function isGitWorkTreeAssign(token) {
|
|
324
|
+
const eq = token.indexOf("=");
|
|
325
|
+
if (eq <= 0)
|
|
326
|
+
return false;
|
|
327
|
+
const key = token.slice(0, eq);
|
|
328
|
+
const value = token.slice(eq + 1);
|
|
329
|
+
if (key === "GIT_WORK_TREE" || key === "GIT_DIR")
|
|
330
|
+
return value.length > 0;
|
|
331
|
+
// `GIT_CONFIG_KEY_0=core.worktree` with its paired VALUE relocates the tree.
|
|
332
|
+
if (/^GIT_CONFIG_KEY_\d+$/.test(key))
|
|
333
|
+
return value.toLowerCase() === "core.worktree";
|
|
334
|
+
// A config FILE override can set core.worktree out of sight.
|
|
335
|
+
return key === "GIT_CONFIG" || key === "GIT_CONFIG_GLOBAL" || key === "GIT_CONFIG_SYSTEM";
|
|
336
|
+
}
|
|
337
|
+
function skipPrefix(tokens) {
|
|
338
|
+
let i = 0;
|
|
339
|
+
let gitContext = false;
|
|
340
|
+
const consumeAssigns = () => {
|
|
341
|
+
while (i < tokens.length) {
|
|
342
|
+
const tok = tokens[i];
|
|
343
|
+
if (tok === undefined)
|
|
344
|
+
break;
|
|
345
|
+
// Standalone grouping / negation punctuation: skip so the binary behind it
|
|
346
|
+
// is still RECOGNIZED. Only compound commands can reach here, and every
|
|
347
|
+
// one of them is fail-closed, so liberal recognition is the safe side.
|
|
348
|
+
if (tok === "{" || tok === "}" || tok === "(" || tok === ")" || tok === "!") {
|
|
349
|
+
i++;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (!isEnvAssign(tok))
|
|
353
|
+
break;
|
|
354
|
+
if (isGitWorkTreeAssign(tok))
|
|
355
|
+
gitContext = true;
|
|
356
|
+
i++;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
consumeAssigns();
|
|
360
|
+
const wrap = tokens[i]?.toLowerCase();
|
|
361
|
+
if (wrap !== undefined && WRAP_BINS.has(wrap)) {
|
|
362
|
+
i++;
|
|
363
|
+
consumeAssigns();
|
|
364
|
+
// Skip this wrapper's own options, including value-taking ones (`sudo -u
|
|
365
|
+
// root`, `env -u VAR`). Do NOT scan past a later non-option token: that is
|
|
366
|
+
// the real executable (`sudo grep rm file` must stay unclassifiable).
|
|
367
|
+
const valueOpts = WRAP_VALUE_OPTS[wrap] ?? new Set();
|
|
368
|
+
while (i < tokens.length) {
|
|
369
|
+
const tok = tokens[i];
|
|
370
|
+
if (tok === undefined)
|
|
371
|
+
break;
|
|
372
|
+
if (isRecognizedBin(tok))
|
|
373
|
+
break;
|
|
374
|
+
if (tok === "--") {
|
|
375
|
+
i++;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
if (tok.startsWith("-")) {
|
|
379
|
+
i += valueOpts.has(tok) ? 2 : 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (isEnvAssign(tok)) {
|
|
383
|
+
if (isGitWorkTreeAssign(tok))
|
|
384
|
+
gitContext = true;
|
|
385
|
+
i++;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { i, gitContext };
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Skip git's global options, reporting whether any of them RELOCATES the work
|
|
395
|
+
* tree. Relocation is not resolved: `-C` composes, `--work-tree` resolves
|
|
396
|
+
* against the `-C` chain, `--git-dir` interacts with both, and `-c
|
|
397
|
+
* core.workTree` depends on the git dir. Each of those resolution rules grew
|
|
398
|
+
* its own fence bypass (#3438), so a relocating command fails closed instead.
|
|
399
|
+
*/
|
|
400
|
+
function skipGitGlobals(tokens, start) {
|
|
401
|
+
let i = start;
|
|
402
|
+
let gitContext = false;
|
|
403
|
+
while (i < tokens.length) {
|
|
404
|
+
const t = tokens[i];
|
|
405
|
+
if (t === undefined || !t.startsWith("-"))
|
|
406
|
+
return { i, gitContext };
|
|
407
|
+
if (t === "-c" || t === "--config-env") {
|
|
408
|
+
if (isCoreWorkTreeConfig(tokens[i + 1]))
|
|
409
|
+
gitContext = true;
|
|
410
|
+
i += 2;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (t.startsWith("--config-env=")) {
|
|
414
|
+
if (isCoreWorkTreeConfig(t.slice("--config-env=".length)))
|
|
415
|
+
gitContext = true;
|
|
416
|
+
i++;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (t === "-C" || t === "--work-tree" || t === "--git-dir") {
|
|
420
|
+
gitContext = true;
|
|
421
|
+
i += 2;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (t.startsWith("--work-tree=") ||
|
|
425
|
+
t.startsWith("--git-dir=") ||
|
|
426
|
+
(t.startsWith("-C") && t.length > 2)) {
|
|
427
|
+
gitContext = true;
|
|
428
|
+
i++;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (t.startsWith("--") && t.includes("=")) {
|
|
432
|
+
i++;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (GIT_GLOBAL_VALUE_OPTS.has(t)) {
|
|
436
|
+
i += 2;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (t.startsWith("-c")) {
|
|
440
|
+
if (isCoreWorkTreeConfig(t.slice(2)))
|
|
441
|
+
gitContext = true;
|
|
442
|
+
i++;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
i++;
|
|
446
|
+
}
|
|
447
|
+
return { i, gitContext };
|
|
448
|
+
}
|
|
449
|
+
function pathsAfterDashDash(tokens, start) {
|
|
450
|
+
const dash = tokens.indexOf("--", start);
|
|
451
|
+
if (dash < 0)
|
|
452
|
+
return [];
|
|
453
|
+
return tokens.slice(dash + 1).filter((t) => t.length > 0);
|
|
454
|
+
}
|
|
455
|
+
function skipRestoreFlag(tokens, i) {
|
|
456
|
+
const t = tokens[i];
|
|
457
|
+
if (t === undefined)
|
|
458
|
+
return i;
|
|
459
|
+
if (t.startsWith("--") && t.includes("="))
|
|
460
|
+
return i + 1;
|
|
461
|
+
if (RESTORE_VALUE_OPTS.has(t))
|
|
462
|
+
return i + 2;
|
|
463
|
+
if (t.startsWith("-s") && t.length > 2)
|
|
464
|
+
return i + 1;
|
|
465
|
+
return i + 1;
|
|
466
|
+
}
|
|
467
|
+
function dests(kind, paths, unsafe = false) {
|
|
468
|
+
return paths
|
|
469
|
+
.filter((path) => path.length > 0)
|
|
470
|
+
.map((path) => (unsafe ? { kind, path, expansion: true } : { kind, path }));
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* `-c core.workTree=...` / `--config-env` relocates the work tree through git
|
|
474
|
+
* config. Resolution depends on the git dir, which this classifier does not
|
|
475
|
+
* model, so the target is not reconstructable and stays fail-closed (#3438).
|
|
476
|
+
*/
|
|
477
|
+
function isCoreWorkTreeConfig(token) {
|
|
478
|
+
if (token === undefined)
|
|
479
|
+
return false;
|
|
480
|
+
const eq = token.indexOf("=");
|
|
481
|
+
const key = eq < 0 ? token : token.slice(0, eq);
|
|
482
|
+
return key.toLowerCase() === "core.worktree";
|
|
483
|
+
}
|
|
484
|
+
function classifyDestFormSegment(segment) {
|
|
485
|
+
const tokens = tokenizeSegment(segment.trim());
|
|
486
|
+
const prefixSkip = skipPrefix(tokens);
|
|
487
|
+
let i = prefixSkip.i;
|
|
488
|
+
const binRaw = tokens[i];
|
|
489
|
+
if (binRaw === undefined)
|
|
490
|
+
return [];
|
|
491
|
+
// Strip grouping punctuation glued to the binary (`{rm`, `(rm`) for the same
|
|
492
|
+
// recognition reason as in skipPrefix. Unquoted grouping can only appear in a
|
|
493
|
+
// command that is already fail-closed.
|
|
494
|
+
const bin = binRaw.replace(/^[({!]+/, "").toLowerCase();
|
|
495
|
+
if (bin === "git" || bin === "git.exe") {
|
|
496
|
+
const skipped = skipGitGlobals(tokens, i + 1);
|
|
497
|
+
i = skipped.i;
|
|
498
|
+
// `--pathspec-from-file=<f>` (and its NUL variant) put the targets INSIDE a
|
|
499
|
+
// file. Reading it would mean file I/O at hook time plus resolving those
|
|
500
|
+
// paths against a cwd this classifier does not know — the resolution trap
|
|
501
|
+
// #3438 abandoned. Recognized, never resolved (#3624).
|
|
502
|
+
const pathspecFromFile = tokens.some((t) => t.startsWith("--pathspec-from-file") || t === "--pathspec-file-nul");
|
|
503
|
+
// Any relocating context option makes the target unprovable — fail closed.
|
|
504
|
+
const unsafe = skipped.gitContext || prefixSkip.gitContext || pathspecFromFile;
|
|
505
|
+
const sub = tokens[i]?.toLowerCase();
|
|
506
|
+
// `--pathspec-from-file` supplies the pathspec instead of argv, so there is
|
|
507
|
+
// no dest token to mark unsafe and no `--` to require. Emit the sentinel
|
|
508
|
+
// directly, the way a compound command does — the kind is known, the target
|
|
509
|
+
// is not (#3624).
|
|
510
|
+
if (pathspecFromFile && (sub === "checkout" || sub === "restore")) {
|
|
511
|
+
return [
|
|
512
|
+
{
|
|
513
|
+
kind: sub === "checkout" ? "git-checkout" : "git-restore",
|
|
514
|
+
path: SHELL_DEST_EXPANSION_SENTINEL,
|
|
515
|
+
expansion: true,
|
|
516
|
+
},
|
|
517
|
+
];
|
|
518
|
+
}
|
|
519
|
+
if (sub === "checkout") {
|
|
520
|
+
return dests("git-checkout", pathsAfterDashDash(tokens, i + 1), unsafe);
|
|
521
|
+
}
|
|
522
|
+
if (sub === "restore") {
|
|
523
|
+
i++;
|
|
524
|
+
const afterDash = pathsAfterDashDash(tokens, i);
|
|
525
|
+
if (afterDash.length > 0)
|
|
526
|
+
return dests("git-restore", afterDash, unsafe);
|
|
527
|
+
const paths = [];
|
|
528
|
+
while (i < tokens.length) {
|
|
529
|
+
const t = tokens[i];
|
|
530
|
+
if (t === undefined)
|
|
531
|
+
break;
|
|
532
|
+
if (t === "--") {
|
|
533
|
+
paths.push(...tokens.slice(i + 1));
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
if (t.startsWith("-")) {
|
|
537
|
+
i = skipRestoreFlag(tokens, i);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
paths.push(t);
|
|
541
|
+
i++;
|
|
542
|
+
}
|
|
543
|
+
return dests("git-restore", paths, unsafe);
|
|
544
|
+
}
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
if (bin === "rm" || bin === "rm.exe") {
|
|
548
|
+
return dests("rm", collectRmPaths(tokens, i + 1));
|
|
549
|
+
}
|
|
550
|
+
if (bin === "rmdir" || bin === "rmdir.exe") {
|
|
551
|
+
return dests("rmdir", collectRmPaths(tokens, i + 1));
|
|
552
|
+
}
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
function collectRmPaths(tokens, start) {
|
|
556
|
+
const paths = [];
|
|
557
|
+
let i = start;
|
|
558
|
+
while (i < tokens.length) {
|
|
559
|
+
const t = tokens[i];
|
|
560
|
+
if (t === undefined)
|
|
561
|
+
break;
|
|
562
|
+
if (t === "--") {
|
|
563
|
+
paths.push(...tokens.slice(i + 1));
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
if (t.startsWith("-")) {
|
|
567
|
+
i++;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
paths.push(t);
|
|
571
|
+
i++;
|
|
572
|
+
}
|
|
573
|
+
return paths;
|
|
574
|
+
}
|
|
575
|
+
//# sourceMappingURL=dest-form.js.map
|
|
@@ -2,7 +2,8 @@ import { type AuthzState, type HumanOriginGrant } from "../authz/index.js";
|
|
|
2
2
|
import { detectDeftDirectiveDisable } from "../policy/deft-directive-disable.js";
|
|
3
3
|
import { detectNoDeftDirective } from "../policy/no-deft-directive.js";
|
|
4
4
|
import { type RuntimeAuthorityPolicy } from "../policy/runtime-authority.js";
|
|
5
|
-
import { type
|
|
5
|
+
import { type GitRunner } from "../session/git.js";
|
|
6
|
+
import { type DetectWorkSelection, type RitualRunner, type VerifyResult } from "../session/verify-session-ritual.js";
|
|
6
7
|
import { type HookPayloadContext } from "./classify/index.js";
|
|
7
8
|
import { type ActiveScopeInspection } from "./scope.js";
|
|
8
9
|
export { ASSIST_SESSION_POSTURE_ENV, hookReadOnlyFromPayload, isAssistPosture, isEphemeralSpawn, isExploreSpawn, isReadOnlyHookContext, } from "./readonly.js";
|
|
@@ -60,6 +61,21 @@ export interface HookPolicySeams {
|
|
|
60
61
|
readonly verifyRitual?: (projectRoot: string) => VerifyResult;
|
|
61
62
|
/** @deprecated Compatibility seam for existing tests and callers. */
|
|
62
63
|
readonly inspectRitual?: (projectRoot: string) => VerifyResult;
|
|
64
|
+
/**
|
|
65
|
+
* Test seam: ritual runner used by the default write-gate verifier (#3738).
|
|
66
|
+
* Ignored when `verifyRitual` / `inspectRitual` is supplied.
|
|
67
|
+
*/
|
|
68
|
+
readonly ritualRunner?: RitualRunner;
|
|
69
|
+
/**
|
|
70
|
+
* Test seam: work-selection detector for the default write-gate verifier (#3738).
|
|
71
|
+
* Ignored when `verifyRitual` / `inspectRitual` is supplied.
|
|
72
|
+
*/
|
|
73
|
+
readonly detectWorkSelection?: DetectWorkSelection;
|
|
74
|
+
/**
|
|
75
|
+
* Test seam: git runner for the default write-gate verifier (#3738).
|
|
76
|
+
* Ignored when `verifyRitual` / `inspectRitual` is supplied.
|
|
77
|
+
*/
|
|
78
|
+
readonly ritualRunGit?: GitRunner;
|
|
63
79
|
readonly inspectScope?: (projectRoot: string) => ActiveScopeInspection;
|
|
64
80
|
readonly sessionStart?: (projectRoot: string) => {
|
|
65
81
|
code: number;
|