@deftai/directive-core 0.98.0 → 0.99.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/authz/classify.js +400 -56
- package/dist/consumer-check-contract/evaluate.d.ts +40 -0
- package/dist/consumer-check-contract/evaluate.js +188 -3
- package/dist/consumer-check-contract/index.d.ts +1 -1
- package/dist/consumer-check-contract/index.js +1 -1
- package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
- package/dist/content-contracts/skills/greptile-detector.js +202 -4
- package/dist/decision/index.d.ts +17 -0
- package/dist/decision/index.js +35 -0
- package/dist/decision/list.d.ts +47 -0
- package/dist/decision/list.js +250 -0
- package/dist/decision/schema.d.ts +88 -0
- package/dist/decision/schema.js +293 -0
- package/dist/decision/write.d.ts +82 -0
- package/dist/decision/write.js +427 -0
- package/dist/eval/report.d.ts +29 -0
- package/dist/eval/report.js +69 -0
- package/dist/eval/run.d.ts +9 -0
- package/dist/eval/run.js +40 -4
- package/dist/eval/version-pin.d.ts +99 -0
- package/dist/eval/version-pin.js +181 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/platform/host-content-surface.d.ts +74 -0
- package/dist/platform/host-content-surface.js +214 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/policy/ceremony-dial.d.ts +233 -0
- package/dist/policy/ceremony-dial.js +829 -0
- package/dist/policy/deft-directive-disable.js +12 -2
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -1
- package/dist/pr-merge-readiness/evaluate.js +10 -0
- package/dist/pr-merge-readiness/mergeability.js +5 -0
- package/dist/pr-merge-readiness/output.js +2 -0
- package/dist/pr-merge-readiness/parse.js +4 -0
- package/dist/pr-merge-readiness/types.d.ts +6 -0
- package/dist/scope/effort-activate-gate.d.ts +28 -0
- package/dist/scope/effort-activate-gate.js +64 -0
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/transition.js +8 -0
- package/dist/scope-provenance/evaluate.d.ts +21 -0
- package/dist/scope-provenance/evaluate.js +143 -33
- package/dist/scope-provenance/index.d.ts +1 -1
- package/dist/scope-provenance/index.js +1 -1
- package/dist/session/session-start.d.ts +24 -1
- package/dist/session/session-start.js +183 -26
- package/dist/swarm/index.d.ts +2 -0
- package/dist/swarm/index.js +2 -0
- package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
- package/dist/swarm/pre-dispatch-cli.js +143 -0
- package/dist/swarm/pre-dispatch.d.ts +87 -0
- package/dist/swarm/pre-dispatch.js +373 -0
- package/dist/vbrief-activate/activate.js +6 -0
- package/dist/vbrief-validate/constants.d.ts +2 -0
- package/dist/vbrief-validate/constants.js +2 -0
- package/dist/vbrief-validate/schema.js +4 -1
- package/package.json +15 -3
package/dist/authz/classify.js
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* (CodeQL js/polynomial-redos).
|
|
8
8
|
*/
|
|
9
9
|
import { classifyMcpTool, classifyShellCommand, listShellOps, } from "../policy/runtime-authority.js";
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Split on whitespace without nested quantifiers (O(n)).
|
|
12
|
+
* Newlines become `;` segment breaks so compound lists like
|
|
13
|
+
* `scp …authz…\necho ok` keep the scp dest (#3213 Greptile residual).
|
|
14
|
+
* Bare space/tab/CR still only separate tokens.
|
|
15
|
+
*/
|
|
11
16
|
function shellTokens(command) {
|
|
12
17
|
const out = [];
|
|
13
18
|
let cur = "";
|
|
@@ -15,11 +20,22 @@ function shellTokens(command) {
|
|
|
15
20
|
const c = command[i];
|
|
16
21
|
if (c === undefined)
|
|
17
22
|
break;
|
|
18
|
-
if (c === " " || c === "\t" || c === "\
|
|
23
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
24
|
+
if (cur.length > 0) {
|
|
25
|
+
out.push(cur);
|
|
26
|
+
cur = "";
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (c === "\n") {
|
|
19
31
|
if (cur.length > 0) {
|
|
20
32
|
out.push(cur);
|
|
21
33
|
cur = "";
|
|
22
34
|
}
|
|
35
|
+
// Emit a segment break once (avoid runs of `;` from blank lines).
|
|
36
|
+
if (out.length > 0 && out[out.length - 1] !== ";") {
|
|
37
|
+
out.push(";");
|
|
38
|
+
}
|
|
23
39
|
continue;
|
|
24
40
|
}
|
|
25
41
|
cur += c;
|
|
@@ -162,6 +178,279 @@ const POLICY_AUTHORITY_MUTATORS = new Set([
|
|
|
162
178
|
]);
|
|
163
179
|
/** Kill-switch / permanent opt-out basenames agents must not plant under UAT (#3186 / #3039). */
|
|
164
180
|
const KILL_SWITCH_BASENAMES = [".deft-directive-disable", ".no-deft-directive"];
|
|
181
|
+
/**
|
|
182
|
+
* Downloaders / decoders / remote-copy tools that can plant files without shell
|
|
183
|
+
* redirects (#3206 / #3213). Not in INDIRECT_WRITE_BINS: those feed hasWriteShape
|
|
184
|
+
* and would classify bare `curl $URL` as a store write via opaque-expansion heuristics.
|
|
185
|
+
*/
|
|
186
|
+
const DOWNLOADER_DECODER_BINS = new Set([
|
|
187
|
+
"curl",
|
|
188
|
+
"wget",
|
|
189
|
+
"xxd",
|
|
190
|
+
"openssl",
|
|
191
|
+
// #3213 residual after #3206: alternate downloaders / remote copy.
|
|
192
|
+
"scp",
|
|
193
|
+
"aria2c",
|
|
194
|
+
"certutil",
|
|
195
|
+
]);
|
|
196
|
+
/**
|
|
197
|
+
* File destination flags for downloaders/decoders (#3206).
|
|
198
|
+
* normalizeToken lowercases, so wget `-O` and curl `-o` share `-o`.
|
|
199
|
+
* scp: `-o` is an SSH option — excluded in isDownloaderDestFlag.
|
|
200
|
+
*/
|
|
201
|
+
const DOWNLOADER_FILE_DEST_FLAGS = new Set([
|
|
202
|
+
"-o",
|
|
203
|
+
"--output",
|
|
204
|
+
"--output-document",
|
|
205
|
+
"-out",
|
|
206
|
+
"--out",
|
|
207
|
+
]);
|
|
208
|
+
/** Directory destination flags (curl --output-dir / wget -P / aria2c -d); bin-scoped below. */
|
|
209
|
+
const CURL_DIR_DEST_FLAGS = new Set(["--output-dir"]);
|
|
210
|
+
const WGET_DIR_DEST_FLAGS = new Set(["-p", "--directory-prefix"]);
|
|
211
|
+
const ARIA2C_DIR_DEST_FLAGS = new Set(["-d", "--dir"]);
|
|
212
|
+
/**
|
|
213
|
+
* Symlink / hard-link plant bins (#3213). Absent from prior killWriteBins →
|
|
214
|
+
* `ln -sf … .deft-directive-disable` classified empty → UAT fail-open.
|
|
215
|
+
*/
|
|
216
|
+
const SYMLINK_PLANT_BINS = new Set(["ln", "link", "mklink"]);
|
|
217
|
+
/** Final path segment of a token (path-qualified bins / .exe). */
|
|
218
|
+
function binBareName(token) {
|
|
219
|
+
const pathish = token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
|
|
220
|
+
const base = pathish.includes("/") ? pathish.slice(pathish.lastIndexOf("/") + 1) : pathish;
|
|
221
|
+
return base.endsWith(".exe") ? base.slice(0, -4) : base;
|
|
222
|
+
}
|
|
223
|
+
/** True when token names a downloader/decoder bin (bare or path-qualified). */
|
|
224
|
+
function isDownloaderDecoderBin(token) {
|
|
225
|
+
const n = normalizeToken(token);
|
|
226
|
+
if (n.startsWith("-"))
|
|
227
|
+
return false;
|
|
228
|
+
return DOWNLOADER_DECODER_BINS.has(binBareName(token));
|
|
229
|
+
}
|
|
230
|
+
function isDownloaderDestFlag(flag, bin) {
|
|
231
|
+
// scp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
|
|
232
|
+
if (bin === "scp")
|
|
233
|
+
return false;
|
|
234
|
+
if (DOWNLOADER_FILE_DEST_FLAGS.has(flag))
|
|
235
|
+
return true;
|
|
236
|
+
if (bin === "curl" && CURL_DIR_DEST_FLAGS.has(flag))
|
|
237
|
+
return true;
|
|
238
|
+
if (bin === "wget" && WGET_DIR_DEST_FLAGS.has(flag))
|
|
239
|
+
return true;
|
|
240
|
+
if (bin === "aria2c" && ARIA2C_DIR_DEST_FLAGS.has(flag))
|
|
241
|
+
return true;
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* OpenSSH/scp flags that take a separate value token (`-o ProxyCommand=…`, `-i key`, `-P port`).
|
|
246
|
+
* Not dest flags — must skip so value tokens are not mistaken for write destinations (#3213).
|
|
247
|
+
* Case-sensitive where needed: scp `-P` (port) takes a value; `-p` (preserve) does not.
|
|
248
|
+
*/
|
|
249
|
+
const SCP_VALUE_FLAGS_LOWER = new Set(["-o", "-i", "-c", "-s", "-j", "-f", "-l", "-b"]);
|
|
250
|
+
const SCP_VALUE_FLAGS_EXACT = new Set(["-P", "-F", "-S", "-J"]);
|
|
251
|
+
/** Shell metacharacters that end a command segment (compound lists / pipelines). */
|
|
252
|
+
function isShellSegmentBreak(token) {
|
|
253
|
+
const t = token.trim();
|
|
254
|
+
if (t.length === 0)
|
|
255
|
+
return false;
|
|
256
|
+
if (t === ";" || t === "|" || t === "||" || t === "&&" || t === "&")
|
|
257
|
+
return true;
|
|
258
|
+
// Trailing operator glued to a prior token is handled by pathish stripping; bare ops here.
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* First unquoted, unescaped shell-list operator (`;` `&` `|`) in a token, or -1.
|
|
263
|
+
* Quoted operators (e.g. `'file;name'`) and escaped unquoted ops (e.g. `path\;file`)
|
|
264
|
+
* are literal data and must not end scp segments (#3213 Greptile residual).
|
|
265
|
+
* O(n). Unquoted / double-quoted `\` escapes the next char; single-quoted `\` is literal.
|
|
266
|
+
*/
|
|
267
|
+
function firstUnquotedShellOpIndex(raw) {
|
|
268
|
+
let inSingle = false;
|
|
269
|
+
let inDouble = false;
|
|
270
|
+
for (let k = 0; k < raw.length; k++) {
|
|
271
|
+
const ch = raw[k];
|
|
272
|
+
// Outside single quotes, backslash escapes the next character (POSIX-ish).
|
|
273
|
+
if (ch === "\\" && k + 1 < raw.length && !inSingle) {
|
|
274
|
+
k++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (ch === "'" && !inDouble) {
|
|
278
|
+
inSingle = !inSingle;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (ch === '"' && !inSingle) {
|
|
282
|
+
inDouble = !inDouble;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (inSingle || inDouble)
|
|
286
|
+
continue;
|
|
287
|
+
if (ch === ";" || ch === "&" || ch === "|")
|
|
288
|
+
return k;
|
|
289
|
+
}
|
|
290
|
+
return -1;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Destinations from curl/wget/xxd/openssl/scp/aria2c/certutil via -o/--output/-O/-out/
|
|
294
|
+
* --output-dir/-P/-d/--dir (separate, =value, or attached short form), xxd -r path-like
|
|
295
|
+
* write positionals (#3206), and positional dests for scp/certutil (#3213).
|
|
296
|
+
* openssl uses flags only (no positional dests — avoids treating -in paths as writes).
|
|
297
|
+
* Segment stops at shell operators so compound `scp …authz…; echo` cannot overwrite dests.
|
|
298
|
+
* O(n) token walk — no nested-quantifier regex on untrusted input.
|
|
299
|
+
*/
|
|
300
|
+
function downloaderDecoderDestinations(tokens) {
|
|
301
|
+
const dests = [];
|
|
302
|
+
let i = 0;
|
|
303
|
+
while (i < tokens.length) {
|
|
304
|
+
if (!isDownloaderDecoderBin(tokens[i])) {
|
|
305
|
+
i++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const bin = binBareName(tokens[i]);
|
|
309
|
+
i++;
|
|
310
|
+
// xxd reverse mode writes; without -r, path positionals are dump inputs (read).
|
|
311
|
+
let xxdReverse = false;
|
|
312
|
+
// scp / certutil: collect pathish operands in this segment (#3213).
|
|
313
|
+
// Fail-closed under UAT: any pathish mentioning protected store/kill paths is a dest
|
|
314
|
+
// candidate (scp source-or-dest of `.deft/authz` is treated as settings — prefer deny
|
|
315
|
+
// over source/dest thrash). Last pathish remains the ordinary write dest.
|
|
316
|
+
// Segment breaks (`;`/`\n`/glued ops) prevent following-command overwrite.
|
|
317
|
+
let lastPositionalPath = null;
|
|
318
|
+
const protectedPathish = [];
|
|
319
|
+
while (i < tokens.length) {
|
|
320
|
+
const raw = tokens[i];
|
|
321
|
+
const n = normalizeToken(raw);
|
|
322
|
+
// Bare shell ops end this bin's segment. Do NOT use normalizeToken here —
|
|
323
|
+
// quoted `';'` becomes `;` after strip and would cut before the real authz dest
|
|
324
|
+
// (Greptile P1 residual #3213). Glued ops are handled by firstUnquotedShellOpIndex.
|
|
325
|
+
if (isShellSegmentBreak(raw)) {
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
// New bare bin starts another command segment (not pathish ./wget operands).
|
|
329
|
+
if (!n.startsWith("-") &&
|
|
330
|
+
!raw.includes("/") &&
|
|
331
|
+
!raw.includes("\\") &&
|
|
332
|
+
isDownloaderDecoderBin(raw)) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
if (bin === "xxd" && (n === "-r" || n === "--reverse")) {
|
|
336
|
+
xxdReverse = true;
|
|
337
|
+
i++;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// Skip openssl/xxd input flags + value so -in PATH is not a write dest.
|
|
341
|
+
if (n === "-in" || n === "--in" || n === "-inform" || n === "--inform") {
|
|
342
|
+
const next = tokens[i + 1];
|
|
343
|
+
if (next !== undefined && !String(next).startsWith("-")) {
|
|
344
|
+
i += 2;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
i++;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
// scp: skip OpenSSH value-taking flags + their values (-o Option=Value, -i key, -P port).
|
|
351
|
+
if (bin === "scp" &&
|
|
352
|
+
(SCP_VALUE_FLAGS_LOWER.has(n) ||
|
|
353
|
+
SCP_VALUE_FLAGS_EXACT.has(raw) ||
|
|
354
|
+
SCP_VALUE_FLAGS_EXACT.has(n))) {
|
|
355
|
+
const next = tokens[i + 1];
|
|
356
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
357
|
+
i += 2;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
i++;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
// --flag=value (and rare -out=value); scp -oOption=Value attached form also lands here.
|
|
364
|
+
if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
|
|
365
|
+
const eq = raw.indexOf("=");
|
|
366
|
+
const flag = normalizeToken(raw.slice(0, eq));
|
|
367
|
+
if (isDownloaderDestFlag(flag, bin)) {
|
|
368
|
+
dests.push(pathishToken(raw.slice(eq + 1)));
|
|
369
|
+
}
|
|
370
|
+
// scp attached -oProxyCommand=… is not a file dest — skip without recording.
|
|
371
|
+
i++;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
// Attached short: -oPATH / -OPATH (after lowercasing both are -opath…)
|
|
375
|
+
// Skip for scp (OpenSSH -oOption=Value attached forms are not file dests).
|
|
376
|
+
if (bin !== "scp" && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
|
|
377
|
+
if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output")) {
|
|
378
|
+
dests.push(pathishToken(raw.slice(4)));
|
|
379
|
+
i++;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (n.startsWith("-o") && !n.startsWith("-out") && n.length > 2) {
|
|
383
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
384
|
+
i++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
// wget attached -Pdir
|
|
388
|
+
if (bin === "wget" && n.startsWith("-p") && n.length > 2 && !n.startsWith("-proxy")) {
|
|
389
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
390
|
+
i++;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
// aria2c attached -dDIR (#3213)
|
|
394
|
+
if (bin === "aria2c" && n.startsWith("-d") && n.length > 2) {
|
|
395
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
396
|
+
i++;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Separate value: -o PATH / --output PATH / -out PATH / --output-dir / -P / -d
|
|
401
|
+
if (isDownloaderDestFlag(n, bin)) {
|
|
402
|
+
const next = tokens[i + 1];
|
|
403
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
404
|
+
dests.push(pathishToken(next));
|
|
405
|
+
i += 2;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
i++;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
// xxd -r only: path-like write positionals (not http(s) URLs). openssl: flags only.
|
|
412
|
+
if (bin === "xxd" && xxdReverse && !n.startsWith("-")) {
|
|
413
|
+
const p = pathishToken(raw);
|
|
414
|
+
if ((p.includes("/") || p.startsWith(".") || p.includes("\\")) &&
|
|
415
|
+
!p.startsWith("http:") &&
|
|
416
|
+
!p.startsWith("https:") &&
|
|
417
|
+
!p.startsWith("ftp:")) {
|
|
418
|
+
dests.push(p);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// scp / certutil: pathish operands (quote-aware glued-op cut).
|
|
422
|
+
// certutil under UAT: any `.deft/authz` / kill-switch pathish is fail-closed settings
|
|
423
|
+
// (read vs write thrash deferred — prefer deny over dest-parser perfection; #3213).
|
|
424
|
+
if ((bin === "scp" || bin === "certutil") && !n.startsWith("-")) {
|
|
425
|
+
const cut = firstUnquotedShellOpIndex(raw);
|
|
426
|
+
const cleaned = cut >= 0 ? raw.slice(0, cut) : raw;
|
|
427
|
+
const p = pathishToken(cleaned);
|
|
428
|
+
if (p.length > 0) {
|
|
429
|
+
lastPositionalPath = p;
|
|
430
|
+
// Fail-closed: protected store/kill basenames anywhere in scp/certutil pathish.
|
|
431
|
+
if (p.includes(".deft/authz") ||
|
|
432
|
+
p.includes(".deft-directive-disable") ||
|
|
433
|
+
p.includes(".no-deft-directive")) {
|
|
434
|
+
protectedPathish.push(p);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (cut >= 0) {
|
|
438
|
+
i++;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
i++;
|
|
443
|
+
}
|
|
444
|
+
// Prefer protected pathish (fail-closed) then last ordinary dest.
|
|
445
|
+
for (const p of protectedPathish) {
|
|
446
|
+
dests.push(p);
|
|
447
|
+
}
|
|
448
|
+
if (lastPositionalPath !== null && !protectedPathish.includes(lastPositionalPath)) {
|
|
449
|
+
dests.push(lastPositionalPath);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return dests;
|
|
453
|
+
}
|
|
165
454
|
function authzSubcommandFromToken(token) {
|
|
166
455
|
const t = normalizeToken(token);
|
|
167
456
|
if (t.startsWith("authz:")) {
|
|
@@ -237,45 +526,73 @@ function hasPolicyAuthorityMutator(tokens) {
|
|
|
237
526
|
}
|
|
238
527
|
return false;
|
|
239
528
|
}
|
|
529
|
+
/** True when pathish token names a kill-switch basename (quote-strip resistant). */
|
|
530
|
+
function pathishMentionsKillSwitch(pathish) {
|
|
531
|
+
for (const name of KILL_SWITCH_BASENAMES) {
|
|
532
|
+
if (pathish === name || pathish.endsWith(`/${name}`) || pathish.includes(name)) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
240
538
|
/**
|
|
241
|
-
* Shell write targeting `.deft-directive-disable` / `.no-deft-directive` (#3186 / #3039).
|
|
539
|
+
* Shell write targeting `.deft-directive-disable` / `.no-deft-directive` (#3186 / #3039 / #3213).
|
|
242
540
|
* Planting the kill-switch under UAT would full-bypass subsequent gates without operator recovery.
|
|
541
|
+
* Includes symlink plant bins (`ln`/`link`/`mklink`) and quote-split-resistant basename checks.
|
|
243
542
|
*/
|
|
244
543
|
function hasKillSwitchShellWrite(command, tokens) {
|
|
245
544
|
const lower = command.toLowerCase().replace(/\\/g, "/");
|
|
545
|
+
// Quote-stripped form so `'.deft'-directive-disable`-class splits still match (#3213).
|
|
546
|
+
const stripped = lower.replace(/['"]/g, "");
|
|
246
547
|
let mentionsKill = false;
|
|
247
548
|
for (const name of KILL_SWITCH_BASENAMES) {
|
|
248
|
-
if (lower.includes(name)) {
|
|
549
|
+
if (lower.includes(name) || stripped.includes(name)) {
|
|
249
550
|
mentionsKill = true;
|
|
250
551
|
break;
|
|
251
552
|
}
|
|
252
553
|
}
|
|
554
|
+
if (!mentionsKill) {
|
|
555
|
+
for (const t of tokens) {
|
|
556
|
+
if (pathishMentionsKillSwitch(pathishToken(t))) {
|
|
557
|
+
mentionsKill = true;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
253
562
|
if (!mentionsKill)
|
|
254
563
|
return false;
|
|
255
|
-
// Redirect dest region after each `>` / `>>` (O(n)).
|
|
256
|
-
for (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
j
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
564
|
+
// Redirect dest region after each `>` / `>>` (O(n)); check raw + quote-stripped.
|
|
565
|
+
for (const hay of [lower, stripped]) {
|
|
566
|
+
for (let i = 0; i < hay.length; i++) {
|
|
567
|
+
if (hay[i] !== ">")
|
|
568
|
+
continue;
|
|
569
|
+
let j = i + 1;
|
|
570
|
+
if (j < hay.length && hay[j] === ">")
|
|
571
|
+
j++;
|
|
572
|
+
let end = j;
|
|
573
|
+
while (end < hay.length &&
|
|
574
|
+
hay[end] !== "|" &&
|
|
575
|
+
hay[end] !== ";" &&
|
|
576
|
+
hay[end] !== "&" &&
|
|
577
|
+
hay[end] !== "\n") {
|
|
578
|
+
end++;
|
|
579
|
+
}
|
|
580
|
+
const dest = hay.slice(j, end);
|
|
581
|
+
for (const name of KILL_SWITCH_BASENAMES) {
|
|
582
|
+
if (dest.includes(name))
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
274
585
|
}
|
|
275
586
|
}
|
|
276
|
-
//
|
|
587
|
+
// Downloader/decoder destinations: curl -o .deft-directive-disable, scp, … (#3206 / #3213).
|
|
588
|
+
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
589
|
+
if (pathishMentionsKillSwitch(dest))
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
// Write/destructive + symlink plant bins with a kill-switch path argument (#3213: ln/link/mklink).
|
|
277
593
|
const killWriteBins = new Set([
|
|
278
594
|
...INDIRECT_WRITE_BINS,
|
|
595
|
+
...SYMLINK_PLANT_BINS,
|
|
279
596
|
"touch",
|
|
280
597
|
"new-item",
|
|
281
598
|
"ni",
|
|
@@ -284,26 +601,27 @@ function hasKillSwitchShellWrite(command, tokens) {
|
|
|
284
601
|
"type",
|
|
285
602
|
]);
|
|
286
603
|
for (let ti = 0; ti < tokens.length; ti++) {
|
|
287
|
-
|
|
604
|
+
const binTok = normalizeToken(tokens[ti]);
|
|
605
|
+
const bare = binBareName(tokens[ti]);
|
|
606
|
+
if (!killWriteBins.has(binTok) && !killWriteBins.has(bare))
|
|
288
607
|
continue;
|
|
289
608
|
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (p.includes(name))
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
609
|
+
if (pathishMentionsKillSwitch(pathishToken(tokens[tj])))
|
|
610
|
+
return true;
|
|
295
611
|
}
|
|
296
612
|
}
|
|
297
|
-
// Bare `touch .deft-directive-disable`
|
|
613
|
+
// Bare `touch .deft-directive-disable` / `ln -sf x .deft-directive-disable` — path later.
|
|
298
614
|
for (const t of tokens) {
|
|
299
615
|
const p = pathishToken(t);
|
|
300
616
|
for (const name of KILL_SWITCH_BASENAMES) {
|
|
301
617
|
// Exact basename or ends with /basename
|
|
302
618
|
if (p === name || p.endsWith(`/${name}`)) {
|
|
303
|
-
// Require some write shape (redirect already handled; touch/ni/echo/…)
|
|
619
|
+
// Require some write shape (redirect already handled; touch/ni/echo/ln/…)
|
|
304
620
|
if (hasWriteShape(command, tokens) ||
|
|
305
621
|
lower.includes("touch") ||
|
|
306
|
-
lower.includes("new-item")
|
|
622
|
+
lower.includes("new-item") ||
|
|
623
|
+
SYMLINK_PLANT_BINS.has(binBareName(tokens[0])) ||
|
|
624
|
+
tokens.some((tok) => SYMLINK_PLANT_BINS.has(binBareName(tok)))) {
|
|
307
625
|
return true;
|
|
308
626
|
}
|
|
309
627
|
}
|
|
@@ -476,42 +794,68 @@ function pathishToken(token) {
|
|
|
476
794
|
return token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
|
|
477
795
|
}
|
|
478
796
|
/**
|
|
479
|
-
*
|
|
797
|
+
* True when a pathish string targets `.deft/authz` (after quote strip / slash normalize).
|
|
798
|
+
* Quote-split forms like `'.deft/'authz'/grants/x'` become `.deft/authz/grants/x` via pathishToken.
|
|
799
|
+
*/
|
|
800
|
+
function pathishIsAuthzDir(pathish) {
|
|
801
|
+
return pathish.includes(".deft/authz");
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Shell **write** targeting `.deft/authz/` (#3110 AC-3 / #3206 / #3213).
|
|
480
805
|
* Pure reads (`cat .deft/authz/state.json`) stay unclassifiable — use `authz:show`.
|
|
481
806
|
* Redirects only count when the destination region contains `.deft/authz`.
|
|
807
|
+
* Pathish/token checks run even when the raw command lacks contiguous `.deft/authz`
|
|
808
|
+
* text (quote-split residual: `cp x '.deft/'authz'/grants/y'`).
|
|
482
809
|
*/
|
|
483
810
|
function hasAuthzDirShellWrite(command, tokens) {
|
|
484
811
|
const lower = command.toLowerCase().replace(/\\/g, "/");
|
|
485
|
-
|
|
486
|
-
|
|
812
|
+
// Quote-stripped contiguous form for redirect dest checks (#3213).
|
|
813
|
+
const stripped = lower.replace(/['"]/g, "");
|
|
487
814
|
// Redirect dest region after each `>` / `>>` (O(n); no nested-quantifier regex).
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
j
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
815
|
+
// Check both raw and quote-stripped so quote-split dests still match.
|
|
816
|
+
for (const hay of [lower, stripped]) {
|
|
817
|
+
for (let i = 0; i < hay.length; i++) {
|
|
818
|
+
if (hay[i] !== ">")
|
|
819
|
+
continue;
|
|
820
|
+
let j = i + 1;
|
|
821
|
+
if (j < hay.length && hay[j] === ">")
|
|
822
|
+
j++;
|
|
823
|
+
// Dest until pipe/semicolon/ampersand/newline.
|
|
824
|
+
let end = j;
|
|
825
|
+
while (end < hay.length &&
|
|
826
|
+
hay[end] !== "|" &&
|
|
827
|
+
hay[end] !== ";" &&
|
|
828
|
+
hay[end] !== "&" &&
|
|
829
|
+
hay[end] !== "\n") {
|
|
830
|
+
end++;
|
|
831
|
+
}
|
|
832
|
+
if (hay.slice(j, end).includes(".deft/authz"))
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
505
835
|
}
|
|
506
|
-
// Write/destructive bins with an authz path argument
|
|
836
|
+
// Write/destructive + symlink plant bins with an authz path argument (pathish =
|
|
837
|
+
// quote-strip resistant). Always run — do not gate on contiguous `.deft/authz`
|
|
838
|
+
// in the raw command (#3213). SYMLINK_PLANT_BINS: `ln -s forged .deft/authz/grants/x`
|
|
839
|
+
// must not fail-open as unclassifiable (SLizard residual).
|
|
507
840
|
for (let ti = 0; ti < tokens.length; ti++) {
|
|
508
|
-
|
|
841
|
+
const bare = binBareName(tokens[ti]);
|
|
842
|
+
const n = normalizeToken(tokens[ti]);
|
|
843
|
+
if (!INDIRECT_WRITE_BINS.has(n) &&
|
|
844
|
+
!INDIRECT_WRITE_BINS.has(bare) &&
|
|
845
|
+
!SYMLINK_PLANT_BINS.has(bare)) {
|
|
509
846
|
continue;
|
|
847
|
+
}
|
|
510
848
|
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
511
|
-
if (pathishToken(tokens[tj])
|
|
849
|
+
if (pathishIsAuthzDir(pathishToken(tokens[tj])))
|
|
512
850
|
return true;
|
|
513
851
|
}
|
|
514
852
|
}
|
|
853
|
+
// Downloader/decoder destinations under .deft/authz (#3206 / #3213 scp/aria2c/certutil).
|
|
854
|
+
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
855
|
+
if (pathishIsAuthzDir(dest))
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
// Contiguous mention without write shape stays false (reads like cat .deft/authz/…).
|
|
515
859
|
return false;
|
|
516
860
|
}
|
|
517
861
|
/** Write/destructive shell bins (token match after normalizeToken). */
|
|
@@ -38,7 +38,47 @@ export interface ConsumerCheckContractOptions {
|
|
|
38
38
|
/** When true, missing CI references warn only (default true for migration). */
|
|
39
39
|
readonly ciWarnOnly?: boolean;
|
|
40
40
|
readonly enforce?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Inject included framework Taskfile text (canonical `.deft/core/Taskfile.yml`).
|
|
43
|
+
* When undefined, resolve from disk via {@link resolveCanonicalDeftTaskfileInclude}.
|
|
44
|
+
* #3218 greenfield include-only consumers.
|
|
45
|
+
*/
|
|
46
|
+
readonly includedFrameworkTaskfileText?: string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Inject included framework `tasks/verify.yml` text next to the canonical include.
|
|
49
|
+
* When undefined, resolve from disk beside the included Taskfile.
|
|
50
|
+
*/
|
|
51
|
+
readonly includedVerifyTaskfileText?: string | null;
|
|
41
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Relative path forms accepted for the deft-install canonical Taskfile include
|
|
55
|
+
* (init-deposit `MINIMAL_TASKFILE` / `CANONICAL_TASKFILE_INCLUDE`).
|
|
56
|
+
* Use only as a path-value matcher — never as a whole-file text search (Greptile P1 #3218).
|
|
57
|
+
*/
|
|
58
|
+
export declare const CANONICAL_DEFT_TASKFILE_PATH_RE: RegExp;
|
|
59
|
+
/** @deprecated Prefer path/line parsers; kept for export stability. */
|
|
60
|
+
export declare const CANONICAL_DEFT_TASKFILE_INCLUDE_RE: RegExp;
|
|
61
|
+
/**
|
|
62
|
+
* Return the relative path of the canonical `.deft/core/Taskfile.yml` include when
|
|
63
|
+
* present as an **active, direct** go-task `includes:` entry; otherwise null.
|
|
64
|
+
*
|
|
65
|
+
* Greenfield `directive init` deposits an include-only Taskfile (#3218); composition
|
|
66
|
+
* lives in the included framework graph, not in root `check` deps.
|
|
67
|
+
*
|
|
68
|
+
* Greptile P1 (#3218):
|
|
69
|
+
* - Path text in comments / cmds / vars / non-include keys must not match.
|
|
70
|
+
* - Only **direct** include entries count: short form `deft: path` or object form
|
|
71
|
+
* `deft: { taskfile: path }` (immediate property). Nested
|
|
72
|
+
* `includes.some.nested.taskfile` is not an executable go-task include.
|
|
73
|
+
* - Namespace must be `deft` so the documented `task deft:check` entrypoint exists.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveCanonicalDeftTaskfileInclude(rootTaskfileText: string): string | null;
|
|
76
|
+
/**
|
|
77
|
+
* True when a framework (included) Taskfile composes every required gate via
|
|
78
|
+
* `check` / `check:consumer` / `check:framework-source` deps or an orchestrator body,
|
|
79
|
+
* and `tasks/verify.yml` defines the local task keys.
|
|
80
|
+
*/
|
|
81
|
+
export declare function frameworkTaskfileComposesRequiredGates(frameworkTaskfileText: string, verifyTaskfileText: string | null, required?: readonly string[]): boolean;
|
|
42
82
|
/** True if `text` mentions the gate as a task dep, script, or CLI invocation. */
|
|
43
83
|
export declare function textReferencesGate(text: string, gateId: string): boolean;
|
|
44
84
|
/**
|