@deftai/directive-core 0.98.1 → 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.
Files changed (55) hide show
  1. package/dist/authz/classify.js +265 -73
  2. package/dist/consumer-check-contract/evaluate.d.ts +40 -0
  3. package/dist/consumer-check-contract/evaluate.js +188 -3
  4. package/dist/consumer-check-contract/index.d.ts +1 -1
  5. package/dist/consumer-check-contract/index.js +1 -1
  6. package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
  7. package/dist/content-contracts/skills/greptile-detector.js +202 -4
  8. package/dist/decision/index.d.ts +17 -0
  9. package/dist/decision/index.js +35 -0
  10. package/dist/decision/list.d.ts +47 -0
  11. package/dist/decision/list.js +250 -0
  12. package/dist/decision/schema.d.ts +88 -0
  13. package/dist/decision/schema.js +293 -0
  14. package/dist/decision/write.d.ts +82 -0
  15. package/dist/decision/write.js +427 -0
  16. package/dist/eval/report.d.ts +29 -0
  17. package/dist/eval/report.js +69 -0
  18. package/dist/eval/run.d.ts +9 -0
  19. package/dist/eval/run.js +40 -4
  20. package/dist/eval/version-pin.d.ts +99 -0
  21. package/dist/eval/version-pin.js +181 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/platform/host-content-surface.d.ts +74 -0
  25. package/dist/platform/host-content-surface.js +214 -0
  26. package/dist/platform/index.d.ts +1 -0
  27. package/dist/platform/index.js +1 -0
  28. package/dist/policy/ceremony-dial.d.ts +233 -0
  29. package/dist/policy/ceremony-dial.js +829 -0
  30. package/dist/policy/deft-directive-disable.js +12 -2
  31. package/dist/policy/index.d.ts +1 -0
  32. package/dist/policy/index.js +15 -1
  33. package/dist/pr-merge-readiness/evaluate.js +10 -0
  34. package/dist/pr-merge-readiness/mergeability.js +5 -0
  35. package/dist/pr-merge-readiness/output.js +2 -0
  36. package/dist/pr-merge-readiness/parse.js +4 -0
  37. package/dist/pr-merge-readiness/types.d.ts +6 -0
  38. package/dist/scope/effort-activate-gate.d.ts +28 -0
  39. package/dist/scope/effort-activate-gate.js +64 -0
  40. package/dist/scope/index.d.ts +1 -0
  41. package/dist/scope/index.js +1 -0
  42. package/dist/scope/transition.js +8 -0
  43. package/dist/session/session-start.d.ts +24 -1
  44. package/dist/session/session-start.js +183 -26
  45. package/dist/swarm/index.d.ts +2 -0
  46. package/dist/swarm/index.js +2 -0
  47. package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
  48. package/dist/swarm/pre-dispatch-cli.js +143 -0
  49. package/dist/swarm/pre-dispatch.d.ts +87 -0
  50. package/dist/swarm/pre-dispatch.js +373 -0
  51. package/dist/vbrief-activate/activate.js +6 -0
  52. package/dist/vbrief-validate/constants.d.ts +2 -0
  53. package/dist/vbrief-validate/constants.js +2 -0
  54. package/dist/vbrief-validate/schema.js +4 -1
  55. package/package.json +15 -3
@@ -7,7 +7,12 @@
7
7
  * (CodeQL js/polynomial-redos).
8
8
  */
9
9
  import { classifyMcpTool, classifyShellCommand, listShellOps, } from "../policy/runtime-authority.js";
