@deftai/directive-core 0.104.0 → 0.105.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 +526 -42
- package/dist/check/cached-orchestrator.d.ts +1 -1
- package/dist/check/cached-orchestrator.js +2 -2
- package/dist/check/named-cause.d.ts +1 -1
- package/dist/check/named-cause.js +33 -3
- package/dist/codebase/default-extractor.d.ts +5 -0
- package/dist/codebase/default-extractor.js +7 -15
- package/dist/deposit/copy-tree.js +60 -0
- package/dist/deposit/python-free.js +26 -18
- package/dist/doctor/checks.d.ts +8 -0
- package/dist/doctor/checks.js +37 -25
- package/dist/doctor/doctor-state.d.ts +4 -0
- package/dist/doctor/doctor-state.js +7 -1
- package/dist/doctor/main.js +9 -19
- package/dist/doctor/types.d.ts +9 -0
- package/dist/fs/contained-write.d.ts +115 -1
- package/dist/fs/contained-write.js +241 -5
- package/dist/fs/mutation-ledger.d.ts +54 -0
- package/dist/fs/mutation-ledger.js +133 -0
- package/dist/fs/non-product-dirs.d.ts +79 -0
- package/dist/fs/non-product-dirs.js +96 -0
- package/dist/hooks/dispatcher.d.ts +3 -1
- package/dist/hooks/dispatcher.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/agent-hooks.js +17 -15
- package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
- package/dist/init-deposit/core-guard-pin-content.js +393 -0
- package/dist/init-deposit/gitignore.js +9 -0
- package/dist/init-deposit/hygiene.d.ts +47 -11
- package/dist/init-deposit/hygiene.js +172 -37
- package/dist/init-deposit/legacy-detect.js +6 -2
- package/dist/init-deposit/refresh.d.ts +21 -1
- package/dist/init-deposit/refresh.js +106 -9
- package/dist/init-deposit/scaffold.js +28 -279
- package/dist/init-deposit/slash-deposit.js +13 -7
- package/dist/init-deposit/xbrief-projections.d.ts +8 -1
- package/dist/init-deposit/xbrief-projections.js +24 -11
- package/dist/intake/clause-derivation.d.ts +38 -1
- package/dist/intake/clause-derivation.js +211 -9
- package/dist/intake/issue-ingest.d.ts +3 -1
- package/dist/intake/issue-ingest.js +35 -6
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
- package/dist/literal-acceptance/capture.d.ts +10 -0
- package/dist/literal-acceptance/capture.js +224 -75
- package/dist/literal-acceptance/evaluate.d.ts +29 -4
- package/dist/literal-acceptance/evaluate.js +70 -2
- package/dist/literal-acceptance/index.d.ts +3 -3
- package/dist/literal-acceptance/index.js +3 -3
- package/dist/literal-acceptance/safety.d.ts +41 -0
- package/dist/literal-acceptance/safety.js +126 -2
- package/dist/literal-acceptance/types.d.ts +8 -0
- package/dist/orphan-active/evaluate.d.ts +7 -1
- package/dist/orphan-active/evaluate.js +102 -23
- package/dist/platform/platform-capabilities.js +2 -3
- package/dist/policy/base-branch.d.ts +37 -0
- package/dist/policy/base-branch.js +114 -0
- package/dist/policy/branch-sync.d.ts +80 -0
- package/dist/policy/branch-sync.js +211 -0
- package/dist/policy/ceremony-dial-escalation.js +1 -0
- package/dist/policy/ceremony-dial.js +4 -14
- package/dist/policy/delivery-branch.d.ts +5 -3
- package/dist/policy/delivery-branch.js +7 -3
- package/dist/policy/file-size-thresholds.d.ts +24 -0
- package/dist/policy/file-size-thresholds.js +23 -0
- package/dist/policy/forge-outage-retry.d.ts +47 -0
- package/dist/policy/forge-outage-retry.js +157 -0
- package/dist/policy/index.d.ts +7 -0
- package/dist/policy/index.js +66 -7
- package/dist/policy/project-invariants.d.ts +50 -0
- package/dist/policy/project-invariants.js +205 -0
- package/dist/policy/sync-default.d.ts +129 -0
- package/dist/policy/sync-default.js +452 -0
- package/dist/policy/sync-max-files.d.ts +93 -0
- package/dist/policy/sync-max-files.js +192 -0
- package/dist/pr-monitor/absent-required.d.ts +11 -0
- package/dist/pr-monitor/absent-required.js +61 -0
- package/dist/pr-monitor/constants.d.ts +7 -0
- package/dist/pr-monitor/constants.js +7 -0
- package/dist/pr-monitor/index.d.ts +2 -1
- package/dist/pr-monitor/index.js +2 -1
- package/dist/pr-monitor/main.js +4 -0
- package/dist/pr-monitor/monitor.js +24 -1
- package/dist/pr-wait-mergeable/classify.js +6 -0
- package/dist/preflight/evaluate.d.ts +4 -0
- package/dist/preflight/evaluate.js +35 -1
- package/dist/preflight/index.d.ts +4 -0
- package/dist/preflight/index.js +2 -0
- package/dist/preflight/intended-placement.d.ts +62 -0
- package/dist/preflight/intended-placement.js +275 -0
- package/dist/preflight/project-invariants-gate.d.ts +29 -0
- package/dist/preflight/project-invariants-gate.js +82 -0
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
- package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
- package/dist/product-first-done-gate/acceptance.d.ts +7 -4
- package/dist/product-first-done-gate/acceptance.js +79 -8
- package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
- package/dist/product-first-done-gate/empty-resolution.js +12 -1
- package/dist/product-first-done-gate/evaluate.d.ts +12 -0
- package/dist/product-first-done-gate/evaluate.js +254 -31
- package/dist/product-first-done-gate/index.d.ts +2 -0
- package/dist/product-first-done-gate/index.js +1 -0
- package/dist/release/build-dist.d.ts +101 -0
- package/dist/release/build-dist.js +428 -46
- package/dist/run-summary/emit.d.ts +16 -10
- package/dist/run-summary/emit.js +26 -15
- package/dist/run-summary/index.d.ts +3 -2
- package/dist/run-summary/index.js +2 -1
- package/dist/run-summary/session-id.d.ts +21 -0
- package/dist/run-summary/session-id.js +92 -0
- package/dist/run-summary/types.d.ts +29 -4
- package/dist/run-summary/types.js +1 -0
- package/dist/scope/acceptance-evidence.d.ts +19 -7
- package/dist/scope/acceptance-evidence.js +38 -11
- package/dist/scope/delivery-evidence.d.ts +3 -3
- package/dist/scope/delivery-evidence.js +22 -27
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/parent-lineage.js +7 -6
- package/dist/scope/project-invariant-coverage.d.ts +54 -0
- package/dist/scope/project-invariant-coverage.js +322 -0
- package/dist/scope/transition.js +1 -1
- package/dist/scope-provenance/compare-intent.d.ts +27 -0
- package/dist/scope-provenance/compare-intent.js +233 -0
- package/dist/scope-provenance/digest.d.ts +18 -1
- package/dist/scope-provenance/digest.js +16 -11
- package/dist/scope-provenance/evaluate.d.ts +5 -1
- package/dist/scope-provenance/evaluate.js +64 -4
- package/dist/scope-provenance/extract-intent.d.ts +43 -0
- package/dist/scope-provenance/extract-intent.js +262 -0
- package/dist/scope-provenance/index.d.ts +7 -1
- package/dist/scope-provenance/index.js +7 -1
- package/dist/scope-provenance/intent-digest.d.ts +12 -0
- package/dist/scope-provenance/intent-digest.js +33 -0
- package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
- package/dist/scope-provenance/intent-evaluate.js +149 -0
- package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
- package/dist/scope-provenance/json-tokenizer.js +225 -0
- package/dist/scope-provenance/known-machine.d.ts +38 -0
- package/dist/scope-provenance/known-machine.js +157 -0
- package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
- package/dist/scope-provenance/mint-artifacts.js +425 -0
- package/dist/session/ac-pass-banking.d.ts +4 -36
- package/dist/session/ac-pass-banking.js +30 -77
- package/dist/session/ac-pass-reuse.d.ts +53 -0
- package/dist/session/ac-pass-reuse.js +95 -0
- package/dist/session/git.js +19 -4
- package/dist/session/index.d.ts +4 -0
- package/dist/session/index.js +4 -0
- package/dist/session/occupancy-stress-worker.d.ts +7 -0
- package/dist/session/occupancy-stress-worker.js +36 -0
- package/dist/session/occupancy.d.ts +97 -0
- package/dist/session/occupancy.js +422 -0
- package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
- package/dist/session/product-state-hash.d.ts +26 -0
- package/dist/session/product-state-hash.js +683 -0
- package/dist/session/session-ready.d.ts +2 -0
- package/dist/session/session-ready.js +52 -30
- package/dist/session/session-start.d.ts +7 -0
- package/dist/session/session-start.js +71 -7
- package/dist/session/verify-ac-session-cache.d.ts +42 -0
- package/dist/session/verify-ac-session-cache.js +126 -0
- package/dist/session/verify-session-ritual.d.ts +5 -0
- package/dist/session/verify-session-ritual.js +9 -0
- package/dist/slash/openclaw-deposit.js +13 -9
- package/dist/slice/constants.d.ts +2 -2
- package/dist/slice/constants.js +2 -2
- package/dist/slice/existing.js +9 -3
- package/dist/slice/lock.d.ts +16 -1
- package/dist/slice/lock.js +137 -11
- package/dist/story-ready/evaluate.d.ts +2 -0
- package/dist/story-ready/evaluate.js +13 -0
- package/dist/swarm/complete-cohort.d.ts +3 -0
- package/dist/swarm/complete-cohort.js +59 -11
- package/dist/swarm/finalize-cohort.js +33 -2
- package/dist/swarm/launch.d.ts +22 -0
- package/dist/swarm/launch.js +155 -0
- package/dist/swarm/routing-set-cli.d.ts +11 -1
- package/dist/swarm/routing-set-cli.js +13 -3
- package/dist/swarm/routing-verify.d.ts +12 -0
- package/dist/swarm/routing-verify.js +59 -20
- package/dist/swarm/routing.d.ts +14 -0
- package/dist/swarm/routing.js +53 -3
- package/dist/telemetry-coverage/fake-trial.js +10 -0
- package/dist/telemetry-coverage/kinds.js +2 -0
- package/dist/triage/actions/index.js +4 -1
- package/dist/ts-check-lane/index.d.ts +3 -0
- package/dist/ts-check-lane/index.js +2 -0
- package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
- package/dist/ts-check-lane/progress-reporter.js +38 -0
- package/dist/ts-check-lane/progress.d.ts +32 -0
- package/dist/ts-check-lane/progress.js +57 -0
- package/dist/ts-check-lane/run-lane.d.ts +3 -1
- package/dist/ts-check-lane/run-lane.js +8 -6
- package/dist/validate-content/validate-links.d.ts +8 -0
- package/dist/validate-content/validate-links.js +9 -15
- package/dist/value/feedback-file.js +12 -2
- package/dist/vbrief-build/index.d.ts +1 -0
- package/dist/vbrief-build/index.js +1 -0
- package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
- package/dist/vbrief-build/project-invariants-io.js +41 -0
- package/dist/vbrief-reconcile/graph.js +18 -1
- package/dist/vbrief-reconcile/index.d.ts +1 -1
- package/dist/vbrief-reconcile/index.js +1 -1
- package/dist/vbrief-reconcile/types.d.ts +13 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
- package/dist/vbrief-reconcile/umbrellas.js +261 -26
- package/dist/vbrief-validate/index.d.ts +1 -1
- package/dist/vbrief-validate/index.js +1 -1
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +68 -0
- package/dist/verify-ac/evaluate.d.ts +7 -1
- package/dist/verify-ac/evaluate.js +34 -26
- package/dist/verify-ac/flag.d.ts +4 -0
- package/dist/verify-ac/flag.js +16 -0
- package/dist/verify-ac/index.d.ts +2 -2
- package/dist/verify-ac/index.js +2 -2
- package/dist/verify-source/contained-writes.js +2 -0
- package/dist/verify-source/verify-stubs.d.ts +15 -1
- package/dist/verify-source/verify-stubs.js +20 -16
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
- package/dist/xbrief-migrate/migrate-project.js +2 -5
- package/package.json +3 -3
package/dist/authz/classify.js
CHANGED
|
@@ -262,10 +262,50 @@ const DOWNLOADER_DECODER_BINS = new Set([
|
|
|
262
262
|
"sqlite3",
|
|
263
263
|
"crane",
|
|
264
264
|
"objcopy",
|
|
265
|
+
// #3382 residual after #3354: dest forms that are not generic -o/--output/--outfile.
|
|
266
|
+
"cmake",
|
|
267
|
+
"script",
|
|
268
|
+
"gallery-dl",
|
|
269
|
+
"megadl",
|
|
270
|
+
"ncftpget",
|
|
271
|
+
"svn",
|
|
272
|
+
"fossil",
|
|
273
|
+
"bzr",
|
|
274
|
+
"cvs",
|
|
275
|
+
"darcs",
|
|
276
|
+
"ed",
|
|
277
|
+
"nvim",
|
|
278
|
+
"nano",
|
|
279
|
+
"vim",
|
|
280
|
+
"vi",
|
|
281
|
+
// #3421 residual after #3382: dest forms the harvest missed.
|
|
282
|
+
"git",
|
|
283
|
+
"ex",
|
|
284
|
+
"dos2unix",
|
|
285
|
+
"unix2dos",
|
|
286
|
+
"mac2unix",
|
|
287
|
+
"unix2mac",
|
|
288
|
+
"aws",
|
|
289
|
+
"pijul",
|
|
290
|
+
"pg_dump",
|
|
291
|
+
"pg_restore",
|
|
292
|
+
"pg_dumpall",
|
|
293
|
+
"convert",
|
|
294
|
+
"magick",
|
|
295
|
+
"mogrify",
|
|
296
|
+
"fallocate",
|
|
297
|
+
"new-item",
|
|
298
|
+
// #3459 residual after #3421: GNU g* peers, forge clone, iwr, fsutil.
|
|
299
|
+
"iwr",
|
|
300
|
+
"invoke-webrequest",
|
|
301
|
+
"fsutil",
|
|
302
|
+
"gh",
|
|
303
|
+
"glab",
|
|
304
|
+
"hub",
|
|
265
305
|
]);
|
|
266
306
|
/**
|
|
267
307
|
* Archive extractors / alt writers that can plant via pathish operands without
|
|
268
|
-
* shell redirects (#3245 / #3288 / #3311 / #3336 / #3354). Used for pathish authz/kill scans (prefer deny)
|
|
308
|
+
* shell redirects (#3245 / #3288 / #3311 / #3336 / #3354 / #3382). Used for pathish authz/kill scans (prefer deny)
|
|
269
309
|
* and write-shape residual under UAT — not bare curl-class URL fetches.
|
|
270
310
|
*/
|
|
271
311
|
const ARCHIVE_ALT_WRITE_BINS = new Set([
|
|
@@ -338,9 +378,30 @@ const ARCHIVE_ALT_WRITE_BINS = new Set([
|
|
|
338
378
|
"sqlite3",
|
|
339
379
|
"crane",
|
|
340
380
|
"objcopy",
|
|
381
|
+
// #3382 residual: dedicated dest-form writers (not general-purpose cmake/git/editors).
|
|
382
|
+
"gallery-dl",
|
|
383
|
+
"megadl",
|
|
384
|
+
"ncftpget",
|
|
385
|
+
// #3421 residual: dedicated dest-form writers (not general-purpose git/aws/editors).
|
|
386
|
+
"dos2unix",
|
|
387
|
+
"unix2dos",
|
|
388
|
+
"mac2unix",
|
|
389
|
+
"unix2mac",
|
|
390
|
+
"pijul",
|
|
391
|
+
"pg_dump",
|
|
392
|
+
"pg_restore",
|
|
393
|
+
"pg_dumpall",
|
|
394
|
+
"convert",
|
|
395
|
+
"magick",
|
|
396
|
+
"mogrify",
|
|
397
|
+
"fallocate",
|
|
398
|
+
// #3459 residual: dest writers that are not general-purpose gh/cmd.
|
|
399
|
+
"iwr",
|
|
400
|
+
"invoke-webrequest",
|
|
401
|
+
"fsutil",
|
|
341
402
|
]);
|
|
342
403
|
/**
|
|
343
|
-
* Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354).
|
|
404
|
+
* Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354 / #3382).
|
|
344
405
|
* Prefer a Set over a long `||` chain so coverage counts one membership check, not N branches.
|
|
345
406
|
*/
|
|
346
407
|
const PROTECTED_POSITIONAL_BINS = new Set([
|
|
@@ -403,7 +464,132 @@ const PROTECTED_POSITIONAL_BINS = new Set([
|
|
|
403
464
|
"sqlite3",
|
|
404
465
|
"crane",
|
|
405
466
|
"objcopy",
|
|
467
|
+
// #3382 residual: positional dest writers (cmake copy / script / VCS / editors).
|
|
468
|
+
"cmake",
|
|
469
|
+
"script",
|
|
470
|
+
"gallery-dl",
|
|
471
|
+
"megadl",
|
|
472
|
+
"ncftpget",
|
|
473
|
+
"svn",
|
|
474
|
+
"fossil",
|
|
475
|
+
"bzr",
|
|
476
|
+
"cvs",
|
|
477
|
+
"darcs",
|
|
478
|
+
"ed",
|
|
479
|
+
"nvim",
|
|
480
|
+
"nano",
|
|
481
|
+
"vim",
|
|
482
|
+
"vi",
|
|
483
|
+
// #3421 residual: positional dest writers (aws, convert, New-Item).
|
|
484
|
+
// git clone/worktree/submodule dests are harvested only on those write subcommands.
|
|
485
|
+
"ex",
|
|
486
|
+
"dos2unix",
|
|
487
|
+
"unix2dos",
|
|
488
|
+
"mac2unix",
|
|
489
|
+
"unix2mac",
|
|
490
|
+
"aws",
|
|
491
|
+
"pijul",
|
|
492
|
+
"pg_dump",
|
|
493
|
+
"pg_restore",
|
|
494
|
+
"pg_dumpall",
|
|
495
|
+
"convert",
|
|
496
|
+
"magick",
|
|
497
|
+
"mogrify",
|
|
498
|
+
"fallocate",
|
|
499
|
+
"new-item",
|
|
500
|
+
// #3459 residual: positional dest writers (fsutil createnew, iwr dest flags).
|
|
501
|
+
// gh/glab/hub clone dests are harvested only on the clone subcommand.
|
|
502
|
+
"iwr",
|
|
503
|
+
"invoke-webrequest",
|
|
504
|
+
"fsutil",
|
|
406
505
|
]);
|
|
506
|
+
/**
|
|
507
|
+
* Bins whose dest is the last pathish operand (src … dest). A protected
|
|
508
|
+
* source that writes elsewhere is not a dest plant (#3421 residual).
|
|
509
|
+
*/
|
|
510
|
+
const LAST_POSITIONAL_DEST_BINS = new Set(["aws", "convert", "magick"]);
|
|
511
|
+
/** Wrappers / assignments before the dest-owning bin (`env aws`, `FOO=1 convert`). */
|
|
512
|
+
const COMMAND_WRAPPER_BINS = new Set([
|
|
513
|
+
"env",
|
|
514
|
+
"command",
|
|
515
|
+
"nice",
|
|
516
|
+
"nohup",
|
|
517
|
+
"sudo",
|
|
518
|
+
"doas",
|
|
519
|
+
"time",
|
|
520
|
+
"stdbuf",
|
|
521
|
+
"timeout",
|
|
522
|
+
"ionice",
|
|
523
|
+
]);
|
|
524
|
+
function isWrapperOrAssignmentToken(raw, n) {
|
|
525
|
+
if (COMMAND_WRAPPER_BINS.has(writeBinName(raw)))
|
|
526
|
+
return true;
|
|
527
|
+
return n.includes("=") && !n.startsWith("-") && !raw.includes("/") && !raw.includes("\\");
|
|
528
|
+
}
|
|
529
|
+
/** Wrapper flags that take a separate value (`env -C DIR`, `nice -n N`). */
|
|
530
|
+
const WRAPPER_VALUE_FLAGS = new Set([
|
|
531
|
+
"-c",
|
|
532
|
+
"--chdir",
|
|
533
|
+
"-u",
|
|
534
|
+
"--unset",
|
|
535
|
+
"-n",
|
|
536
|
+
"--adjustment",
|
|
537
|
+
"-o",
|
|
538
|
+
"--output",
|
|
539
|
+
"-i",
|
|
540
|
+
"--input",
|
|
541
|
+
"-e",
|
|
542
|
+
"--error",
|
|
543
|
+
"-s",
|
|
544
|
+
"--signal",
|
|
545
|
+
"-k",
|
|
546
|
+
"--kill-after",
|
|
547
|
+
]);
|
|
548
|
+
/** In-place ImageMagick writer: every protected operand is a dest, not last-only. */
|
|
549
|
+
const MAGICK_FAMILY_BINS = new Set(["convert", "magick", "mogrify"]);
|
|
550
|
+
const MAGICK_WRITE_DEST_FLAGS = new Set(["-write"]);
|
|
551
|
+
/** True when this token is a subcommand of a last-positional dest bin (`aws s3 cp`). */
|
|
552
|
+
function tokenEndsShellSegment(token) {
|
|
553
|
+
return isShellSegmentBreak(token) || firstUnquotedShellOpIndex(token) >= 0;
|
|
554
|
+
}
|
|
555
|
+
function segmentStartedByLastPositionalDest(tokens, tokenIndex) {
|
|
556
|
+
let start = tokenIndex;
|
|
557
|
+
while (start > 0 && !tokenEndsShellSegment(tokens[start - 1]))
|
|
558
|
+
start--;
|
|
559
|
+
let skipNext = false;
|
|
560
|
+
let afterWrapper = false;
|
|
561
|
+
for (let k = start; k < tokenIndex; k++) {
|
|
562
|
+
const raw = tokens[k];
|
|
563
|
+
if (isShellSegmentBreak(raw))
|
|
564
|
+
continue;
|
|
565
|
+
const n = normalizeToken(raw);
|
|
566
|
+
const bare = binBareName(raw);
|
|
567
|
+
if (skipNext) {
|
|
568
|
+
skipNext = false;
|
|
569
|
+
if (LAST_POSITIONAL_DEST_BINS.has(bare))
|
|
570
|
+
return true;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (n.startsWith("-")) {
|
|
574
|
+
if (!n.includes("=") && WRAPPER_VALUE_FLAGS.has(n))
|
|
575
|
+
skipNext = true;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (isWrapperOrAssignmentToken(raw, n)) {
|
|
579
|
+
afterWrapper = COMMAND_WRAPPER_BINS.has(bare);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (LAST_POSITIONAL_DEST_BINS.has(bare))
|
|
583
|
+
return true;
|
|
584
|
+
// timeout 5 aws / similar: skip one non-bin operand after a wrapper.
|
|
585
|
+
if (afterWrapper) {
|
|
586
|
+
afterWrapper = false;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
407
593
|
/** wget family (directory-prefix dest flags). */
|
|
408
594
|
const WGET_FAMILY_BINS = new Set(["wget", "wget2"]);
|
|
409
595
|
/** aria2 family (dir dest flags). */
|
|
@@ -419,6 +605,57 @@ const SQLITE3_OUTPUT_META = [".output", ".once"];
|
|
|
419
605
|
const TAR_FAMILY_BINS = new Set(["tar", "bsdtar", "gtar", "star", "gnutar"]);
|
|
420
606
|
/** xh / httpie family (download-dir dest flags) (#3311). */
|
|
421
607
|
const XH_FAMILY_BINS = new Set(["xh", "httpie"]);
|
|
608
|
+
/** gallery-dl dest-dir flags (#3382). */
|
|
609
|
+
const GALLERY_DL_FAMILY_BINS = new Set(["gallery-dl", "gallery_dl"]);
|
|
610
|
+
const GALLERY_DL_DIR_DEST_FLAGS = new Set(["-d", "--destination", "--dest"]);
|
|
611
|
+
/** megadl dest-path flags (#3382). */
|
|
612
|
+
const MEGADL_FAMILY_BINS = new Set(["megadl"]);
|
|
613
|
+
const MEGADL_PATH_DEST_FLAGS = new Set(["--path"]);
|
|
614
|
+
/**
|
|
615
|
+
* Extra dest flags harvested on unknown write-shaped bins when dest is protected (#3382 / #3421).
|
|
616
|
+
* Not merged into DOWNLOADER_FILE_DEST_FLAGS: curl `-d` is POST data, not a file dest.
|
|
617
|
+
*/
|
|
618
|
+
const GENERIC_PROTECTED_EXTRA_DEST_FLAGS = new Set([
|
|
619
|
+
"-d",
|
|
620
|
+
"--dir",
|
|
621
|
+
"--destination",
|
|
622
|
+
"--dest",
|
|
623
|
+
"--path",
|
|
624
|
+
"--directory",
|
|
625
|
+
"--workdir",
|
|
626
|
+
"--file",
|
|
627
|
+
"--separate-git-dir",
|
|
628
|
+
"--pack-destination",
|
|
629
|
+
]);
|
|
630
|
+
/** Bins whose `--file` / `-f` operand is an input, not a dest plant. */
|
|
631
|
+
const READ_SHAPED_FILE_FLAG_BINS = new Set([
|
|
632
|
+
"grep",
|
|
633
|
+
"egrep",
|
|
634
|
+
"fgrep",
|
|
635
|
+
"rg",
|
|
636
|
+
"ag",
|
|
637
|
+
"ack",
|
|
638
|
+
"get-content",
|
|
639
|
+
"gc",
|
|
640
|
+
"cat",
|
|
641
|
+
"type",
|
|
642
|
+
"less",
|
|
643
|
+
"more",
|
|
644
|
+
"head",
|
|
645
|
+
"tail",
|
|
646
|
+
]);
|
|
647
|
+
const READ_INPUT_FILE_FLAGS = new Set(["--file", "-f"]);
|
|
648
|
+
/** PowerShell New-Item dest flags (not generic: Get-Content -Path is a read). */
|
|
649
|
+
const NEW_ITEM_PATH_DEST_FLAGS = new Set(["-path", "-literalpath", "--literalpath"]);
|
|
650
|
+
/** git subcommands whose positionals can plant a dest (#3421). */
|
|
651
|
+
const GIT_WRITE_SUBCOMMANDS = new Set(["clone", "worktree", "submodule"]);
|
|
652
|
+
/** Forge CLIs whose `clone` dest is harvested like git clone (#3459). */
|
|
653
|
+
const FORGE_CLONE_BINS = new Set(["gh", "glab", "hub"]);
|
|
654
|
+
/** fossil dest-dir flags (#3382 PATH form + #3421 --workdir=). */
|
|
655
|
+
const FOSSIL_WORKDIR_DEST_FLAGS = new Set(["--workdir"]);
|
|
656
|
+
/** pg_dump family dest-file flags (#3421). */
|
|
657
|
+
const PG_DUMP_FAMILY_BINS = new Set(["pg_dump", "pg_restore", "pg_dumpall"]);
|
|
658
|
+
const PG_DUMP_FILE_DEST_FLAGS = new Set(["-f", "--file"]);
|
|
422
659
|
/**
|
|
423
660
|
* File destination flags for downloaders/decoders (#3206).
|
|
424
661
|
* normalizeToken lowercases, so wget `-O` and curl `-o` share `-o`.
|
|
@@ -433,6 +670,8 @@ const DOWNLOADER_FILE_DEST_FLAGS = new Set([
|
|
|
433
670
|
"-out",
|
|
434
671
|
"--out",
|
|
435
672
|
"--outfile",
|
|
673
|
+
// PowerShell iwr/Invoke-WebRequest -OutFile (single-dash long form) (#3459).
|
|
674
|
+
"-outfile",
|
|
436
675
|
// unar single-dash long form; GNU/others use --output-directory.
|
|
437
676
|
"-output-directory",
|
|
438
677
|
"--output-directory",
|
|
@@ -479,12 +718,43 @@ function binBareName(token) {
|
|
|
479
718
|
const base = pathish.includes("/") ? pathish.slice(pathish.lastIndexOf("/") + 1) : pathish;
|
|
480
719
|
return base.endsWith(".exe") ? base.slice(0, -4) : base;
|
|
481
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* GNU coreutils g* prefixes (Homebrew `ginstall`, `gcp`).
|
|
723
|
+
* Stems only — `git` / `gh` / `gpg` do not map to `it` / `h` / `pg` (#3459).
|
|
724
|
+
*/
|
|
725
|
+
const GNU_COREUTILS_WRITE_STEMS = new Set([
|
|
726
|
+
"cp",
|
|
727
|
+
"mv",
|
|
728
|
+
"rm",
|
|
729
|
+
"rmdir",
|
|
730
|
+
"unlink",
|
|
731
|
+
"shred",
|
|
732
|
+
"truncate",
|
|
733
|
+
"chmod",
|
|
734
|
+
"chown",
|
|
735
|
+
"dd",
|
|
736
|
+
"tee",
|
|
737
|
+
"install",
|
|
738
|
+
"ln",
|
|
739
|
+
"link",
|
|
740
|
+
"mkdir",
|
|
741
|
+
"touch",
|
|
742
|
+
]);
|
|
743
|
+
function writeBinName(token) {
|
|
744
|
+
const bare = binBareName(token);
|
|
745
|
+
if (bare.length > 1 && bare.startsWith("g")) {
|
|
746
|
+
const stem = bare.slice(1);
|
|
747
|
+
if (GNU_COREUTILS_WRITE_STEMS.has(stem))
|
|
748
|
+
return stem;
|
|
749
|
+
}
|
|
750
|
+
return bare;
|
|
751
|
+
}
|
|
482
752
|
/** True when token names a downloader/decoder bin (bare or path-qualified). */
|
|
483
753
|
function isDownloaderDecoderBin(token) {
|
|
484
754
|
const n = normalizeToken(token);
|
|
485
755
|
if (n.startsWith("-"))
|
|
486
756
|
return false;
|
|
487
|
-
return DOWNLOADER_DECODER_BINS.has(
|
|
757
|
+
return DOWNLOADER_DECODER_BINS.has(writeBinName(token));
|
|
488
758
|
}
|
|
489
759
|
/**
|
|
490
760
|
* True when `flag` (normalized lower) or `rawFlag` (original token) is a dest flag for `bin`.
|
|
@@ -532,6 +802,18 @@ function isDownloaderDestFlag(flag, bin, rawFlag) {
|
|
|
532
802
|
return true;
|
|
533
803
|
if (ATOOL_FAMILY_BINS.has(bin) && ATOOL_DIR_DEST_FLAGS.has(flag))
|
|
534
804
|
return true;
|
|
805
|
+
if (GALLERY_DL_FAMILY_BINS.has(bin) && GALLERY_DL_DIR_DEST_FLAGS.has(flag))
|
|
806
|
+
return true;
|
|
807
|
+
if (MEGADL_FAMILY_BINS.has(bin) && MEGADL_PATH_DEST_FLAGS.has(flag))
|
|
808
|
+
return true;
|
|
809
|
+
if (bin === "fossil" && FOSSIL_WORKDIR_DEST_FLAGS.has(flag))
|
|
810
|
+
return true;
|
|
811
|
+
if (PG_DUMP_FAMILY_BINS.has(bin) && PG_DUMP_FILE_DEST_FLAGS.has(flag))
|
|
812
|
+
return true;
|
|
813
|
+
if (bin === "new-item" && NEW_ITEM_PATH_DEST_FLAGS.has(flag))
|
|
814
|
+
return true;
|
|
815
|
+
if (MAGICK_FAMILY_BINS.has(bin) && MAGICK_WRITE_DEST_FLAGS.has(flag))
|
|
816
|
+
return true;
|
|
535
817
|
return false;
|
|
536
818
|
}
|
|
537
819
|
/**
|
|
@@ -599,7 +881,7 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
599
881
|
i++;
|
|
600
882
|
continue;
|
|
601
883
|
}
|
|
602
|
-
const bin =
|
|
884
|
+
const bin = writeBinName(tokens[i]);
|
|
603
885
|
i++;
|
|
604
886
|
// xxd reverse mode writes; without -r, path positionals are dump inputs (read).
|
|
605
887
|
let xxdReverse = false;
|
|
@@ -610,7 +892,9 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
610
892
|
// Segment breaks (`;`/`\n`/glued ops) prevent following-command overwrite.
|
|
611
893
|
let lastPositionalPath = null;
|
|
612
894
|
const protectedPathish = [];
|
|
613
|
-
const collectsProtectedPositionals = PROTECTED_POSITIONAL_BINS.has(bin)
|
|
895
|
+
const collectsProtectedPositionals = PROTECTED_POSITIONAL_BINS.has(bin) ||
|
|
896
|
+
(bin === "git" && gitHasWriteSubcommand(tokens, i)) ||
|
|
897
|
+
(FORGE_CLONE_BINS.has(bin) && forgeHasCloneSubcommand(tokens, i));
|
|
614
898
|
while (i < tokens.length) {
|
|
615
899
|
const raw = tokens[i];
|
|
616
900
|
const n = normalizeToken(raw);
|
|
@@ -672,7 +956,8 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
672
956
|
// Skip for scp (OpenSSH -oOption=Value attached forms are not file dests).
|
|
673
957
|
// 7z requires attached -oDIR (#3245).
|
|
674
958
|
if (bin !== "scp" && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
|
|
675
|
-
|
|
959
|
+
// `-outfile` is PowerShell -OutFile (next token is dest), not attached -outFILE (#3459).
|
|
960
|
+
if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output") && n !== "-outfile") {
|
|
676
961
|
dests.push(pathishToken(raw.slice(4)));
|
|
677
962
|
i++;
|
|
678
963
|
continue;
|
|
@@ -698,6 +983,12 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
698
983
|
i++;
|
|
699
984
|
continue;
|
|
700
985
|
}
|
|
986
|
+
// gallery-dl attached -dDIR (#3382)
|
|
987
|
+
if (GALLERY_DL_FAMILY_BINS.has(bin) && n.startsWith("-d") && n.length > 2) {
|
|
988
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
989
|
+
i++;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
701
992
|
// unzip / cabextract attached -dDIR (#3245 / #3336)
|
|
702
993
|
if (UNZIP_DIR_DEST_BINS.has(bin) && n.startsWith("-d") && n.length > 2) {
|
|
703
994
|
dests.push(pathishToken(raw.slice(2)));
|
|
@@ -751,9 +1042,7 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
751
1042
|
if (p.startsWith(prefix) && p.length > prefix.length) {
|
|
752
1043
|
const dest = p.slice(prefix.length);
|
|
753
1044
|
dests.push(dest);
|
|
754
|
-
if (dest
|
|
755
|
-
dest.includes(".deft-directive-disable") ||
|
|
756
|
-
dest.includes(".no-deft-directive")) {
|
|
1045
|
+
if (pathishIsProtectedDest(dest)) {
|
|
757
1046
|
protectedPathish.push(dest);
|
|
758
1047
|
}
|
|
759
1048
|
break;
|
|
@@ -765,7 +1054,7 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
765
1054
|
const meta = sqlite3MetaDest(raw, tokens[i + 1]);
|
|
766
1055
|
if (meta !== null) {
|
|
767
1056
|
dests.push(meta.dest);
|
|
768
|
-
if (
|
|
1057
|
+
if (pathishIsProtectedDest(meta.dest)) {
|
|
769
1058
|
protectedPathish.push(meta.dest);
|
|
770
1059
|
}
|
|
771
1060
|
i += meta.consumedNext ? 2 : 1;
|
|
@@ -773,18 +1062,16 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
773
1062
|
}
|
|
774
1063
|
}
|
|
775
1064
|
// scp / certutil / rclone / archive extractors: pathish operands (quote-aware glued-op cut).
|
|
776
|
-
// Under UAT: any `.deft/authz` / kill-switch pathish is
|
|
777
|
-
//
|
|
1065
|
+
// Under UAT: any `.deft/authz` / approved-scope / kill-switch pathish is
|
|
1066
|
+
// fail-closed settings (prefer deny over dest-parser perfection; #3213 / #3421).
|
|
1067
|
+
// Last-positional dest bins (aws/convert/magick): only the last operand is dest.
|
|
778
1068
|
if (collectsProtectedPositionals && !n.startsWith("-")) {
|
|
779
1069
|
const cut = firstUnquotedShellOpIndex(raw);
|
|
780
1070
|
const cleaned = cut >= 0 ? raw.slice(0, cut) : raw;
|
|
781
1071
|
const p = pathishToken(cleaned);
|
|
782
1072
|
if (p.length > 0) {
|
|
783
1073
|
lastPositionalPath = p;
|
|
784
|
-
|
|
785
|
-
if (p.includes(".deft/authz") ||
|
|
786
|
-
p.includes(".deft-directive-disable") ||
|
|
787
|
-
p.includes(".no-deft-directive")) {
|
|
1074
|
+
if (pathishIsProtectedDest(p) && !LAST_POSITIONAL_DEST_BINS.has(bin)) {
|
|
788
1075
|
protectedPathish.push(p);
|
|
789
1076
|
}
|
|
790
1077
|
}
|
|
@@ -886,13 +1173,107 @@ function hasPolicyAuthorityMutator(tokens) {
|
|
|
886
1173
|
}
|
|
887
1174
|
/** True when pathish token names a kill-switch basename (quote-strip resistant). */
|
|
888
1175
|
function pathishIsProtectedDest(pathish) {
|
|
889
|
-
return pathishIsAuthzDir(pathish) ||
|
|
1176
|
+
return (pathishIsAuthzDir(pathish) ||
|
|
1177
|
+
pathishIsApprovedScopeDir(pathish) ||
|
|
1178
|
+
pathishMentionsKillSwitch(pathish));
|
|
1179
|
+
}
|
|
1180
|
+
function isGenericProtectedDestFlag(flag) {
|
|
1181
|
+
return DOWNLOADER_FILE_DEST_FLAGS.has(flag) || GENERIC_PROTECTED_EXTRA_DEST_FLAGS.has(flag);
|
|
1182
|
+
}
|
|
1183
|
+
function isReadShapedInputFileFlag(bin, flag) {
|
|
1184
|
+
return READ_SHAPED_FILE_FLAG_BINS.has(bin) && READ_INPUT_FILE_FLAGS.has(flag);
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Git globals that do not take a separate value. Unlisted globals consume the
|
|
1188
|
+
* next token so a later dest writer is still seen (#3421 residual).
|
|
1189
|
+
*/
|
|
1190
|
+
const GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
1191
|
+
"--no-pager",
|
|
1192
|
+
"-p",
|
|
1193
|
+
"--paginate",
|
|
1194
|
+
"-P",
|
|
1195
|
+
"--bare",
|
|
1196
|
+
"--help",
|
|
1197
|
+
"-h",
|
|
1198
|
+
"--version",
|
|
1199
|
+
"--literal-pathspecs",
|
|
1200
|
+
"--glob-pathspecs",
|
|
1201
|
+
"--noglob-pathspecs",
|
|
1202
|
+
"--icase-pathspecs",
|
|
1203
|
+
"--no-replace-objects",
|
|
1204
|
+
"--no-optional-locks",
|
|
1205
|
+
"--no-lazy-fetch",
|
|
1206
|
+
"--no-advice",
|
|
1207
|
+
"--quiet",
|
|
1208
|
+
"-q",
|
|
1209
|
+
]);
|
|
1210
|
+
/**
|
|
1211
|
+
* True only when the git *subcommand* (first non-flag token) is a dest writer.
|
|
1212
|
+
* Later operands named clone/worktree/submodule (e.g. `git log worktree -- …`)
|
|
1213
|
+
* are not write subcommands (#3423 residual).
|
|
1214
|
+
* Boolean globals (`--no-pager`) do not consume the next token. Other globals
|
|
1215
|
+
* consume it even when that token spells a subcommand (#3421 residual).
|
|
1216
|
+
*/
|
|
1217
|
+
function gitHasWriteSubcommand(tokens, start) {
|
|
1218
|
+
let i = start;
|
|
1219
|
+
while (i < tokens.length) {
|
|
1220
|
+
const raw = tokens[i];
|
|
1221
|
+
if (isShellSegmentBreak(raw))
|
|
1222
|
+
return false;
|
|
1223
|
+
const n = normalizeToken(raw);
|
|
1224
|
+
if (n.startsWith("-")) {
|
|
1225
|
+
if (!n.includes("=") && !GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS.has(n)) {
|
|
1226
|
+
const next = tokens[i + 1];
|
|
1227
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
1228
|
+
i += 2;
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
i++;
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
return GIT_WRITE_SUBCOMMANDS.has(n);
|
|
1236
|
+
}
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* True when this forge-CLI segment is a clone dest writer
|
|
1241
|
+
* (`gh repo clone`, `glab repo clone`, `hub clone`) (#3459).
|
|
1242
|
+
* `clone` as a later flag value (`gh pr create --title clone`) is not a dest writer.
|
|
1243
|
+
*/
|
|
1244
|
+
function forgeHasCloneSubcommand(tokens, start) {
|
|
1245
|
+
const nonFlags = [];
|
|
1246
|
+
let i = start;
|
|
1247
|
+
while (i < tokens.length) {
|
|
1248
|
+
const raw = tokens[i];
|
|
1249
|
+
if (isShellSegmentBreak(raw))
|
|
1250
|
+
break;
|
|
1251
|
+
const n = normalizeToken(raw);
|
|
1252
|
+
if (n.startsWith("-")) {
|
|
1253
|
+
if (!n.includes("=") && (GH_VALUE_FLAGS.has(raw) || GH_VALUE_FLAGS.has(n))) {
|
|
1254
|
+
const next = tokens[i + 1];
|
|
1255
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
1256
|
+
i += 2;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
i++;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
nonFlags.push(n);
|
|
1264
|
+
i++;
|
|
1265
|
+
}
|
|
1266
|
+
if (nonFlags[0] === "clone")
|
|
1267
|
+
return true;
|
|
1268
|
+
return nonFlags[0] === "repo" && nonFlags[1] === "clone";
|
|
890
1269
|
}
|
|
891
1270
|
/**
|
|
892
|
-
* Fail-closed dest harvest for write-shaped Shell under UAT (#3354).
|
|
1271
|
+
* Fail-closed dest harvest for write-shaped Shell under UAT (#3354 / #3382 / #3421).
|
|
893
1272
|
* Named-bin parsers above are not the only path: any token that looks like
|
|
894
|
-
* `-o` / `--output` / `--outfile` / `--output-file`
|
|
895
|
-
*
|
|
1273
|
+
* `-o` / `--output` / `--outfile` / `--output-file` / `-d` / `--dir` /
|
|
1274
|
+
* `--destination` / `--path` / `--directory` / `--workdir` / `--file` / `-f`
|
|
1275
|
+
* (or attached `-oDIR` / `-dDIR`) whose dest is authz/kill-switch/approved-scope
|
|
1276
|
+
* is collected even when the bin is unknown.
|
|
896
1277
|
* scp `-o` (OpenSSH option) and cpio `-o` (copy-out) stay excluded.
|
|
897
1278
|
*/
|
|
898
1279
|
function genericProtectedDests(tokens) {
|
|
@@ -909,13 +1290,13 @@ function genericProtectedDests(tokens) {
|
|
|
909
1290
|
}
|
|
910
1291
|
if (!n.startsWith("-")) {
|
|
911
1292
|
if (isDownloaderDecoderBin(raw)) {
|
|
912
|
-
currentBin =
|
|
1293
|
+
currentBin = writeBinName(raw);
|
|
913
1294
|
}
|
|
914
1295
|
else if (currentBin.length === 0 &&
|
|
915
1296
|
!raw.includes("/") &&
|
|
916
1297
|
!raw.includes("\\") &&
|
|
917
1298
|
n.length > 0) {
|
|
918
|
-
currentBin =
|
|
1299
|
+
currentBin = writeBinName(raw);
|
|
919
1300
|
}
|
|
920
1301
|
}
|
|
921
1302
|
if (currentBin === "scp" || currentBin === "cpio")
|
|
@@ -923,7 +1304,7 @@ function genericProtectedDests(tokens) {
|
|
|
923
1304
|
if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
|
|
924
1305
|
const eq = raw.indexOf("=");
|
|
925
1306
|
const flag = normalizeToken(raw.slice(0, eq));
|
|
926
|
-
if (
|
|
1307
|
+
if (isGenericProtectedDestFlag(flag) && !isReadShapedInputFileFlag(currentBin, flag)) {
|
|
927
1308
|
const dest = pathishToken(raw.slice(eq + 1));
|
|
928
1309
|
if (pathishIsProtectedDest(dest))
|
|
929
1310
|
dests.push(dest);
|
|
@@ -940,7 +1321,13 @@ function genericProtectedDests(tokens) {
|
|
|
940
1321
|
dests.push(dest);
|
|
941
1322
|
continue;
|
|
942
1323
|
}
|
|
943
|
-
if (
|
|
1324
|
+
if (n.startsWith("-") && !n.startsWith("--") && n.startsWith("-d") && n.length > 2) {
|
|
1325
|
+
const dest = pathishToken(raw.slice(2));
|
|
1326
|
+
if (pathishIsProtectedDest(dest))
|
|
1327
|
+
dests.push(dest);
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (isGenericProtectedDestFlag(n) && !isReadShapedInputFileFlag(currentBin, n)) {
|
|
944
1331
|
const next = tokens[i + 1];
|
|
945
1332
|
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
946
1333
|
const dest = pathishToken(next);
|
|
@@ -969,8 +1356,9 @@ function sqlite3MetaDest(raw, next) {
|
|
|
969
1356
|
return null;
|
|
970
1357
|
}
|
|
971
1358
|
function pathishMentionsKillSwitch(pathish) {
|
|
1359
|
+
const p = canonicalizePathish(pathish);
|
|
972
1360
|
for (const name of KILL_SWITCH_BASENAMES) {
|
|
973
|
-
if (
|
|
1361
|
+
if (p === name || p.endsWith(`/${name}`) || p.includes(name)) {
|
|
974
1362
|
return true;
|
|
975
1363
|
}
|
|
976
1364
|
}
|
|
@@ -1045,9 +1433,13 @@ function hasKillSwitchShellWrite(command, tokens) {
|
|
|
1045
1433
|
]);
|
|
1046
1434
|
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1047
1435
|
const binTok = normalizeToken(tokens[ti]);
|
|
1048
|
-
const bare =
|
|
1436
|
+
const bare = writeBinName(tokens[ti]);
|
|
1049
1437
|
if (!killWriteBins.has(binTok) && !killWriteBins.has(bare))
|
|
1050
1438
|
continue;
|
|
1439
|
+
// Last-positional dest bins (and their subcommands): dest harvest owns dest vs source.
|
|
1440
|
+
if (LAST_POSITIONAL_DEST_BINS.has(bare) || segmentStartedByLastPositionalDest(tokens, ti)) {
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1051
1443
|
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
1052
1444
|
const p = pathishToken(tokens[tj]);
|
|
1053
1445
|
if (pathishMentionsKillSwitch(p))
|
|
@@ -1061,8 +1453,14 @@ function hasKillSwitchShellWrite(command, tokens) {
|
|
|
1061
1453
|
}
|
|
1062
1454
|
}
|
|
1063
1455
|
// Bare `touch .deft-directive-disable` / `ln -sf x .deft-directive-disable` — path later.
|
|
1064
|
-
|
|
1065
|
-
|
|
1456
|
+
// Skip tokens in a last-positional dest segment (dest harvest owns dest vs source).
|
|
1457
|
+
// Other segments still scan so `aws … ; touch .deft-directive-disable` is settings.
|
|
1458
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1459
|
+
if (LAST_POSITIONAL_DEST_BINS.has(writeBinName(tokens[ti])) ||
|
|
1460
|
+
segmentStartedByLastPositionalDest(tokens, ti)) {
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
const p = pathishToken(tokens[ti]);
|
|
1066
1464
|
for (const name of KILL_SWITCH_BASENAMES) {
|
|
1067
1465
|
// Exact basename or ends with /basename
|
|
1068
1466
|
if (p === name || p.endsWith(`/${name}`)) {
|
|
@@ -1070,8 +1468,8 @@ function hasKillSwitchShellWrite(command, tokens) {
|
|
|
1070
1468
|
if (hasWriteShape(command, tokens) ||
|
|
1071
1469
|
lower.includes("touch") ||
|
|
1072
1470
|
lower.includes("new-item") ||
|
|
1073
|
-
SYMLINK_PLANT_BINS.has(
|
|
1074
|
-
tokens.some((tok) => SYMLINK_PLANT_BINS.has(
|
|
1471
|
+
SYMLINK_PLANT_BINS.has(writeBinName(tokens[0])) ||
|
|
1472
|
+
tokens.some((tok) => SYMLINK_PLANT_BINS.has(writeBinName(tok)))) {
|
|
1075
1473
|
return true;
|
|
1076
1474
|
}
|
|
1077
1475
|
}
|
|
@@ -1100,7 +1498,10 @@ function isProgrammaticWriteBinToken(token) {
|
|
|
1100
1498
|
bare === "perl" ||
|
|
1101
1499
|
bare === "ruby" ||
|
|
1102
1500
|
bare === "pwsh" ||
|
|
1103
|
-
bare === "powershell"
|
|
1501
|
+
bare === "powershell" ||
|
|
1502
|
+
bare === "tsx" ||
|
|
1503
|
+
bare === "ts-node" ||
|
|
1504
|
+
bare === "tsnode") {
|
|
1104
1505
|
return true;
|
|
1105
1506
|
}
|
|
1106
1507
|
// Versioned: python3.11, python3.12, node18, …
|
|
@@ -1243,17 +1644,89 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
|
|
|
1243
1644
|
function pathishToken(token) {
|
|
1244
1645
|
return token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
|
|
1245
1646
|
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Collapse `//`, `/./`, and `/../` lexically so equivalent dests still match
|
|
1649
|
+
* (`.deft/foo/../approved-scope/x`) without following symlinks or matching
|
|
1650
|
+
* prefix siblings. O(n) — no nested-quantifier regex.
|
|
1651
|
+
*/
|
|
1652
|
+
function canonicalizePathish(pathish) {
|
|
1653
|
+
const parts = [];
|
|
1654
|
+
let cur = "";
|
|
1655
|
+
const abs = pathish.startsWith("/");
|
|
1656
|
+
for (let i = 0; i <= pathish.length; i++) {
|
|
1657
|
+
const c = i < pathish.length ? pathish[i] : "/";
|
|
1658
|
+
if (c === "/" || i === pathish.length) {
|
|
1659
|
+
if (cur.length === 0 || cur === ".") {
|
|
1660
|
+
// drop empty and `.`
|
|
1661
|
+
}
|
|
1662
|
+
else if (cur === "..") {
|
|
1663
|
+
if (parts.length > 0 && parts[parts.length - 1] !== "..") {
|
|
1664
|
+
parts.pop();
|
|
1665
|
+
}
|
|
1666
|
+
else if (!abs) {
|
|
1667
|
+
parts.push("..");
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
parts.push(cur);
|
|
1672
|
+
}
|
|
1673
|
+
cur = "";
|
|
1674
|
+
}
|
|
1675
|
+
else {
|
|
1676
|
+
cur += c;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const joined = parts.join("/");
|
|
1680
|
+
return abs ? `/${joined}` : joined;
|
|
1681
|
+
}
|
|
1246
1682
|
/**
|
|
1247
1683
|
* True when a pathish string targets `.deft/authz` (after quote strip / slash normalize).
|
|
1248
1684
|
* Quote-split forms like `'.deft/'authz'/grants/x'` become `.deft/authz/grants/x` via pathishToken.
|
|
1249
1685
|
*/
|
|
1686
|
+
/** True when `dir` is a path segment (left-bounded; `foo.deft/authz` is not `.deft/authz`). */
|
|
1687
|
+
function pathishHasDirSegment(pathish, dir) {
|
|
1688
|
+
const p = canonicalizePathish(pathish.trim());
|
|
1689
|
+
if (p === dir || p.startsWith(`${dir}/`) || p.startsWith(`${dir}\\`))
|
|
1690
|
+
return true;
|
|
1691
|
+
for (let i = 0; i < p.length; i++) {
|
|
1692
|
+
const c = p[i];
|
|
1693
|
+
if (c !== "/" && c !== "\\" && c !== ":" && c !== "=")
|
|
1694
|
+
continue;
|
|
1695
|
+
const rest = p.slice(i + 1);
|
|
1696
|
+
if (rest === dir || rest.startsWith(`${dir}/`) || rest.startsWith(`${dir}\\`))
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1250
1701
|
function pathishIsAuthzDir(pathish) {
|
|
1251
|
-
return pathish
|
|
1702
|
+
return pathishHasDirSegment(pathish, ".deft/authz");
|
|
1703
|
+
}
|
|
1704
|
+
/** True when a pathish string targets `.deft/approved-scope` (#3421 / #3410 mint). */
|
|
1705
|
+
function pathishIsApprovedScopeDir(pathish) {
|
|
1706
|
+
return pathishHasDirSegment(pathish, ".deft/approved-scope");
|
|
1707
|
+
}
|
|
1708
|
+
function pathishIsSettingsStoreDir(pathish) {
|
|
1709
|
+
return pathishIsAuthzDir(pathish) || pathishIsApprovedScopeDir(pathish);
|
|
1710
|
+
}
|
|
1711
|
+
/** True when dest is a settings-store path or a split-path `authz` segment after `.deft`. */
|
|
1712
|
+
function destMentionsAuthzSegment(dest) {
|
|
1713
|
+
const p = canonicalizePathish(pathishToken(dest).trim());
|
|
1714
|
+
if (pathishIsSettingsStoreDir(p))
|
|
1715
|
+
return true;
|
|
1716
|
+
const segs = p.split("/").filter((s) => s.length > 0);
|
|
1717
|
+
for (let i = 0; i < segs.length; i++) {
|
|
1718
|
+
if (segs[i] !== "authz")
|
|
1719
|
+
continue;
|
|
1720
|
+
const prev = i > 0 ? segs[i - 1] : "";
|
|
1721
|
+
if (prev === ".deft" || prev.length === 0)
|
|
1722
|
+
return true;
|
|
1723
|
+
}
|
|
1724
|
+
return false;
|
|
1252
1725
|
}
|
|
1253
1726
|
/**
|
|
1254
|
-
* Shell **write** targeting `.deft/authz/`
|
|
1727
|
+
* Shell **write** targeting `.deft/authz/` or `.deft/approved-scope/` (#3110 / #3421).
|
|
1255
1728
|
* Pure reads (`cat .deft/authz/state.json`) stay unclassifiable — use `authz:show`.
|
|
1256
|
-
* Redirects only count when the destination region contains
|
|
1729
|
+
* Redirects only count when the destination region contains a settings-store dir.
|
|
1257
1730
|
* Pathish/token checks run even when the raw command lacks contiguous `.deft/authz`
|
|
1258
1731
|
* text (quote-split residual: `cp x '.deft/'authz'/grants/y'`).
|
|
1259
1732
|
*/
|
|
@@ -1279,7 +1752,7 @@ function hasAuthzDirShellWrite(command, tokens) {
|
|
|
1279
1752
|
hay[end] !== "\n") {
|
|
1280
1753
|
end++;
|
|
1281
1754
|
}
|
|
1282
|
-
if (hay.slice(j, end)
|
|
1755
|
+
if (pathishIsSettingsStoreDir(hay.slice(j, end)))
|
|
1283
1756
|
return true;
|
|
1284
1757
|
}
|
|
1285
1758
|
}
|
|
@@ -1289,7 +1762,7 @@ function hasAuthzDirShellWrite(command, tokens) {
|
|
|
1289
1762
|
// must not fail-open as unclassifiable (SLizard residual). ARCHIVE_ALT_WRITE_BINS: tar/rclone
|
|
1290
1763
|
// residual pathish without dest-flag perfection thrash.
|
|
1291
1764
|
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1292
|
-
const bare =
|
|
1765
|
+
const bare = writeBinName(tokens[ti]);
|
|
1293
1766
|
const n = normalizeToken(tokens[ti]);
|
|
1294
1767
|
if (!INDIRECT_WRITE_BINS.has(n) &&
|
|
1295
1768
|
!INDIRECT_WRITE_BINS.has(bare) &&
|
|
@@ -1297,20 +1770,24 @@ function hasAuthzDirShellWrite(command, tokens) {
|
|
|
1297
1770
|
!ARCHIVE_ALT_WRITE_BINS.has(bare)) {
|
|
1298
1771
|
continue;
|
|
1299
1772
|
}
|
|
1773
|
+
// Last-positional dest bins (and their subcommands): dest harvest owns dest vs source.
|
|
1774
|
+
if (LAST_POSITIONAL_DEST_BINS.has(bare) || segmentStartedByLastPositionalDest(tokens, ti)) {
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1300
1777
|
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
1301
1778
|
const p = pathishToken(tokens[tj]);
|
|
1302
|
-
if (
|
|
1779
|
+
if (pathishIsSettingsStoreDir(p))
|
|
1303
1780
|
return true;
|
|
1304
1781
|
for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
|
|
1305
|
-
if (p.startsWith(prefix) &&
|
|
1782
|
+
if (p.startsWith(prefix) && pathishIsSettingsStoreDir(p.slice(prefix.length))) {
|
|
1306
1783
|
return true;
|
|
1307
1784
|
}
|
|
1308
1785
|
}
|
|
1309
1786
|
}
|
|
1310
1787
|
}
|
|
1311
|
-
// Downloader/decoder destinations under
|
|
1788
|
+
// Downloader/decoder destinations under settings-store dirs (#3206 / #3421).
|
|
1312
1789
|
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
1313
|
-
if (
|
|
1790
|
+
if (pathishIsSettingsStoreDir(dest))
|
|
1314
1791
|
return true;
|
|
1315
1792
|
}
|
|
1316
1793
|
// Contiguous mention without write shape stays false (reads like cat .deft/authz/…).
|
|
@@ -1347,8 +1824,15 @@ const INDIRECT_WRITE_BINS = new Set([
|
|
|
1347
1824
|
"remove-item",
|
|
1348
1825
|
"ri",
|
|
1349
1826
|
"ni",
|
|
1827
|
+
"new-item",
|
|
1350
1828
|
"sc",
|
|
1351
1829
|
"mi",
|
|
1830
|
+
// #3459 residual: Windows copy / fsutil / touch / TS runners.
|
|
1831
|
+
"copy",
|
|
1832
|
+
"fsutil",
|
|
1833
|
+
"touch",
|
|
1834
|
+
"tsx",
|
|
1835
|
+
"ts-node",
|
|
1352
1836
|
]);
|
|
1353
1837
|
/**
|
|
1354
1838
|
* O(n): true when command expands `$…` / `` `…` `` / `%VAR%`
|
|
@@ -1390,7 +1874,7 @@ function hasWriteShape(command, tokens) {
|
|
|
1390
1874
|
if (command.includes(">"))
|
|
1391
1875
|
return true;
|
|
1392
1876
|
for (const t of tokens) {
|
|
1393
|
-
const bare =
|
|
1877
|
+
const bare = writeBinName(t);
|
|
1394
1878
|
if (INDIRECT_WRITE_BINS.has(normalizeToken(t)) || INDIRECT_WRITE_BINS.has(bare))
|
|
1395
1879
|
return true;
|
|
1396
1880
|
// #3245: archive extractors / alt downloaders are write-shaped (defense-in-depth for
|
|
@@ -1748,7 +2232,7 @@ export function classifyShellAuthzOps(command) {
|
|
|
1748
2232
|
lower[end] !== "\n") {
|
|
1749
2233
|
end++;
|
|
1750
2234
|
}
|
|
1751
|
-
if (lower.slice(j, end)
|
|
2235
|
+
if (destMentionsAuthzSegment(lower.slice(j, end))) {
|
|
1752
2236
|
found.add("settings");
|
|
1753
2237
|
break;
|
|
1754
2238
|
}
|