@codyswann/lisa 4.71.3 → 4.71.5

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 (86) hide show
  1. package/all/copy-overwrite/scripts/lib/npm-update-classifier-cache.mjs +94 -0
  2. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +1 -0
  3. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +9 -2
  4. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +122 -8
  5. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +12 -3
  6. package/dist/cli/hook-refresh-files.d.ts +86 -0
  7. package/dist/cli/hook-refresh-files.d.ts.map +1 -0
  8. package/dist/cli/hook-refresh-files.js +209 -0
  9. package/dist/cli/hook-refresh-files.js.map +1 -0
  10. package/dist/cli/index.d.ts +3 -0
  11. package/dist/cli/index.d.ts.map +1 -1
  12. package/dist/cli/index.js +3 -0
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/refresh-hooks-cmd.d.ts +27 -0
  15. package/dist/cli/refresh-hooks-cmd.d.ts.map +1 -0
  16. package/dist/cli/refresh-hooks-cmd.js +156 -0
  17. package/dist/cli/refresh-hooks-cmd.js.map +1 -0
  18. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  19. package/dist/core/lisa-owned-hash-ledger.js +20 -0
  20. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  21. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  22. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  23. package/dist/core/upstream-evidence-manifest.js +21 -9
  24. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  25. package/dist/opencode/plugin-templates/parity-safety-net.sh +122 -8
  26. package/package.json +4 -4
  27. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa/hooks/parity-safety-net.sh +122 -8
  30. package/plugins/lisa-agy/hooks/parity-safety-net.sh +122 -8
  31. package/plugins/lisa-agy/plugin.json +1 -1
  32. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  35. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +122 -8
  39. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +122 -8
  41. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-agy/plugin.json +1 -1
  44. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  64. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails-agy/plugin.json +1 -1
  69. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  74. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  79. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/src/base/hooks/parity-safety-net.sh +122 -8
  82. package/rails/copy-overwrite/scripts/lisa-scratch-run.sh +54 -9
  83. package/scripts/build-plugins.sh +3 -0
  84. package/scripts/generate-scratch-supervisor-profile.mjs +75 -0
  85. package/scripts/two-channel-couplings.json +1 -1
  86. package/typescript/copy-contents/.husky/commit-msg +28 -31
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -524,6 +524,94 @@ follow_names=""
524
524
  follow_count=0
525
525
  follow_bytes=0
526
526
  followed_text=""