10
- /** Split on whitespace without nested quantifiers (O(n)). */
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,13 +20,24 @@ function shellTokens(command) {
15
20
  const c = command[i];
16
21
  if (c === undefined)
17
22
  break;
18
- if (c === " " || c === "\t" || c === "\n" || c === "\r") {
23
+ if (c === " " || c === "\t" || c === "\r") {
19
24
  if (cur.length > 0) {
20
25
  out.push(cur);
21
26
  cur = "";
22
27
  }
23
28
  continue;
24
29
  }
30
+ if (c === "\n") {
31
+ if (cur.length > 0) {
32
+ out.push(cur);
33
+ cur = "";
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
+ }
39
+ continue;
40
+ }
25
41
  cur += c;
26
42
  }
27
43
  if (cur.length > 0)
@@ -163,14 +179,24 @@ const POLICY_AUTHORITY_MUTATORS = new Set([
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"];
165
181
  /**
166
- * Downloaders / decoders that can plant files without shell redirects (#3206).
167
- * Not in INDIRECT_WRITE_BINS: those feed hasWriteShape and would classify bare
168
- * `curl $URL` as a store write via opaque-expansion heuristics.
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.
169
185
  */
170
- const DOWNLOADER_DECODER_BINS = new Set(["curl", "wget", "xxd", "openssl"]);
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
+ ]);
171
196
  /**
172
197
  * File destination flags for downloaders/decoders (#3206).
173
198
  * normalizeToken lowercases, so wget `-O` and curl `-o` share `-o`.
199
+ * scp: `-o` is an SSH option — excluded in isDownloaderDestFlag.
174
200
  */
175
201
  const DOWNLOADER_FILE_DEST_FLAGS = new Set([
176
202
  "-o",
@@ -179,9 +205,15 @@ const DOWNLOADER_FILE_DEST_FLAGS = new Set([
179
205
  "-out",
180
206
  "--out",
181
207
  ]);
182
- /** Directory destination flags (curl --output-dir / wget -P); bin-scoped below. */
208
+ /** Directory destination flags (curl --output-dir / wget -P / aria2c -d); bin-scoped below. */
183
209
  const CURL_DIR_DEST_FLAGS = new Set(["--output-dir"]);
184
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"]);
185
217
  /** Final path segment of a token (path-qualified bins / .exe). */
186
218
  function binBareName(token) {
187
219
  const pathish = token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
@@ -196,18 +228,73 @@ function isDownloaderDecoderBin(token) {
196
228
  return DOWNLOADER_DECODER_BINS.has(binBareName(token));
197
229
  }
198
230
  function isDownloaderDestFlag(flag, bin) {
231
+ // scp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
232
+ if (bin === "scp")
233
+ return false;
199
234
  if (DOWNLOADER_FILE_DEST_FLAGS.has(flag))
200
235
  return true;
201
236
  if (bin === "curl" && CURL_DIR_DEST_FLAGS.has(flag))
202
237
  return true;
203
238
  if (bin === "wget" && WGET_DIR_DEST_FLAGS.has(flag))
204
239
  return true;
240
+ if (bin === "aria2c" && ARIA2C_DIR_DEST_FLAGS.has(flag))
241
+ return true;
205
242
  return false;
206
243
  }
207
244
  /**
208
- * Destinations from curl/wget/xxd/openssl via -o/--output/-O/-out/--output-dir/-P
209
- * (separate, =value, or attached short form) and xxd -r path-like write positionals (#3206).
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).
210
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.
211
298
  * O(n) token walk — no nested-quantifier regex on untrusted input.
212
299
  */
213
300
  function downloaderDecoderDestinations(tokens) {
@@ -222,9 +309,22 @@ function downloaderDecoderDestinations(tokens) {
222
309
  i++;
223
310
  // xxd reverse mode writes; without -r, path positionals are dump inputs (read).
224
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 = [];
225
319
  while (i < tokens.length) {
226
320
  const raw = tokens[i];
227
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
+ }
228
328
  // New bare bin starts another command segment (not pathish ./wget operands).
229
329
  if (!n.startsWith("-") &&
230
330
  !raw.includes("/") &&
@@ -247,18 +347,33 @@ function downloaderDecoderDestinations(tokens) {
247
347
  i++;
248
348
  continue;
249
349
  }
250
- // --flag=value (and rare -out=value)
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.
251
364
  if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
252
365
  const eq = raw.indexOf("=");
253
366
  const flag = normalizeToken(raw.slice(0, eq));
254
367
  if (isDownloaderDestFlag(flag, bin)) {
255
368
  dests.push(pathishToken(raw.slice(eq + 1)));
256
369
  }
370
+ // scp attached -oProxyCommand=… is not a file dest — skip without recording.
257
371
  i++;
258
372
  continue;
259
373
  }
260
374
  // Attached short: -oPATH / -OPATH (after lowercasing both are -opath…)
261
- if (n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
375
+ // Skip for scp (OpenSSH -oOption=Value attached forms are not file dests).
376
+ if (bin !== "scp" && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
262
377
  if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output")) {
263
378
  dests.push(pathishToken(raw.slice(4)));
264
379
  i++;
@@ -275,11 +390,17 @@ function downloaderDecoderDestinations(tokens) {
275
390
  i++;
276
391
  continue;
277
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
+ }
278
399
  }
279
- // Separate value: -o PATH / --output PATH / -out PATH / --output-dir / -P
400
+ // Separate value: -o PATH / --output PATH / -out PATH / --output-dir / -P / -d
280
401
  if (isDownloaderDestFlag(n, bin)) {
281
402
  const next = tokens[i + 1];
282
- if (next !== undefined && !String(next).startsWith("-")) {
403
+ if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
283
404
  dests.push(pathishToken(next));
284
405
  i += 2;
285
406
  continue;
@@ -297,8 +418,36 @@ function downloaderDecoderDestinations(tokens) {
297
418
  dests.push(p);
298
419
  }
299
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
+ }
300
442
  i++;
301
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
+ }
302
451
  }
303
452
  return dests;
304
453
  }
@@ -377,52 +526,73 @@ function hasPolicyAuthorityMutator(tokens) {
377
526
  }
378
527
  return false;
379
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
+ }
380
538
  /**
381
- * Shell write targeting `.deft-directive-disable` / `.no-deft-directive` (#3186 / #3039).
539
+ * Shell write targeting `.deft-directive-disable` / `.no-deft-directive` (#3186 / #3039 / #3213).
382
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.
383
542
  */
384
543
  function hasKillSwitchShellWrite(command, tokens) {
385
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, "");
386
547
  let mentionsKill = false;
387
548
  for (const name of KILL_SWITCH_BASENAMES) {
388
- if (lower.includes(name)) {
549
+ if (lower.includes(name) || stripped.includes(name)) {
389
550
  mentionsKill = true;
390
551
  break;
391
552
  }
392
553
  }
554
+ if (!mentionsKill) {
555
+ for (const t of tokens) {
556
+ if (pathishMentionsKillSwitch(pathishToken(t))) {
557
+ mentionsKill = true;
558
+ break;
559
+ }
560
+ }
561
+ }
393
562
  if (!mentionsKill)
394
563
  return false;
395
- // Redirect dest region after each `>` / `>>` (O(n)).
396
- for (let i = 0; i < lower.length; i++) {
397
- if (lower[i] !== ">")
398
- continue;
399
- let j = i + 1;
400
- if (j < lower.length && lower[j] === ">")
401
- j++;
402
- let end = j;
403
- while (end < lower.length &&
404
- lower[end] !== "|" &&
405
- lower[end] !== ";" &&
406
- lower[end] !== "&" &&
407
- lower[end] !== "\n") {
408
- end++;
409
- }
410
- const dest = lower.slice(j, end);
411
- for (const name of KILL_SWITCH_BASENAMES) {
412
- if (dest.includes(name))
413
- return true;
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
+ }
414
585
  }
415
586
  }
416
- // Downloader/decoder destinations: curl -o .deft-directive-disable, wget -O, … (#3206).
587
+ // Downloader/decoder destinations: curl -o .deft-directive-disable, scp, … (#3206 / #3213).
417
588
  for (const dest of downloaderDecoderDestinations(tokens)) {
418
- for (const name of KILL_SWITCH_BASENAMES) {
419
- if (dest.includes(name))
420
- return true;
421
- }
589
+ if (pathishMentionsKillSwitch(dest))
590
+ return true;
422
591
  }
423
- // Write/destructive bins with a kill-switch path argument (touch, New-Item, …).
592
+ // Write/destructive + symlink plant bins with a kill-switch path argument (#3213: ln/link/mklink).
424
593
  const killWriteBins = new Set([
425
594
  ...INDIRECT_WRITE_BINS,
595
+ ...SYMLINK_PLANT_BINS,
426
596
  "touch",
427
597
  "new-item",
428
598
  "ni",
@@ -431,26 +601,27 @@ function hasKillSwitchShellWrite(command, tokens) {
431
601
  "type",
432
602
  ]);
433
603
  for (let ti = 0; ti < tokens.length; ti++) {
434
- if (!killWriteBins.has(normalizeToken(tokens[ti])))
604
+ const binTok = normalizeToken(tokens[ti]);
605
+ const bare = binBareName(tokens[ti]);
606
+ if (!killWriteBins.has(binTok) && !killWriteBins.has(bare))
435
607
  continue;
436
608
  for (let tj = ti + 1; tj < tokens.length; tj++) {
437
- const p = pathishToken(tokens[tj]);
438
- for (const name of KILL_SWITCH_BASENAMES) {
439
- if (p.includes(name))
440
- return true;
441
- }
609
+ if (pathishMentionsKillSwitch(pathishToken(tokens[tj])))
610
+ return true;
442
611
  }
443
612
  }
444
- // Bare `touch .deft-directive-disable` first token may be touch, path later.
613
+ // Bare `touch .deft-directive-disable` / `ln -sf x .deft-directive-disable` path later.
445
614
  for (const t of tokens) {
446
615
  const p = pathishToken(t);
447
616
  for (const name of KILL_SWITCH_BASENAMES) {
448
617
  // Exact basename or ends with /basename
449
618
  if (p === name || p.endsWith(`/${name}`)) {
450
- // Require some write shape (redirect already handled; touch/ni/echo/…)
619
+ // Require some write shape (redirect already handled; touch/ni/echo/ln/…)
451
620
  if (hasWriteShape(command, tokens) ||
452
621
  lower.includes("touch") ||
453
- 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)))) {
454
625
  return true;
455
626
  }
456
627
  }
@@ -623,47 +794,68 @@ function pathishToken(token) {
623
794
  return token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
624
795
  }
625
796
  /**
626
- * Shell **write** targeting `.deft/authz/` (#3110 AC-3).
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).
627
805
  * Pure reads (`cat .deft/authz/state.json`) stay unclassifiable — use `authz:show`.
628
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'`).
629
809
  */
630
810
  function hasAuthzDirShellWrite(command, tokens) {
631
811
  const lower = command.toLowerCase().replace(/\\/g, "/");
632
- if (!lower.includes(".deft/authz"))
633
- return false;
812
+ // Quote-stripped contiguous form for redirect dest checks (#3213).
813
+ const stripped = lower.replace(/['"]/g, "");
634
814
  // Redirect dest region after each `>` / `>>` (O(n); no nested-quantifier regex).
635
- for (let i = 0; i < lower.length; i++) {
636
- if (lower[i] !== ">")
637
- continue;
638
- let j = i + 1;
639
- if (j < lower.length && lower[j] === ">")
640
- j++;
641
- // Dest until pipe/semicolon/ampersand/newline.
642
- let end = j;
643
- while (end < lower.length &&
644
- lower[end] !== "|" &&
645
- lower[end] !== ";" &&
646
- lower[end] !== "&" &&
647
- lower[end] !== "\n") {
648
- end++;
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;
649
834
  }
650
- if (lower.slice(j, end).includes(".deft/authz"))
651
- return true;
652
835
  }
653
- // 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).
654
840
  for (let ti = 0; ti < tokens.length; ti++) {
655
- if (!INDIRECT_WRITE_BINS.has(normalizeToken(tokens[ti])))
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)) {
656
846
  continue;
847
+ }
657
848
  for (let tj = ti + 1; tj < tokens.length; tj++) {
658
- if (pathishToken(tokens[tj]).includes(".deft/authz"))
849
+ if (pathishIsAuthzDir(pathishToken(tokens[tj])))
659
850
  return true;
660
851
  }
661
852
  }
662
- // Downloader/decoder destinations under .deft/authz (#3206 residual after #3186).
853
+ // Downloader/decoder destinations under .deft/authz (#3206 / #3213 scp/aria2c/certutil).
663
854
  for (const dest of downloaderDecoderDestinations(tokens)) {
664
- if (dest.includes(".deft/authz"))
855
+ if (pathishIsAuthzDir(dest))
665
856
  return true;
666
857
  }
858
+ // Contiguous mention without write shape stays false (reads like cat .deft/authz/…).
667
859
  return false;
668
860
  }
669
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
  /**