@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.
- package/dist/authz/classify.js +265 -73
- 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/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,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 === "\
|
|
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
|
|
167
|
-
* Not in INDIRECT_WRITE_BINS: those feed hasWriteShape
|
|
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([
|
|
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
|
-
*
|
|
209
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
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 (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
j
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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,
|
|
587
|
+
// Downloader/decoder destinations: curl -o .deft-directive-disable, scp, … (#3206 / #3213).
|
|
417
588
|
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return true;
|
|
421
|
-
}
|
|
589
|
+
if (pathishMentionsKillSwitch(dest))
|
|
590
|
+
return true;
|
|
422
591
|
}
|
|
423
|
-
// Write/destructive bins with a kill-switch path argument (
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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`
|
|
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
|
-
*
|
|
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
|
-
|
|
633
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
j
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
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])
|
|
849
|
+
if (pathishIsAuthzDir(pathishToken(tokens[tj])))
|
|
659
850
|
return true;
|
|
660
851
|
}
|
|
661
852
|
}
|
|
662
|
-
// Downloader/decoder destinations under .deft/authz (#3206
|
|
853
|
+
// Downloader/decoder destinations under .deft/authz (#3206 / #3213 scp/aria2c/certutil).
|
|
663
854
|
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
664
|
-
if (dest
|
|
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
|
/**
|