@fusengine/harness 0.1.78 → 0.1.79

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 (2) hide show
  1. package/dist/cli/bin.mjs +628 -0
  2. package/package.json +1 -1
package/dist/cli/bin.mjs CHANGED
@@ -293,6 +293,624 @@ function runSecurityScan(dir) {
293
293
  };
294
294
  }
295
295
  //#endregion
296
+ //#region src/codex-rules/rules/rm-variants.ts
297
+ /**
298
+ * `rm` prefix rules — hand-mapped from `security.ts`'s two `rm -rf`-family
299
+ * CRITICAL_PATTERNS labels. Both source regexes accept `-rf`/`-fr` combined
300
+ * or `-r -f`/`-f -r` split flags, so each label needs 3 prefix_rule variants
301
+ * (combined flag + 2 split-flag orders) to avoid a flag-order gap.
302
+ */
303
+ const RM_VARIANT_RULES = [
304
+ {
305
+ sourceLabel: "DANGEROUS PATTERN: recursive delete of / or ~",
306
+ pattern: [
307
+ "rm",
308
+ ["-rf", "-fr"],
309
+ ["/", "~"]
310
+ ],
311
+ decision: "forbidden",
312
+ justification: "Recursive force-delete of the filesystem root or home directory.",
313
+ match: ["rm -rf /", "rm -fr ~"],
314
+ notMatch: ["rm -rf /tmp/build"]
315
+ },
316
+ {
317
+ sourceLabel: "DANGEROUS PATTERN: recursive delete of / or ~",
318
+ pattern: [
319
+ "rm",
320
+ "-r",
321
+ "-f",
322
+ ["/", "~"]
323
+ ],
324
+ decision: "forbidden",
325
+ justification: "Recursive force-delete of the filesystem root or home directory (split flags).",
326
+ match: ["rm -r -f /", "rm -r -f ~"],
327
+ notMatch: ["rm -r -f /tmp/build"]
328
+ },
329
+ {
330
+ sourceLabel: "DANGEROUS PATTERN: recursive delete of / or ~",
331
+ pattern: [
332
+ "rm",
333
+ "-f",
334
+ "-r",
335
+ ["/", "~"]
336
+ ],
337
+ decision: "forbidden",
338
+ justification: "Recursive force-delete of the filesystem root or home directory (split flags, reversed).",
339
+ match: ["rm -f -r /", "rm -f -r ~"],
340
+ notMatch: ["rm -f -r /tmp/build"]
341
+ },
342
+ {
343
+ sourceLabel: "DANGEROUS PATTERN: recursive delete of a system directory",
344
+ pattern: [
345
+ "rm",
346
+ ["-rf", "-fr"],
347
+ [
348
+ "/etc",
349
+ "/usr",
350
+ "/var",
351
+ "/bin",
352
+ "/sbin",
353
+ "/boot",
354
+ "/lib"
355
+ ]
356
+ ],
357
+ decision: "forbidden",
358
+ justification: "Recursive force-delete of a core system directory.",
359
+ match: ["rm -rf /etc", "rm -fr /usr"],
360
+ notMatch: ["rm -rf /home/user/project"]
361
+ },
362
+ {
363
+ sourceLabel: "DANGEROUS PATTERN: recursive delete of a system directory",
364
+ pattern: [
365
+ "rm",
366
+ "-r",
367
+ "-f",
368
+ [
369
+ "/etc",
370
+ "/usr",
371
+ "/var",
372
+ "/bin",
373
+ "/sbin",
374
+ "/boot",
375
+ "/lib"
376
+ ]
377
+ ],
378
+ decision: "forbidden",
379
+ justification: "Recursive force-delete of a core system directory (split flags).",
380
+ match: ["rm -r -f /var", "rm -r -f /bin"],
381
+ notMatch: ["rm -r -f /home/user"]
382
+ },
383
+ {
384
+ sourceLabel: "DANGEROUS PATTERN: recursive delete of a system directory",
385
+ pattern: [
386
+ "rm",
387
+ "-f",
388
+ "-r",
389
+ [
390
+ "/etc",
391
+ "/usr",
392
+ "/var",
393
+ "/bin",
394
+ "/sbin",
395
+ "/boot",
396
+ "/lib"
397
+ ]
398
+ ],
399
+ decision: "forbidden",
400
+ justification: "Recursive force-delete of a core system directory (split flags, reversed).",
401
+ match: ["rm -f -r /sbin", "rm -f -r /lib"],
402
+ notMatch: ["rm -f -r /tmp"]
403
+ }
404
+ ];
405
+ //#endregion
406
+ //#region src/codex-rules/rules/mkfs.ts
407
+ /**
408
+ * `mkfs` filesystem-formatting variants — hand-mapped from `security.ts`'s
409
+ * `\bmkfs(?:\.[a-z0-9]+)?\b` CRITICAL_PATTERNS label. The source regex
410
+ * accepts ANY `.suffix`; `prefix_rule` cannot express an open-ended suffix,
411
+ * so every real-world `mkfs.*` filesystem type is enumerated explicitly as
412
+ * an alternation at position 0 (exhaustive, not a mechanical derivation).
413
+ */
414
+ const MKFS_RULES = [{
415
+ sourceLabel: "CRITICAL: Detected dangerous command 'mkfs'",
416
+ pattern: [[
417
+ "mkfs",
418
+ "mkfs.ext2",
419
+ "mkfs.ext3",
420
+ "mkfs.ext4",
421
+ "mkfs.xfs",
422
+ "mkfs.btrfs",
423
+ "mkfs.exfat",
424
+ "mkfs.vfat",
425
+ "mkfs.fat",
426
+ "mkfs.ntfs",
427
+ "mkfs.f2fs",
428
+ "mkfs.hfsplus",
429
+ "mkfs.apfs",
430
+ "mkfs.msdos",
431
+ "mkfs.minix",
432
+ "mkfs.jfs",
433
+ "mkfs.reiserfs",
434
+ "mkfs.udf",
435
+ "mkfs.bfs",
436
+ "mkfs.cramfs"
437
+ ]],
438
+ decision: "forbidden",
439
+ justification: "Formats a block device, destroying its contents (exhaustive mkfs.* variant enumeration).",
440
+ match: [
441
+ "mkfs.exfat /dev/sda1",
442
+ "mkfs.f2fs /dev/sdb1",
443
+ "mkfs /dev/sdb1",
444
+ "mkfs.ext4 /dev/sda1"
445
+ ],
446
+ notMatch: ["ls /dev"]
447
+ }];
448
+ //#endregion
449
+ //#region src/codex-rules/rules/diskutil.ts
450
+ /**
451
+ * `diskutil` erase/partition variants — hand-mapped from `security.ts`'s
452
+ * case-insensitive `\bdiskutil\s+(?:erase|partitionDisk)\b/i` CRITICAL
453
+ * label. `prefix_rule` has no case-insensitive matching, so every real
454
+ * casing macOS users actually type (camelCase, all-lowercase, all-caps) is
455
+ * enumerated explicitly as an alternation at position 1.
456
+ */
457
+ const DISKUTIL_RULES = [{
458
+ sourceLabel: "CRITICAL: Detected dangerous command 'diskutil erase'",
459
+ pattern: ["diskutil", [
460
+ "erase",
461
+ "eraseDisk",
462
+ "erasedisk",
463
+ "ERASEDISK",
464
+ "eraseVolume",
465
+ "erasevolume",
466
+ "ERASEVOLUME",
467
+ "partitionDisk",
468
+ "partitiondisk",
469
+ "PARTITIONDISK"
470
+ ]],
471
+ decision: "forbidden",
472
+ justification: "Erases or repartitions a macOS disk/volume (case-variant enumeration; execpolicy has no case-insensitive match).",
473
+ match: [
474
+ "diskutil eraseDisk /dev/disk2",
475
+ "diskutil ERASEDISK /dev/disk2",
476
+ "diskutil erase /dev/disk2"
477
+ ],
478
+ notMatch: ["diskutil list"]
479
+ }];
480
+ //#endregion
481
+ //#region src/codex-rules/rules/dd.ts
482
+ /**
483
+ * `dd` — hand-mapped from `security.ts`'s `CRITICAL: Detected dangerous
484
+ * command 'dd if='` label (source regex only fires on `of=/dev/...`).
485
+ * Owner decision: DOWNGRADE forbidden -> prompt. `prefix_rule` cannot
486
+ * glob-match a `key=value` argv token against a raw-device path, so a
487
+ * literal forbidden translation would silently block legitimate
488
+ * `dd if=a.img of=b.img` usage — prompting on every `dd` invocation instead
489
+ * never lets a raw-disk write through silently, without blocking safe use.
490
+ */
491
+ const DD_RULES = [{
492
+ sourceLabel: "CRITICAL: Detected dangerous command 'dd if='",
493
+ pattern: ["dd"],
494
+ decision: "prompt",
495
+ justification: "dd can overwrite a raw block device via of=/dev/...; prompts on every dd invocation (broader than security.ts, which only fires on of=/dev/*) because execpolicy cannot glob-match that argv token — owner decision: do not block legitimate dd usage.",
496
+ match: ["dd if=/dev/zero of=/dev/sda", "dd if=a.img of=b.img"]
497
+ }];
498
+ //#endregion
499
+ //#region src/codex-rules/rules/single-commands.ts
500
+ /**
501
+ * Single-purpose destructive binaries — hand-mapped 1:1 from `security.ts`'s
502
+ * `chmod 777 on /`, `shred`, `fdisk` CRITICAL_PATTERNS labels. `mkfs`,
503
+ * `dd`, and `diskutil erase` live in their own files (`mkfs.ts`, `dd.ts`,
504
+ * `diskutil.ts`) — each needed a wider case/variant enumeration than fits
505
+ * this file's SRP scope. The root-chmod rule needs the same flag-order
506
+ * alternation as `chmod-ask.ts` — source security.ts accepts flags before
507
+ * `777` for the root case too (`chmod -R 777 /` is still CRITICAL there).
508
+ */
509
+ const SINGLE_COMMAND_RULES = [
510
+ {
511
+ sourceLabel: "DANGEROUS PATTERN: chmod 777 on /",
512
+ pattern: [
513
+ "chmod",
514
+ "777",
515
+ "/"
516
+ ],
517
+ decision: "forbidden",
518
+ justification: "World-writable permissions on the filesystem root.",
519
+ match: ["chmod 777 /"]
520
+ },
521
+ {
522
+ sourceLabel: "DANGEROUS PATTERN: chmod 777 on /",
523
+ pattern: [
524
+ "chmod",
525
+ "-R",
526
+ "777",
527
+ "/"
528
+ ],
529
+ decision: "forbidden",
530
+ justification: "World-writable permissions on the filesystem root, recursive.",
531
+ match: ["chmod -R 777 /"]
532
+ },
533
+ {
534
+ sourceLabel: "DANGEROUS PATTERN: chmod 777 on /",
535
+ pattern: [
536
+ "chmod",
537
+ "-r",
538
+ "777",
539
+ "/"
540
+ ],
541
+ decision: "forbidden",
542
+ justification: "World-writable permissions on the filesystem root, recursive (lowercase flag).",
543
+ match: ["chmod -r 777 /"]
544
+ },
545
+ {
546
+ sourceLabel: "DANGEROUS PATTERN: chmod 777 on /",
547
+ pattern: [
548
+ "chmod",
549
+ "-v",
550
+ "777",
551
+ "/"
552
+ ],
553
+ decision: "forbidden",
554
+ justification: "World-writable permissions on the filesystem root, verbose flag.",
555
+ match: ["chmod -v 777 /"]
556
+ },
557
+ {
558
+ sourceLabel: "CRITICAL: Detected dangerous command 'shred'",
559
+ pattern: ["shred"],
560
+ decision: "forbidden",
561
+ justification: "Irreversibly overwrites file contents.",
562
+ match: ["shred -u secrets.txt"],
563
+ notMatch: ["cat secrets.txt"]
564
+ },
565
+ {
566
+ sourceLabel: "CRITICAL: Detected dangerous command 'fdisk'",
567
+ pattern: ["fdisk"],
568
+ decision: "forbidden",
569
+ justification: "Edits disk partition tables.",
570
+ match: ["fdisk /dev/sda"],
571
+ notMatch: ["cat /proc/partitions"]
572
+ }
573
+ ];
574
+ //#endregion
575
+ //#region src/codex-rules/rules/privilege.ts
576
+ /**
577
+ * Privilege-escalation binaries — hand-mapped 1:1 from `security.ts`'s
578
+ * `sudo`/`su`/`doas`/`passwd`/`del` CRITICAL_PATTERNS labels (parity
579
+ * `security_rules.PRIVILEGE_COMMANDS`).
580
+ */
581
+ const PRIVILEGE_RULES = [
582
+ {
583
+ sourceLabel: "PRIVILEGE ESCALATION: sudo",
584
+ pattern: ["sudo"],
585
+ decision: "forbidden",
586
+ justification: "Runs a command with elevated privileges.",
587
+ match: ["sudo rm -rf /tmp"],
588
+ notMatch: ["echo sudo"]
589
+ },
590
+ {
591
+ sourceLabel: "PRIVILEGE ESCALATION: su",
592
+ pattern: ["su"],
593
+ decision: "forbidden",
594
+ justification: "Switches to another user, typically root.",
595
+ match: ["su -", "su root"],
596
+ notMatch: ["sudo ls"]
597
+ },
598
+ {
599
+ sourceLabel: "PRIVILEGE ESCALATION: doas",
600
+ pattern: ["doas"],
601
+ decision: "forbidden",
602
+ justification: "BSD-style sudo alternative for elevated privileges.",
603
+ match: ["doas reboot"],
604
+ notMatch: ["ls"]
605
+ },
606
+ {
607
+ sourceLabel: "PRIVILEGE ESCALATION: passwd",
608
+ pattern: ["passwd"],
609
+ decision: "forbidden",
610
+ justification: "Changes a user's password.",
611
+ match: ["passwd root"],
612
+ notMatch: ["cat /etc/passwd"]
613
+ },
614
+ {
615
+ sourceLabel: "CRITICAL: Detected dangerous command 'del'",
616
+ pattern: ["del"],
617
+ decision: "forbidden",
618
+ justification: "Windows-style file deletion (parity security_rules.CRITICAL_COMMANDS).",
619
+ match: ["del file.txt"],
620
+ notMatch: ["delete.sh"]
621
+ }
622
+ ];
623
+ //#endregion
624
+ //#region src/codex-rules/rules/ask.ts
625
+ /**
626
+ * `prompt`-decision rules — hand-mapped 1:1 from `security.ts`'s
627
+ * ASK_PATTERNS. `DANGEROUS PATTERN: chmod 777` lives in `chmod-ask.ts`
628
+ * (needed multiple flag-order variants). `DANGEROUS PATTERN: write to /etc`
629
+ * is intentionally absent here: it fires on shell redirection
630
+ * (`> /etc/...`), which is not a single argv invocation — see
631
+ * `../skip-list.ts`.
632
+ */
633
+ const ASK_RULES$1 = [
634
+ {
635
+ sourceLabel: "DANGEROUS PATTERN: recursive chown",
636
+ pattern: ["chown", "-R"],
637
+ decision: "prompt",
638
+ justification: "Recursive ownership change; confirm scope.",
639
+ match: ["chown -R user:user /srv/app"],
640
+ notMatch: ["chown user file.txt"]
641
+ },
642
+ {
643
+ sourceLabel: "DANGEROUS PATTERN: eval",
644
+ pattern: ["eval"],
645
+ decision: "prompt",
646
+ justification: "Evaluates arbitrary shell input.",
647
+ match: ["eval $(cat script.sh)"],
648
+ notMatch: ["node eval.js"]
649
+ },
650
+ {
651
+ sourceLabel: "DELETE: 'rm' permanently deletes - confirmation required",
652
+ pattern: ["rm"],
653
+ decision: "prompt",
654
+ justification: "rm permanently deletes; confirm before running.",
655
+ match: ["rm file.txt"],
656
+ notMatch: ["rmdir empty"]
657
+ },
658
+ {
659
+ sourceLabel: "DELETE: 'unlink' command detected - confirmation required",
660
+ pattern: ["unlink"],
661
+ decision: "prompt",
662
+ justification: "unlink permanently removes a file; confirm before running.",
663
+ match: ["unlink file.txt"],
664
+ notMatch: ["ln -s a b"]
665
+ }
666
+ ];
667
+ //#endregion
668
+ //#region src/codex-rules/rules/chmod-ask.ts
669
+ /**
670
+ * `chmod 777` `prompt`-decision variants — hand-mapped from `security.ts`'s
671
+ * `DANGEROUS PATTERN: chmod 777` ASK_PATTERNS label. The source regex
672
+ * accepts any chain of `-[a-zA-Z]+` flags before `777`; `prefix_rule` needs
673
+ * one literal-token variant per common flag order, so every frequent
674
+ * ordering is enumerated. Always `prompt`: `prefix_rule` cannot distinguish
675
+ * an absolute-path target from a relative one, so both are prompted rather
676
+ * than letting an absolute-path `chmod 777` fall through silently.
677
+ */
678
+ const CHMOD_ASK_RULES = [
679
+ {
680
+ sourceLabel: "DANGEROUS PATTERN: chmod 777",
681
+ pattern: ["chmod", "777"],
682
+ decision: "prompt",
683
+ justification: "World-writable permissions; confirm the target is intended.",
684
+ match: ["chmod 777 file.txt", "chmod 777 /tmp/file"],
685
+ notMatch: ["chmod 755 file.txt"]
686
+ },
687
+ {
688
+ sourceLabel: "DANGEROUS PATTERN: chmod 777",
689
+ pattern: [
690
+ "chmod",
691
+ "-R",
692
+ "777"
693
+ ],
694
+ decision: "prompt",
695
+ justification: "World-writable permissions, recursive; confirm the target is intended.",
696
+ match: ["chmod -R 777 dir"],
697
+ notMatch: ["chmod -R 755 dir"]
698
+ },
699
+ {
700
+ sourceLabel: "DANGEROUS PATTERN: chmod 777",
701
+ pattern: [
702
+ "chmod",
703
+ "-r",
704
+ "777"
705
+ ],
706
+ decision: "prompt",
707
+ justification: "World-writable permissions, recursive (lowercase flag); confirm the target is intended.",
708
+ match: ["chmod -r 777 dir"]
709
+ },
710
+ {
711
+ sourceLabel: "DANGEROUS PATTERN: chmod 777",
712
+ pattern: [
713
+ "chmod",
714
+ "-v",
715
+ "777"
716
+ ],
717
+ decision: "prompt",
718
+ justification: "World-writable permissions, verbose flag; confirm the target is intended.",
719
+ match: ["chmod -v 777 file.txt"]
720
+ },
721
+ {
722
+ sourceLabel: "DANGEROUS PATTERN: chmod 777",
723
+ pattern: [
724
+ "chmod",
725
+ "-Rv",
726
+ "777"
727
+ ],
728
+ decision: "prompt",
729
+ justification: "World-writable permissions, combined recursive+verbose flags; confirm the target is intended.",
730
+ match: ["chmod -Rv 777 dir"]
731
+ },
732
+ {
733
+ sourceLabel: "DANGEROUS PATTERN: chmod 777",
734
+ pattern: [
735
+ "chmod",
736
+ "-vR",
737
+ "777"
738
+ ],
739
+ decision: "prompt",
740
+ justification: "World-writable permissions, combined verbose+recursive flags; confirm the target is intended.",
741
+ match: ["chmod -vR 777 dir"]
742
+ }
743
+ ];
744
+ //#endregion
745
+ //#region src/codex-rules/rules/remote-fetch.ts
746
+ /**
747
+ * Remote-fetch tools (`curl`/`wget`) — targets the DOWNLOAD half of a
748
+ * `curl ... | sh` pipe instead of the shell half. Codex splits a piped
749
+ * command into independent argv segments at the pipe, so a `prefix_rule`
750
+ * anchored on `curl`/`wget` catches the attack regardless of which shell
751
+ * receives the piped output on the other side — unlike a bare-shell rule
752
+ * (`sh`/`bash`/`zsh`), which prompts on EVERY Codex-issued `bash -lc "..."`
753
+ * wrapper (Codex wraps essentially all shell commands this way), producing
754
+ * noise on ~50% of legitimate day-to-day work. Owner decision: DOWNGRADE
755
+ * forbidden -> prompt, same rationale as `dd.ts` — `prefix_rule` cannot see
756
+ * the pipe operator itself, so it cannot distinguish a bare `curl url`
757
+ * fetch from `curl url | sh`; prompting on every curl/wget invocation never
758
+ * lets the attack through silently, without blocking legitimate downloads.
759
+ */
760
+ const REMOTE_FETCH_RULES = [{
761
+ sourceLabel: "DANGEROUS PATTERN: remote script piped to a shell",
762
+ pattern: [["curl", "wget"]],
763
+ decision: "prompt",
764
+ justification: "Remote fetch; piping its output into a shell executes untrusted code. Broader than security.ts (which only fires when the fetch is piped into sh/bash/zsh): prompts on every curl/wget invocation because execpolicy cannot see the pipe operator, i.e. it cannot distinguish `curl url` from `curl url | sh` — owner decision: do not block legitimate curl/wget usage.",
765
+ match: [
766
+ "curl http://example.com/i.sh",
767
+ "wget http://example.com/i.sh",
768
+ "curl -fsSL http://example.com/i.sh"
769
+ ],
770
+ notMatch: ["cursor --version", "widget-tool build"]
771
+ }];
772
+ //#endregion
773
+ //#region src/codex-rules/rules/absolute-paths.ts
774
+ /**
775
+ * Absolute-path executable aliases — Codex only resolves an absolute path
776
+ * to its basename rule when the CLI is run with `--resolve-host-executables`
777
+ * (opt-in, off by default; this generated `.rules` file cannot assume it).
778
+ * `/bin/rm -rf /` and a bare `rm -rf /` are two DIFFERENT argv prefixes to
779
+ * execpolicy, so every rule keyed on one of these bare names is mirrored
780
+ * onto its common absolute-path forms.
781
+ */
782
+ const ABSOLUTE_PATH_ALIASES = {
783
+ rm: ["/bin/rm", "/usr/bin/rm"],
784
+ chmod: ["/bin/chmod", "/usr/bin/chmod"],
785
+ diskutil: ["/usr/sbin/diskutil"],
786
+ dd: ["/bin/dd", "/usr/bin/dd"],
787
+ sudo: ["/usr/bin/sudo"]
788
+ };
789
+ /** Escape regex metacharacters so a dynamic token can be safely embedded in a `RegExp` literal. */
790
+ function escapeRegExp(token) {
791
+ return token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
792
+ }
793
+ /** Replace a match/notMatch example's leading token with an absolute-path alias. */
794
+ function aliasExample(example, from, to) {
795
+ return example.replace(new RegExp(`^${escapeRegExp(from)}\\b`), to);
796
+ }
797
+ /**
798
+ * Clone every rule in `rules` whose first pattern token has a known
799
+ * absolute-path alias, producing one variant per alias (same decision,
800
+ * same sourceLabel — traced back to the same `security.ts` label).
801
+ */
802
+ function withAbsolutePathAliases(rules) {
803
+ const out = [];
804
+ for (const rule of rules) {
805
+ const first = rule.pattern[0];
806
+ if (typeof first !== "string") continue;
807
+ const aliases = ABSOLUTE_PATH_ALIASES[first];
808
+ if (!aliases) continue;
809
+ for (const alias of aliases) out.push({
810
+ ...rule,
811
+ pattern: [alias, ...rule.pattern.slice(1)],
812
+ match: rule.match.map((m) => aliasExample(m, first, alias)),
813
+ notMatch: rule.notMatch?.map((m) => aliasExample(m, first, alias)),
814
+ justification: `${rule.justification} (absolute-path form; Codex does not resolve executable basenames by default).`
815
+ });
816
+ }
817
+ return out;
818
+ }
819
+ //#endregion
820
+ //#region src/codex-rules/skip-list.ts
821
+ /**
822
+ * `security.ts` labels with NO argv-prefix equivalent. Every entry here is
823
+ * shell syntax (pipes, redirection, function-definition syntax) that never
824
+ * appears as a plain argv token when a command is exec'd without a shell —
825
+ * Codex execpolicy's `prefix_rule` cannot express them. Covered instead by
826
+ * `sandbox_mode`/`approval_policy`. Never drop a label silently: it is
827
+ * either a prefix_rule (see `rules/`) or an entry here.
828
+ */
829
+ const SKIP_LIST = [
830
+ {
831
+ sourceLabel: "DANGEROUS PATTERN: fork bomb",
832
+ reason: "Shell function-definition syntax `:(){ :|:& };:` is not a single argv invocation — no prefix_rule can express it."
833
+ },
834
+ {
835
+ sourceLabel: "DANGEROUS PATTERN: redirect to a raw disk device",
836
+ reason: "`>`/`>>` redirection to /dev/sda is shell syntax, invisible to argv-prefix matching."
837
+ },
838
+ {
839
+ sourceLabel: "DANGEROUS PATTERN: write to /etc",
840
+ reason: "`>`/`>>` redirection into /etc/* is shell syntax, invisible to argv-prefix matching."
841
+ }
842
+ ];
843
+ //#endregion
844
+ //#region src/codex-rules/mapping.ts
845
+ /** Every `decision = "forbidden"` rule, hand-mapped from CRITICAL_PATTERNS. */
846
+ const CRITICAL_BASE = [
847
+ ...RM_VARIANT_RULES,
848
+ ...MKFS_RULES,
849
+ ...DISKUTIL_RULES,
850
+ ...SINGLE_COMMAND_RULES,
851
+ ...PRIVILEGE_RULES
852
+ ];
853
+ /** Every `decision = "prompt"` rule, hand-mapped from ASK_PATTERNS (+ dd, downgraded). */
854
+ const ASK_BASE = [
855
+ ...ASK_RULES$1,
856
+ ...CHMOD_ASK_RULES,
857
+ ...DD_RULES,
858
+ ...REMOTE_FETCH_RULES
859
+ ];
860
+ /** Absolute-path mirrors of the bare-name rules above (see `rules/absolute-paths.ts`). */
861
+ const ABSOLUTE_ALIASES = withAbsolutePathAliases([...CRITICAL_BASE, ...ASK_BASE]);
862
+ const CRITICAL_RULES = [...CRITICAL_BASE, ...ABSOLUTE_ALIASES.filter((r) => r.decision === "forbidden")];
863
+ const ASK_RULES = [...ASK_BASE, ...ABSOLUTE_ALIASES.filter((r) => r.decision === "prompt")];
864
+ /** Every generated `prefix_rule`, critical (`forbidden`) + ask (`prompt`). */
865
+ const ALL_RULES = [...CRITICAL_RULES, ...ASK_RULES];
866
+ //#endregion
867
+ //#region src/codex-rules/starlark.ts
868
+ /**
869
+ * Neutralize characters that would break out of a Starlark `#` line comment
870
+ * (a bare `\n`/`\r` in an interpolated label/reason would start a new line
871
+ * no longer prefixed by `#`, corrupting the generated file).
872
+ */
873
+ function sanitizeComment(text) {
874
+ return text.replace(/[\r\n]+/g, " ");
875
+ }
876
+ /** Render a string array as a Starlark list literal (double-quoted strings). */
877
+ function starlarkList(values) {
878
+ return `[${values.map((v) => JSON.stringify(v)).join(", ")}]`;
879
+ }
880
+ /** Render one pattern element: a bare token, or a `[...]` alternation. */
881
+ function starlarkPatternElement(el) {
882
+ return Array.isArray(el) ? starlarkList(el) : JSON.stringify(el);
883
+ }
884
+ /**
885
+ * Render a single `CodexPrefixRule` as a Starlark `prefix_rule(...)` call,
886
+ * prefixed by a traceability comment pointing back at its `security.ts` label.
887
+ */
888
+ function renderPrefixRule(rule) {
889
+ const pattern = `[${rule.pattern.map(starlarkPatternElement).join(", ")}]`;
890
+ const lines = [
891
+ `# source: security.ts label "${sanitizeComment(rule.sourceLabel)}"`,
892
+ "prefix_rule(",
893
+ ` pattern = ${pattern},`,
894
+ ` decision = "${rule.decision}",`,
895
+ ` justification = ${JSON.stringify(rule.justification)},`,
896
+ ` match = ${starlarkList(rule.match)},`
897
+ ];
898
+ if (rule.notMatch && rule.notMatch.length > 0) lines.push(` not_match = ${starlarkList(rule.notMatch)},`);
899
+ lines.push(")");
900
+ return lines.join("\n");
901
+ }
902
+ /** Render a list of rules, each block separated by a blank line. */
903
+ function renderPrefixRules(rules) {
904
+ return rules.map(renderPrefixRule).join("\n\n");
905
+ }
906
+ //#endregion
907
+ //#region src/codex-rules/build.ts
908
+ const HEADER = "# Codex execpolicy rules — generated from @fusengine/harness src/policy/guards/security.ts\n# DO NOT hand-edit: regenerate via `harness codex-rules`.\n#\n# security.ts is the source of truth for dangerous-command detection; every\n# CRITICAL_PATTERNS label maps to decision = \"forbidden\", every ASK_PATTERNS\n# label maps to decision = \"prompt\". Strictest decision wins across matches.\n# The harness remains the AUTHORITATIVE enforcement layer — this file is a\n# complementary defense layer on the Codex side, not a replacement.\n#\n# IRREDUCIBLE gaps (shell syntax, invisible to argv-prefix matching, no\n# prefix_rule can express them) — compensated by sandbox_mode +\n# approval_policy.granular.sandbox_approval, not by this file:\n# - fork bomb: `:(){ :|:& };:`\n# - redirect to a raw disk device: `> /dev/sda`\n# - write to /etc via redirection: `> /etc/...`\n#\n# ASSUMED over-width (intentionally broader than security.ts, chosen to\n# avoid a silent gap rather than an exact-but-brittle translation):\n# - dd: prompts on every dd invocation; security.ts only fires on of=/dev/*\n# - rm: prompts even on paths security.ts exempts via a 'trash' substring\n# - rm/eval/unlink: prompts even on a bare token with no trailing argument;\n# security.ts requires a space (i.e. an argument) after the command\n# - chmod 777: prompts on relative paths too, not only absolute ones\n# - sudo (/usr/bin/sudo alias): forbids the absolute-path form too;\n# security.ts's word-boundary regex does not match a preceding '/'\n# - curl/wget: prompts on EVERY curl/wget invocation, not only when piped\n# into a shell; security.ts's \"remote script piped to a shell\" label\n# only fires on `curl|wget ... | sh`. A simple plain-word pipe IS split\n# by Codex into separate argv commands (a bare-shell rule would also\n# catch that case), but this rule additionally targets the download\n# half for compound chains and non-shell payload execution where\n# sh/bash never appears as argv[0] (curl -o x && chmod +x x && ./x,\n# curl ... | python3); it replaces a former bare sh/bash/zsh heuristic\n# that prompted on ~50% of Codex's own `bash -lc \"...\"` command\n# wrapping and rarely caught the actual download\n#\n# NOT covered (out of scope, not an over-width — a purely local pipe with no\n# network fetch is invisible to this rule AND to security.ts, which only\n# matches curl/wget as the left-hand side of the pipe):\n# - `cat file | sh`: local pipe, no curl/wget involved\n# - quoted-variable/redirection installer one-liners, e.g.\n# `curl -fsSL \"$URL\" | sh`: variable substitution disables Codex's\n# pipe-splitting, collapsing the whole invocation to a single\n# ['bash', '-lc', '<script>'] where curl is not argv[0] — this exact\n# form was covered by the removed bare sh/bash heuristic and is no\n# longer covered by any rule here\n#\n# The following labels have NO argv-prefix equivalent at all (shell syntax\n# not visible to execpolicy's argv matching) and are intentionally NOT\n# expressed as prefix_rule() below — covered instead by sandbox_mode/approval_policy:";
909
+ /** Render the full Codex execpolicy `.rules` Starlark file as a string. */
910
+ function buildCodexRules() {
911
+ return `${HEADER}\n${SKIP_LIST.map((s) => `# - ${sanitizeComment(s.sourceLabel)}: ${sanitizeComment(s.reason)}`).join("\n")}\n\n${renderPrefixRules(ALL_RULES)}\n`;
912
+ }
913
+ //#endregion
296
914
  //#region src/refs/discover.ts