527
+ followed_rm_text=""
528
+ # Generated from the canonical reviewed supervisor, never from a host manifest.
529
+ # Regenerate through scripts/generate-scratch-supervisor-profile.mjs.
530
+ readonly SCRATCH_SUPERVISOR_SHA256='6fdec1ad0441d7636dc6749c44a23167baf7b75004f156b8a7d4ce1c18c790b7'
531
+ readonly SCRATCH_SUPERVISOR_CLEANUP_COUNT='7'
532
+
533
+ # An authenticated public supervisor owns its fixed cleanup sites. Its original
534
+ # source remains visible to every other policy; its actual payload is followed.
535
+ follow_supervisor() {
536
+ local token="$1" depth="$2" statement="$3" path="" profile="" payload="" rm_source="" original=""
537
+ case "$token" in *lisa-scratch-run.sh) ;; *) return 1 ;; esac
538
+ follow_locate "$token" || follow_refuse "$token" "supervisor source could not be resolved"
539
+ path="$follow_located"
540
+ profile="$(python3 - "$path" "$SCRATCH_SUPERVISOR_SHA256" "$statement" "$token" "$SCRATCH_SUPERVISOR_CLEANUP_COUNT" <<'PY'
541
+ import hashlib
542
+ import json
543
+ import os
544
+ import re
545
+ import shlex
546
+ import stat
547
+ import sys
548
+
549
+ path, digest, statement, operand, expected_count = sys.argv[1:]
550
+ def refuse():
551
+ sys.exit(2)
552
+
553
+ try:
554
+ info = os.lstat(path)
555
+ if not stat.S_ISREG(info.st_mode) or info.st_size > 262144:
556
+ refuse()
557
+ raw = open(path, 'rb').read(262145)
558
+ if len(raw) != info.st_size or not re.fullmatch(r'[0-9a-f]{64}', digest):
559
+ refuse()
560
+ if hashlib.sha256(raw).hexdigest() != digest:
561
+ refuse()
562
+ tokens = shlex.split(statement)
563
+ indices = [index for index, token in enumerate(tokens) if token == operand]
564
+ if len(indices) != 1 or '$' in operand or '`' in operand:
565
+ refuse()
566
+ index = indices[0]
567
+ if index < 1 or tokens[index - 1] not in ('sh', 'bash', '/bin/sh', '/bin/bash'):
568
+ refuse()
569
+ prefix = tokens[:index - 1]
570
+ if prefix[:1] == ['env']:
571
+ prefix = prefix[1:]
572
+ assignments = {}
573
+ for token in prefix:
574
+ if '=' not in token or re.search(r'[$`\r\n]', token):
575
+ refuse()
576
+ key, value = token.split('=', 1)
577
+ if key not in ('LISA_SCRATCH_BASE', 'LISA_SCRATCH_TRACE', 'TMPDIR') or key in assignments:
578
+ refuse()
579
+ assignments[key] = value
580
+ # Match actual shell :- precedence, including inherited and
581
+ # explicitly empty values. TMPDIR cannot override a nonempty inherited base.
582
+ effective = dict(os.environ, **assignments)
583
+ base = effective.get('LISA_SCRATCH_BASE') or effective.get('TMPDIR') or '/tmp'
584
+ if not base or not os.path.isabs(base) or re.search(r'[$`\r\n]', base):
585
+ refuse()
586
+ real_base = os.path.realpath(base)
587
+ roots = ['/tmp', '/var/tmp', os.environ.get('TMPDIR', '/tmp')]
588
+ if not any(os.path.commonpath([real_base, os.path.realpath(root)]) == os.path.realpath(root) for root in roots if root and os.path.isabs(root)):
589
+ refuse()
590
+ args = tokens[index + 1:]
591
+ if len(args) < 4 or args[0] != '--suite' or args[2] != '--':
592
+ refuse()
593
+ if not re.fullmatch(r'[A-Za-z0-9._-]{1,64}', args[1]):
594
+ refuse()
595
+ payload = args[3:]
596
+ if not payload[0] or payload[0].startswith('-') or any(re.search(r'[$`\x00\r\n]', value) for value in payload):
597
+ refuse()
598
+ source = raw.decode('utf8')
599
+ cleaned, count = re.subn(r'(?m)^(\s*)rm -rf "(\$_quarantine|\$_root|\$_entry|\$LISA_SCRATCH_ROOT)"', r'\1:', source)
600
+ if not re.fullmatch(r'[1-9][0-9]*', expected_count) or count != int(expected_count):
601
+ refuse()
602
+ print(json.dumps({'source': cleaned, 'original': source, 'payload': shlex.join(payload)}))
603
+ except (OSError, UnicodeError, ValueError):
604
+ refuse()
605
+ PY
606
+ )" || follow_refuse "$path" "supervisor source or public entry authority could not be authenticated"
607
+ rm_source="$(printf '%s' "$profile" | jq -er '.source')" || follow_refuse "$path" "invalid supervisor profile"
608
+ original="$(printf '%s' "$profile" | jq -er '.original')" || follow_refuse "$path" "invalid supervisor source"
609
+ payload="$(printf '%s' "$profile" | jq -er '.payload')" || follow_refuse "$path" "invalid supervisor payload"
610
+ follow_take "$path" "$depth" "$rm_source" "$original"
611
+ [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ] || follow_refuse "$path" "supervisor payload exceeds inspection depth"
612
+ follow_scan "$payload" "$((depth + 1))"
613
+ return 0
614
+ }
527
615
 
528
616
  FOLLOW_GUIDANCE="This command EXECUTES a file. These guards classify shell text, so the script an
529
617
  invocation runs is read and scanned as part of the command — a destructive line
@@ -810,7 +898,7 @@ follow_locate() {
810
898
  # executes, to a bounded depth. Full-line comments are dropped: a commented line
811
899
  # is never executed, so scanning it can only manufacture a false refusal.
812
900
  follow_take() {
813
- local path="$1" depth="$2" size="" content=""
901
+ local path="$1" depth="$2" size="" content="" rm_content="${3-}"
814
902
  case "$follow_paths" in
815
903
  *$'\n'"$path"$'\n'*) return 0 ;;
816
904
  esac
@@ -830,10 +918,22 @@ follow_take() {
830
918
  if [ "$follow_bytes" -gt "$FOLLOW_MAX_TOTAL_BYTES" ]; then
831
919
  follow_refuse "$path" "the files this command executes total more than the ${FOLLOW_MAX_TOTAL_BYTES}-byte inspection budget"
832
920
  fi
833
- content="$(LC_ALL=C tr -d '\000' <"$path" | sed 's/^[[:space:]]*#.*$//')" \
834
- || follow_refuse "$path" "it could not be read"
921
+ if [ "$#" -eq 4 ]; then
922
+ # Scan the same complete bytes whose digest was authenticated, not a
923
+ # second file read that could pair changed source with the cleanup view.
924
+ content="$(printf '%s' "$4" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "authenticated source could not be read"
925
+ else
926
+ content="$(LC_ALL=C tr -d '\000' <"$path" | sed 's/^[[:space:]]*#.*$//')" \
927
+ || follow_refuse "$path" "it could not be read"
928
+ fi
835
929
  follow_names="$follow_names $path"
836
930
  followed_text="$followed_text"$'\n'"$content"
931
+ if [ "$#" -lt 3 ]; then
932
+ rm_content="$content"
933
+ else
934
+ rm_content="$(printf '%s' "$rm_content" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "supervisor cleanup view could not be read"
935
+ fi
936
+ followed_rm_text="$followed_rm_text"$'\n'"$rm_content"
837
937
  if [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ]; then
838
938
  # A `cd` inside the followed script governs what THAT script goes on to
839
939
  # execute, and nothing after it in the command that ran the script — the
@@ -852,6 +952,11 @@ follow_take() {
852
952
  follow_target() {
853
953
  local token="$1" depth="$2" path=""
854
954
  [ -n "$token" ] || return 0
955
+ # Entry authority belongs to each invocation, not the file-read cache. Only
956
+ # follow_supervisor may admit the exact public interpreter/argv profile.
957
+ case "$token" in
958
+ *lisa-scratch-run.sh) follow_refuse "$token" "supervisor requires its authenticated public interpreter entry" ;;
959
+ esac
855
960
  if ! follow_locate "$token"; then
856
961
  if [ "$depth" -eq 0 ]; then
857
962
  follow_refuse "$token" "no readable file could be resolved for it"
@@ -885,6 +990,9 @@ follow_direct() {
885
990
  return 0
886
991
  fi
887
992
  path="$follow_located"
993
+ case "$path" in
994
+ *lisa-scratch-run.sh) follow_refuse "$path" "supervisor requires its authenticated public interpreter entry" ;;
995
+ esac
888
996
  IFS= read -r -n 256 raw <"$path" 2>/dev/null || true
889
997
  while [ "$k" -lt "${#raw}" ]; do
890
998
  char="${raw:k:1}"
@@ -1216,7 +1324,9 @@ follow_scan() {
1216
1324
  fi
1217
1325
  follow_unwrap "${toks[j]}"
1218
1326
  tj="$follow_unwrapped"
1219
- follow_target "$tj" "$depth"
1327
+ if ! follow_supervisor "$tj" "$depth" "$stmt"; then
1328
+ follow_target "$tj" "$depth"
1329
+ fi
1220
1330
  cmd_pos=0
1221
1331
  i=$((j + 1))
1222
1332
  continue
@@ -1245,6 +1355,7 @@ follow_scan() {
1245
1355
  }
1246
1356
 
1247
1357
  follow_scan "$command_for_guards" 0
1358
+ rm_command_for_guards="$command_for_guards"$'\n'"$followed_rm_text"
1248
1359
  if [ -n "$follow_names" ]; then
1249
1360
  command_for_guards="$command_for_guards"$'\n'"$followed_text"
1250
1361
  # A refusal must say WHERE the match is. Without this the operator reads
@@ -1263,6 +1374,8 @@ fi
1263
1374
  # `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
1264
1375
  normalized_command_str="$(printf '%s' "$command_for_guards" \
1265
1376
  | awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
1377
+ rm_normalized_command_str="$(printf '%s' "$rm_command_for_guards" \
1378
+ | awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
1266
1379
 
1267
1380
  # matches / matches_cs run an ERE against the guarded command text. matches is
1268
1381
  # case-insensitive (the default for these guards); matches_cs is case-SENSITIVE
@@ -1567,7 +1680,7 @@ rm_assignment_value() {
1567
1680
  # small lexer below preserves quoted right-hand sides such as V="/tmp/x"
1568
1681
  # while excluding an argument such as echo "V=/tmp/x". Any ambiguity fails
1569
1682
  # closed.
1570
- RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$normalized_command_str" \
1683
+ RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$rm_normalized_command_str" \
1571
1684
  RM_ASSIGNMENT_STATEMENT="$rm_assignment_stmt" \
1572
1685
  python3 - <<'PY'
1573
1686
  import os
@@ -1766,7 +1879,7 @@ classify_rm_target() {
1766
1879
  # process substitution cannot deny anything either — it ends that subshell, not
1767
1880
  # this hook.
1768
1881
  rm_segments_status=0
1769
- rm_segments="$(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
1882
+ rm_segments="$(printf '%s' "$rm_normalized_command_str" | tr '&|;' '\n' \
1770
1883
  | grep -Ei -- "$RM_CMD"'([[:space:]]|$)')" || rm_segments_status=$?
1771
1884
  case "$rm_segments_status" in
1772
1885
  0 | 1) ;;
@@ -2278,9 +2391,10 @@ git_segments="$(printf '%s' "$normalized_command_str" \
2278
2391
  -e 's/[0-9]*>&[0-9-]*/>/g' \
2279
2392
  -e 's/[0-9]*<&[0-9-]*/</g' \
2280
2393
  -e 's/||/;/g' \
2281
- | tr '&;' '\n')" || git_segments_status=$?
2394
+ | tr '&;' '\n' \
2395
+ | grep -E -- "$RM_RF_CLUSTER|$RM_RF_SPLIT")" || git_segments_status=$?
2282
2396
  case "$git_segments_status" in
2283
- 0) ;;
2397
+ 0 | 1) ;;
2284
2398
  *) scan_failed "$git_segments_status" ;;