297
915
  /** Immediate subdirectory names of `p`, or `[]` when it is missing/inaccessible. */
298
916
  function subdirs(p) {
@@ -412,6 +1030,7 @@ function maybePlaySound(argv) {
412
1030
  * harness init [id] write the wiring file for a harness (defaults to the detected one)
413
1031
  * harness hook <id> runtime: read a hook payload on stdin, route to the adapter, print the response
414
1032
  * harness changelog fetch + diff the Claude Code changelog, print a JSON summary (changelog-watcher)
1033
+ * harness codex-rules generate a Codex execpolicy .rules (Starlark) file from security.ts; stdout or --out <path>
415
1034
  */
416
1035
  const hookDebug = process.env.FUSE_HARNESS_DEBUG === "1";
417
1036
  function traceHook(label, data) {
@@ -503,6 +1122,15 @@ else if (cmd === "scan") {
503
1122
  const dir = process.argv[3] ?? process.cwd();
504
1123
  process.stdout.write(JSON.stringify(runSecurityScan(dir), null, 2) + "\n");
505
1124
  process.exit(0);
1125
+ } else if (cmd === "codex-rules") {
1126
+ const outIdx = process.argv.indexOf("--out");
1127
+ const outPath = outIdx !== -1 ? process.argv[outIdx + 1] : void 0;
1128
+ const rules = buildCodexRules();
1129
+ if (outPath) {
1130
+ writeFileSync(outPath, rules);
1131
+ process.stderr.write(`harness: wrote Codex execpolicy rules -> ${outPath}\n`);
1132
+ } else process.stdout.write(rules);
1133
+ process.exit(0);
506
1134
  } else {
507
1135
  const files = stagedFiles();
508
1136
  if (files.length === 0) process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",