2285
2399
  esac
2286
2400
  while IFS= read -r git_stmt; do
@@ -275,6 +275,15 @@ lisa_token_is_well_formed() {
275
275
  [ "${#1}" -eq 64 ]
276
276
  }
277
277
 
278
+ # The basename is a 96-bit locator, never the run's 256-bit authority. Keeping
279
+ # it independent of suite length leaves Chrome's six-character temp suffix and
280
+ # SingletonSocket within 104 bytes including NUL under canonical /private/tmp.
281
+ # The complete token still binds the marker, acknowledgement and quarantine.
282
+ lisa_root_matches_token() {
283
+ lisa_token_is_well_formed "$2" || return 1
284
+ [ "${1##*/}" = "$(printf 'r.%.24s' "$2")" ]
285
+ }
286
+
278
287
  lisa_suite_is_well_formed() {
279
288
  case "$1" in
280
289
  "" | *[!A-Za-z0-9._-]*) return 1 ;;
@@ -325,6 +334,10 @@ lisa_authority_main() {
325
334
  lisa_die "$EX_AMBIGUOUS" "authority: unreadable or malformed arming marker"
326
335
  lisa_token_is_well_formed "$_token" ||
327
336
  lisa_die "$EX_AMBIGUOUS" "authority: malformed run token"
337
+ [ "$_token" = "${LISA_SCRATCH_TOKEN:-}" ] ||
338
+ lisa_die "$EX_AMBIGUOUS" "authority: run token differs from supervisor authority"
339
+ lisa_root_matches_token "$_root" "$_token" ||
340
+ lisa_die "$EX_AMBIGUOUS" "authority: run locator differs from full token"
328
341
  _version="$(lisa_marker_get "$_marker" version)" ||
329
342
  lisa_die "$EX_AMBIGUOUS" "authority: arming marker has no version"
330
343
  [ "$_version" = "$LISA_SCRATCH_VERSION" ] ||
@@ -503,6 +516,10 @@ lisa_remove_root() {
503
516
  printf '%s\n' "lisa-scratch-run: run root no longer well-formed; refusing to delete" >&2
504
517
  return 1
505
518
  }
519
+ lisa_root_matches_token "$_root" "$_token" || {
520
+ printf '%s\n' "lisa-scratch-run: run locator differs from full token; refusing to delete" >&2
521
+ return 1
522
+ }
506
523
  _now="$(lisa_dev_ino "$_root")" || return 1
507
524
  if [ "$_now" != "$_dev_ino" ]; then
508
525
  printf '%s\n' "lisa-scratch-run: run root filesystem identity changed; refusing to delete" >&2
@@ -546,6 +563,9 @@ lisa_launcher_main() {
546
563
  _token="$2"
547
564
  shift 2
548
565
 
566
+ lisa_root_matches_token "$_root" "$_token" ||
567
+ lisa_die "$EX_ARM" "payload run locator differs from full token; refusing"
568
+
549
569
  if ! lisa_isolate_group LISA_SCRATCH_LAUNCH_REENTRY \
550
570
  "$LISA_SCRATCH_SH" "$LISA_SCRATCH_SELF" --launch "$_root" "$_token" "$@"; then
551
571
  lisa_die "$EX_ARM" "cannot isolate the payload process group; refusing"
@@ -667,7 +687,11 @@ lisa_abort_arming() {
667
687
  [ -n "${LISA_SCRATCH_PAYLOAD_PID:-}" ] && kill -TERM "$LISA_SCRATCH_PAYLOAD_PID" 2>/dev/null
668
688
  [ -n "${LISA_SCRATCH_AUTHORITY_PID:-}" ] && kill -TERM "$LISA_SCRATCH_AUTHORITY_PID" 2>/dev/null
669
689
  if [ -n "${LISA_SCRATCH_ROOT:-}" ] &&
670
- lisa_root_is_well_formed "$LISA_SCRATCH_ROOT" "${LISA_SCRATCH_BASE_CANONICAL:-}"; then
690
+ [ -n "${LISA_SCRATCH_ROOT_DEVINO:-}" ] &&
691
+ lisa_root_is_well_formed "$LISA_SCRATCH_ROOT" "${LISA_SCRATCH_BASE_CANONICAL:-}" &&
692
+ lisa_root_matches_token "$LISA_SCRATCH_ROOT" "${LISA_SCRATCH_TOKEN:-}" &&
693
+ [ "$(lisa_canonical_dir "$LISA_SCRATCH_ROOT")" = "$LISA_SCRATCH_ROOT" ] &&
694
+ [ "$(lisa_dev_ino "$LISA_SCRATCH_ROOT")" = "$LISA_SCRATCH_ROOT_DEVINO" ]; then
671
695
  rm -rf "$LISA_SCRATCH_ROOT" 2>/dev/null || true
672
696
  fi
673
697
  kill -"$_sig" $$
@@ -720,6 +744,7 @@ lisa_supervisor_main() {
720
744
  # handler is older than every resource it can be asked to release, and it
721
745
  # simply does nothing while there is nothing to release.
722
746
  LISA_SCRATCH_ROOT=""
747
+ LISA_SCRATCH_ROOT_DEVINO=""
723
748
  LISA_SCRATCH_BASE_CANONICAL=""
724
749
  LISA_SCRATCH_PAYLOAD_PID=""
725
750
  LISA_SCRATCH_AUTHORITY_PID=""
@@ -742,28 +767,48 @@ lisa_supervisor_main() {
742
767
  lisa_die "$EX_ARM" "cannot create scratch namespace under $_base"
743
768
  chmod 700 "$_base/$LISA_SCRATCH_NAMESPACE" 2>/dev/null || true
744
769
 
745
- _root="$_base/$LISA_SCRATCH_NAMESPACE/$_suite.$_token"
770
+ _locator="$(printf 'r.%.24s' "$_token")"
771
+ _root="$_base/$LISA_SCRATCH_NAMESPACE/$_locator"
746
772
  lisa_root_is_well_formed "$_root" "$_base" ||
747
773
  lisa_die "$EX_ARM" "computed run root is not well-formed: $_root"
748
774
 
749
- # The already-installed handler becomes able to act the moment these are set,
750
- # which is one statement before the directory they name can exist.
775
+ # Naming a root does not authorize deletion: exclusive creation and its
776
+ # captured filesystem identity must also succeed before the handler can act.
751
777
  LISA_SCRATCH_ROOT="$_root"
752
778
  LISA_SCRATCH_TOKEN="$_token"
753
779
  LISA_SCRATCH_SUITE="$_suite"
754
780
  LISA_SCRATCH_BASE_CANONICAL="$_base"
755
781
  export LISA_SCRATCH_ROOT LISA_SCRATCH_TOKEN LISA_SCRATCH_SUITE
756
782
 
757
- # Exclusive create: mkdir of an existing leaf fails, so two runs can never
758
- # share a root even if a token somehow repeated.
759
- mkdir "$_root" || lisa_die "$EX_ARM" "cannot exclusively create run root"
783
+ # Latch signals across exclusive creation and identity capture. Otherwise a
784
+ # signal between mkdir's success and the next assignment could either leak
785
+ # our newly created root or mistake a collision for something we own.
786
+ LISA_SCRATCH_ARM_SIGNAL=""
787
+ for _sig in TERM INT HUP; do
788
+ # shellcheck disable=SC2064
789
+ trap "LISA_SCRATCH_ARM_SIGNAL=$_sig" "$_sig"
790
+ done
791
+ _created=no
792
+ if mkdir "$_root"; then
793
+ _created=yes
794
+ LISA_SCRATCH_ROOT_DEVINO="$(lisa_dev_ino "$_root")" || LISA_SCRATCH_ROOT_DEVINO=""
795
+ fi
796
+ for _sig in TERM INT HUP; do
797
+ # shellcheck disable=SC2064
798
+ trap "lisa_abort_arming $_sig" "$_sig"
799
+ done
800
+ [ -z "$LISA_SCRATCH_ARM_SIGNAL" ] || lisa_abort_arming "$LISA_SCRATCH_ARM_SIGNAL"
801
+ [ "$_created" = yes ] || lisa_die "$EX_ARM" "cannot exclusively create run root"
802
+ [ -n "$LISA_SCRATCH_ROOT_DEVINO" ] ||
803
+ lisa_die "$EX_ARM" "cannot read run root filesystem identity"
760
804
  chmod 700 "$_root" 2>/dev/null || true
761
805
  _canon="$(lisa_canonical_dir "$_root")" ||
762
806
  lisa_die "$EX_ARM" "run root is not a canonical directory"
763
807
  [ "$_canon" = "$_root" ] ||
764
808
  lisa_die "$EX_ARM" "run root resolves elsewhere: $_root -> $_canon"
765
- _dev_ino="$(lisa_dev_ino "$_root")" ||
766
- lisa_die "$EX_ARM" "cannot read run root filesystem identity"
809
+ _dev_ino="$LISA_SCRATCH_ROOT_DEVINO"
810
+ [ "$(lisa_dev_ino "$_root")" = "$_dev_ino" ] ||
811
+ lisa_die "$EX_ARM" "run root filesystem identity changed during creation"
767
812
 
768
813
  lisa_trace "arm-begin suite=$_suite root=$_root"
769
814
 
@@ -126,6 +126,9 @@ build_per_agent_variant() {
126
126
  }
127
127
 
128
128
  # Base plugin
129
+ if [ -f "$ROOT_DIR/scripts/generate-scratch-supervisor-profile.mjs" ]; then
130
+ node "$ROOT_DIR/scripts/generate-scratch-supervisor-profile.mjs"
131
+ fi
129
132
  build_plugin base lisa
130
133
 
131
134
  # Threshold-ratchet comparator: the canonical implementation lives in the base
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Bind the guard's reviewed public-supervisor profile to canonical source.
4
+ * The generated constants ship inside the guard. A host manifest or command
5
+ * argument cannot replace them. Source changes require generation and review.
6
+ * @module scripts/generate-scratch-supervisor-profile
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
13
+
14
+ /** Render the two fixed constants from complete canonical supervisor bytes.
15
+ * @param {string} guard - Editable canonical guard source.
16
+ * @param {Buffer} source - Complete canonical supervisor bytes.
17
+ * @returns {string} Guard with the exact source profile.
18
+ */
19
+ export function renderScratchProfile(guard, source) {
20
+ const text = source.toString("utf8");
21
+ if (!Buffer.from(text).equals(source) || source.length > 262144) {
22
+ throw new Error("Canonical supervisor source is not bounded UTF8");
23
+ }
24
+ const count = [
25
+ ...text.matchAll(
26
+ /^\s*rm -rf "(\$_quarantine|\$_root|\$_entry|\$LISA_SCRATCH_ROOT)"/gm
27
+ ),
28
+ ].length;
29
+ if (count === 0)
30
+ throw new Error("Canonical supervisor cleanup profile is absent");
31
+ const values = {
32
+ SHA256: createHash("sha256").update(source).digest("hex"),
33
+ CLEANUP_COUNT: String(count),
34
+ };
35
+ return Object.entries(values).reduce((current, [key, value]) => {
36
+ const expression = new RegExp(
37
+ `^readonly SCRATCH_SUPERVISOR_${key}='[^'\\n]*'$`,
38
+ "gm"
39
+ );
40
+ if ([...current.matchAll(expression)].length !== 1) {
41
+ throw new Error("Canonical guard source profile is ambiguous");
42
+ }
43
+ return current.replace(
44
+ expression,
45
+ `readonly SCRATCH_SUPERVISOR_${key}='${value}'`
46
+ );
47
+ }, guard);
48
+ }
49
+
50
+ if (invokedAsScript(import.meta.url)) {
51
+ if (
52
+ process.argv.slice(2).some(argument => argument !== "--check") ||
53
+ process.argv.length > 3
54
+ ) {
55
+ throw new Error("Usage: generate-scratch-supervisor-profile.mjs [--check]");
56
+ }
57
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
58
+ const guardPath = path.join(
59
+ root,
60
+ "plugins/src/base/hooks/parity-safety-net.sh"
61
+ );
62
+ const current = readFileSync(guardPath, "utf8");
63
+ const rendered = renderScratchProfile(
64
+ current,
65
+ readFileSync(
66
+ path.join(root, "rails/copy-overwrite/scripts/lisa-scratch-run.sh")
67
+ )
68
+ );
69
+ if (process.argv.includes("--check")) {
70
+ if (current !== rendered)
71
+ throw new Error("Canonical supervisor source profile is stale");
72
+ } else if (current !== rendered) {
73
+ writeFileSync(guardPath, rendered);
74
+ }
75
+ }
@@ -105,7 +105,7 @@
105
105
  "workflows": 25,
106
106
  "steps": 798,
107
107
  "couplings": 35,
108
- "inventory": 163
108
+ "inventory": 164
109
109
  },
110
110
  "counts": {
111
111
  "package-backed": 10,