@codyswann/lisa 4.71.3 → 4.71.4
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/all/copy-overwrite/scripts/lib/npm-update-classifier-cache.mjs +94 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +1 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +9 -2
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +122 -8
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +12 -3
- package/dist/cli/hook-refresh-files.d.ts +86 -0
- package/dist/cli/hook-refresh-files.d.ts.map +1 -0
- package/dist/cli/hook-refresh-files.js +209 -0
- package/dist/cli/hook-refresh-files.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/refresh-hooks-cmd.d.ts +27 -0
- package/dist/cli/refresh-hooks-cmd.d.ts.map +1 -0
- package/dist/cli/refresh-hooks-cmd.js +156 -0
- package/dist/cli/refresh-hooks-cmd.js.map +1 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +20 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +20 -8
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +122 -8
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +122 -8
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +122 -8
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +122 -8
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +122 -8
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +122 -8
- package/rails/copy-overwrite/scripts/lisa-scratch-run.sh +54 -9
- package/scripts/build-plugins.sh +3 -0
- package/scripts/generate-scratch-supervisor-profile.mjs +75 -0
- package/scripts/two-channel-couplings.json +1 -1
|
@@ -527,6 +527,94 @@ follow_names=""
|
|
|
527
527
|
follow_count=0
|
|
528
528
|
follow_bytes=0
|
|
529
529
|
followed_text=""
|
|
530
|
+
followed_rm_text=""
|
|
531
|
+
# Generated from the canonical reviewed supervisor, never from a host manifest.
|
|
532
|
+
# Regenerate through scripts/generate-scratch-supervisor-profile.mjs.
|
|
533
|
+
readonly SCRATCH_SUPERVISOR_SHA256='6fdec1ad0441d7636dc6749c44a23167baf7b75004f156b8a7d4ce1c18c790b7'
|
|
534
|
+
readonly SCRATCH_SUPERVISOR_CLEANUP_COUNT='7'
|
|
535
|
+
|
|
536
|
+
# An authenticated public supervisor owns its fixed cleanup sites. Its original
|
|
537
|
+
# source remains visible to every other policy; its actual payload is followed.
|
|
538
|
+
follow_supervisor() {
|
|
539
|
+
local token="$1" depth="$2" statement="$3" path="" profile="" payload="" rm_source="" original=""
|
|
540
|
+
case "$token" in *lisa-scratch-run.sh) ;; *) return 1 ;; esac
|
|
541
|
+
follow_locate "$token" || follow_refuse "$token" "supervisor source could not be resolved"
|
|
542
|
+
path="$follow_located"
|
|
543
|
+
profile="$(python3 - "$path" "$SCRATCH_SUPERVISOR_SHA256" "$statement" "$token" "$SCRATCH_SUPERVISOR_CLEANUP_COUNT" <<'PY'
|
|
544
|
+
import hashlib
|
|
545
|
+
import json
|
|
546
|
+
import os
|
|
547
|
+
import re
|
|
548
|
+
import shlex
|
|
549
|
+
import stat
|
|
550
|
+
import sys
|
|
551
|
+
|
|
552
|
+
path, digest, statement, operand, expected_count = sys.argv[1:]
|
|
553
|
+
def refuse():
|
|
554
|
+
sys.exit(2)
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
info = os.lstat(path)
|
|
558
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > 262144:
|
|
559
|
+
refuse()
|
|
560
|
+
raw = open(path, 'rb').read(262145)
|
|
561
|
+
if len(raw) != info.st_size or not re.fullmatch(r'[0-9a-f]{64}', digest):
|
|
562
|
+
refuse()
|
|
563
|
+
if hashlib.sha256(raw).hexdigest() != digest:
|
|
564
|
+
refuse()
|
|
565
|
+
tokens = shlex.split(statement)
|
|
566
|
+
indices = [index for index, token in enumerate(tokens) if token == operand]
|
|
567
|
+
if len(indices) != 1 or '$' in operand or '`' in operand:
|
|
568
|
+
refuse()
|
|
569
|
+
index = indices[0]
|
|
570
|
+
if index < 1 or tokens[index - 1] not in ('sh', 'bash', '/bin/sh', '/bin/bash'):
|
|
571
|
+
refuse()
|
|
572
|
+
prefix = tokens[:index - 1]
|
|
573
|
+
if prefix[:1] == ['env']:
|
|
574
|
+
prefix = prefix[1:]
|
|
575
|
+
assignments = {}
|
|
576
|
+
for token in prefix:
|
|
577
|
+
if '=' not in token or re.search(r'[$`\r\n]', token):
|
|
578
|
+
refuse()
|
|
579
|
+
key, value = token.split('=', 1)
|
|
580
|
+
if key not in ('LISA_SCRATCH_BASE', 'LISA_SCRATCH_TRACE', 'TMPDIR') or key in assignments:
|
|
581
|
+
refuse()
|
|
582
|
+
assignments[key] = value
|
|
583
|
+
# Match actual shell :- precedence, including inherited and
|
|
584
|
+
# explicitly empty values. TMPDIR cannot override a nonempty inherited base.
|
|
585
|
+
effective = dict(os.environ, **assignments)
|
|
586
|
+
base = effective.get('LISA_SCRATCH_BASE') or effective.get('TMPDIR') or '/tmp'
|
|
587
|
+
if not base or not os.path.isabs(base) or re.search(r'[$`\r\n]', base):
|
|
588
|
+
refuse()
|
|
589
|
+
real_base = os.path.realpath(base)
|
|
590
|
+
roots = ['/tmp', '/var/tmp', os.environ.get('TMPDIR', '/tmp')]
|
|
591
|
+
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)):
|
|
592
|
+
refuse()
|
|
593
|
+
args = tokens[index + 1:]
|
|
594
|
+
if len(args) < 4 or args[0] != '--suite' or args[2] != '--':
|
|
595
|
+
refuse()
|
|
596
|
+
if not re.fullmatch(r'[A-Za-z0-9._-]{1,64}', args[1]):
|
|
597
|
+
refuse()
|
|
598
|
+
payload = args[3:]
|
|
599
|
+
if not payload[0] or payload[0].startswith('-') or any(re.search(r'[$`\x00\r\n]', value) for value in payload):
|
|
600
|
+
refuse()
|
|
601
|
+
source = raw.decode('utf8')
|
|
602
|
+
cleaned, count = re.subn(r'(?m)^(\s*)rm -rf "(\$_quarantine|\$_root|\$_entry|\$LISA_SCRATCH_ROOT)"', r'\1:', source)
|
|
603
|
+
if not re.fullmatch(r'[1-9][0-9]*', expected_count) or count != int(expected_count):
|
|
604
|
+
refuse()
|
|
605
|
+
print(json.dumps({'source': cleaned, 'original': source, 'payload': shlex.join(payload)}))
|
|
606
|
+
except (OSError, UnicodeError, ValueError):
|
|
607
|
+
refuse()
|
|
608
|
+
PY
|
|
609
|
+
)" || follow_refuse "$path" "supervisor source or public entry authority could not be authenticated"
|
|
610
|
+
rm_source="$(printf '%s' "$profile" | jq -er '.source')" || follow_refuse "$path" "invalid supervisor profile"
|
|
611
|
+
original="$(printf '%s' "$profile" | jq -er '.original')" || follow_refuse "$path" "invalid supervisor source"
|
|
612
|
+
payload="$(printf '%s' "$profile" | jq -er '.payload')" || follow_refuse "$path" "invalid supervisor payload"
|
|
613
|
+
follow_take "$path" "$depth" "$rm_source" "$original"
|
|
614
|
+
[ "$depth" -lt "$FOLLOW_MAX_DEPTH" ] || follow_refuse "$path" "supervisor payload exceeds inspection depth"
|
|
615
|
+
follow_scan "$payload" "$((depth + 1))"
|
|
616
|
+
return 0
|
|
617
|
+
}
|
|
530
618
|
|
|
531
619
|
FOLLOW_GUIDANCE="This command EXECUTES a file. These guards classify shell text, so the script an
|
|
532
620
|
invocation runs is read and scanned as part of the command — a destructive line
|
|
@@ -813,7 +901,7 @@ follow_locate() {
|
|
|
813
901
|
# executes, to a bounded depth. Full-line comments are dropped: a commented line
|
|
814
902
|
# is never executed, so scanning it can only manufacture a false refusal.
|
|
815
903
|
follow_take() {
|
|
816
|
-
local path="$1" depth="$2" size="" content=""
|
|
904
|
+
local path="$1" depth="$2" size="" content="" rm_content="${3-}"
|
|
817
905
|
case "$follow_paths" in
|
|
818
906
|
*$'\n'"$path"$'\n'*) return 0 ;;
|
|
819
907
|
esac
|
|
@@ -833,10 +921,22 @@ follow_take() {
|
|
|
833
921
|
if [ "$follow_bytes" -gt "$FOLLOW_MAX_TOTAL_BYTES" ]; then
|
|
834
922
|
follow_refuse "$path" "the files this command executes total more than the ${FOLLOW_MAX_TOTAL_BYTES}-byte inspection budget"
|
|
835
923
|
fi
|
|
836
|
-
|
|
837
|
-
|
|
924
|
+
if [ "$#" -eq 4 ]; then
|
|
925
|
+
# Scan the same complete bytes whose digest was authenticated, not a
|
|
926
|
+
# second file read that could pair changed source with the cleanup view.
|
|
927
|
+
content="$(printf '%s' "$4" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "authenticated source could not be read"
|
|
928
|
+
else
|
|
929
|
+
content="$(LC_ALL=C tr -d '\000' <"$path" | sed 's/^[[:space:]]*#.*$//')" \
|
|
930
|
+
|| follow_refuse "$path" "it could not be read"
|
|
931
|
+
fi
|
|
838
932
|
follow_names="$follow_names $path"
|
|
839
933
|
followed_text="$followed_text"$'\n'"$content"
|
|
934
|
+
if [ "$#" -lt 3 ]; then
|
|
935
|
+
rm_content="$content"
|
|
936
|
+
else
|
|
937
|
+
rm_content="$(printf '%s' "$rm_content" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "supervisor cleanup view could not be read"
|
|
938
|
+
fi
|
|
939
|
+
followed_rm_text="$followed_rm_text"$'\n'"$rm_content"
|
|
840
940
|
if [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ]; then
|
|
841
941
|
# A `cd` inside the followed script governs what THAT script goes on to
|
|
842
942
|
# execute, and nothing after it in the command that ran the script — the
|
|
@@ -855,6 +955,11 @@ follow_take() {
|
|
|
855
955
|
follow_target() {
|
|
856
956
|
local token="$1" depth="$2" path=""
|
|
857
957
|
[ -n "$token" ] || return 0
|
|
958
|
+
# Entry authority belongs to each invocation, not the file-read cache. Only
|
|
959
|
+
# follow_supervisor may admit the exact public interpreter/argv profile.
|
|
960
|
+
case "$token" in
|
|
961
|
+
*lisa-scratch-run.sh) follow_refuse "$token" "supervisor requires its authenticated public interpreter entry" ;;
|
|
962
|
+
esac
|
|
858
963
|
if ! follow_locate "$token"; then
|
|
859
964
|
if [ "$depth" -eq 0 ]; then
|
|
860
965
|
follow_refuse "$token" "no readable file could be resolved for it"
|
|
@@ -888,6 +993,9 @@ follow_direct() {
|
|
|
888
993
|
return 0
|
|
889
994
|
fi
|
|
890
995
|
path="$follow_located"
|
|
996
|
+
case "$path" in
|
|
997
|
+
*lisa-scratch-run.sh) follow_refuse "$path" "supervisor requires its authenticated public interpreter entry" ;;
|
|
998
|
+
esac
|
|
891
999
|
IFS= read -r -n 256 raw <"$path" 2>/dev/null || true
|
|
892
1000
|
while [ "$k" -lt "${#raw}" ]; do
|
|
893
1001
|
char="${raw:k:1}"
|
|
@@ -1219,7 +1327,9 @@ follow_scan() {
|
|
|
1219
1327
|
fi
|
|
1220
1328
|
follow_unwrap "${toks[j]}"
|
|
1221
1329
|
tj="$follow_unwrapped"
|
|
1222
|
-
|
|
1330
|
+
if ! follow_supervisor "$tj" "$depth" "$stmt"; then
|
|
1331
|
+
follow_target "$tj" "$depth"
|
|
1332
|
+
fi
|
|
1223
1333
|
cmd_pos=0
|
|
1224
1334
|
i=$((j + 1))
|
|
1225
1335
|
continue
|
|
@@ -1248,6 +1358,7 @@ follow_scan() {
|
|
|
1248
1358
|
}
|
|
1249
1359
|
|
|
1250
1360
|
follow_scan "$command_for_guards" 0
|
|
1361
|
+
rm_command_for_guards="$command_for_guards"$'\n'"$followed_rm_text"
|
|
1251
1362
|
if [ -n "$follow_names" ]; then
|
|
1252
1363
|
command_for_guards="$command_for_guards"$'\n'"$followed_text"
|
|
1253
1364
|
# A refusal must say WHERE the match is. Without this the operator reads
|
|
@@ -1266,6 +1377,8 @@ fi
|
|
|
1266
1377
|
# `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
|
|
1267
1378
|
normalized_command_str="$(printf '%s' "$command_for_guards" \
|
|
1268
1379
|
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
1380
|
+
rm_normalized_command_str="$(printf '%s' "$rm_command_for_guards" \
|
|
1381
|
+
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
1269
1382
|
|
|
1270
1383
|
# matches / matches_cs run an ERE against the guarded command text. matches is
|
|
1271
1384
|
# case-insensitive (the default for these guards); matches_cs is case-SENSITIVE
|
|
@@ -1570,7 +1683,7 @@ rm_assignment_value() {
|
|
|
1570
1683
|
# small lexer below preserves quoted right-hand sides such as V="/tmp/x"
|
|
1571
1684
|
# while excluding an argument such as echo "V=/tmp/x". Any ambiguity fails
|
|
1572
1685
|
# closed.
|
|
1573
|
-
RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$
|
|
1686
|
+
RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$rm_normalized_command_str" \
|
|
1574
1687
|
RM_ASSIGNMENT_STATEMENT="$rm_assignment_stmt" \
|
|
1575
1688
|
python3 - <<'PY'
|
|
1576
1689
|
import os
|
|
@@ -1769,7 +1882,7 @@ classify_rm_target() {
|
|
|
1769
1882
|
# process substitution cannot deny anything either — it ends that subshell, not
|
|
1770
1883
|
# this hook.
|
|
1771
1884
|
rm_segments_status=0
|
|
1772
|
-
rm_segments="$(printf '%s' "$
|
|
1885
|
+
rm_segments="$(printf '%s' "$rm_normalized_command_str" | tr '&|;' '\n' \
|
|
1773
1886
|
| grep -Ei -- "$RM_CMD"'([[:space:]]|$)')" || rm_segments_status=$?
|
|
1774
1887
|
case "$rm_segments_status" in
|
|
1775
1888
|
0 | 1) ;;
|
|
@@ -2281,9 +2394,10 @@ git_segments="$(printf '%s' "$normalized_command_str" \
|
|
|
2281
2394
|
-e 's/[0-9]*>&[0-9-]*/>/g' \
|
|
2282
2395
|
-e 's/[0-9]*<&[0-9-]*/</g' \
|
|
2283
2396
|
-e 's/||/;/g' \
|
|
2284
|
-
| tr '&;' '\n'
|
|
2397
|
+
| tr '&;' '\n' \
|
|
2398
|
+
| grep -E -- "$RM_RF_CLUSTER|$RM_RF_SPLIT")" || git_segments_status=$?
|
|
2285
2399
|
case "$git_segments_status" in
|
|
2286
|
-
0) ;;
|
|
2400
|
+
0 | 1) ;;
|
|
2287
2401
|
*) scan_failed "$git_segments_status" ;;
|
|
2288
2402
|
esac
|
|
2289
2403
|
while IFS= read -r git_stmt; do
|
|
@@ -527,6 +527,94 @@ follow_names=""
|
|
|
527
527
|
follow_count=0
|
|
528
528
|
follow_bytes=0
|
|
529
529
|
followed_text=""
|
|
530
|
+
followed_rm_text=""
|
|
531
|
+
# Generated from the canonical reviewed supervisor, never from a host manifest.
|
|
532
|
+
# Regenerate through scripts/generate-scratch-supervisor-profile.mjs.
|
|
533
|
+
readonly SCRATCH_SUPERVISOR_SHA256='6fdec1ad0441d7636dc6749c44a23167baf7b75004f156b8a7d4ce1c18c790b7'
|
|
534
|
+
readonly SCRATCH_SUPERVISOR_CLEANUP_COUNT='7'
|
|
535
|
+
|
|
536
|
+
# An authenticated public supervisor owns its fixed cleanup sites. Its original
|
|
537
|
+
# source remains visible to every other policy; its actual payload is followed.
|
|
538
|
+
follow_supervisor() {
|
|
539
|
+
local token="$1" depth="$2" statement="$3" path="" profile="" payload="" rm_source="" original=""
|
|
540
|
+
case "$token" in *lisa-scratch-run.sh) ;; *) return 1 ;; esac
|
|
541
|
+
follow_locate "$token" || follow_refuse "$token" "supervisor source could not be resolved"
|
|
542
|
+
path="$follow_located"
|
|
543
|
+
profile="$(python3 - "$path" "$SCRATCH_SUPERVISOR_SHA256" "$statement" "$token" "$SCRATCH_SUPERVISOR_CLEANUP_COUNT" <<'PY'
|
|
544
|
+
import hashlib
|
|
545
|
+
import json
|
|
546
|
+
import os
|
|
547
|
+
import re
|
|
548
|
+
import shlex
|
|
549
|
+
import stat
|
|
550
|
+
import sys
|
|
551
|
+
|
|
552
|
+
path, digest, statement, operand, expected_count = sys.argv[1:]
|
|
553
|
+
def refuse():
|
|
554
|
+
sys.exit(2)
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
info = os.lstat(path)
|
|
558
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > 262144:
|
|
559
|
+
refuse()
|
|
560
|
+
raw = open(path, 'rb').read(262145)
|
|
561
|
+
if len(raw) != info.st_size or not re.fullmatch(r'[0-9a-f]{64}', digest):
|
|
562
|
+
refuse()
|
|
563
|
+
if hashlib.sha256(raw).hexdigest() != digest:
|
|
564
|
+
refuse()
|
|
565
|
+
tokens = shlex.split(statement)
|
|
566
|
+
indices = [index for index, token in enumerate(tokens) if token == operand]
|
|
567
|
+
if len(indices) != 1 or '$' in operand or '`' in operand:
|
|
568
|
+
refuse()
|
|
569
|
+
index = indices[0]
|
|
570
|
+
if index < 1 or tokens[index - 1] not in ('sh', 'bash', '/bin/sh', '/bin/bash'):
|
|
571
|
+
refuse()
|
|
572
|
+
prefix = tokens[:index - 1]
|
|
573
|
+
if prefix[:1] == ['env']:
|
|
574
|
+
prefix = prefix[1:]
|
|
575
|
+
assignments = {}
|
|
576
|
+
for token in prefix:
|
|
577
|
+
if '=' not in token or re.search(r'[$`\r\n]', token):
|
|
578
|
+
refuse()
|
|
579
|
+
key, value = token.split('=', 1)
|
|
580
|
+
if key not in ('LISA_SCRATCH_BASE', 'LISA_SCRATCH_TRACE', 'TMPDIR') or key in assignments:
|
|
581
|
+
refuse()
|
|
582
|
+
assignments[key] = value
|
|
583
|
+
# Match actual shell :- precedence, including inherited and
|
|
584
|
+
# explicitly empty values. TMPDIR cannot override a nonempty inherited base.
|
|
585
|
+
effective = dict(os.environ, **assignments)
|
|
586
|
+
base = effective.get('LISA_SCRATCH_BASE') or effective.get('TMPDIR') or '/tmp'
|
|
587
|
+
if not base or not os.path.isabs(base) or re.search(r'[$`\r\n]', base):
|
|
588
|
+
refuse()
|
|
589
|
+
real_base = os.path.realpath(base)
|
|
590
|
+
roots = ['/tmp', '/var/tmp', os.environ.get('TMPDIR', '/tmp')]
|
|
591
|
+
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)):
|
|
592
|
+
refuse()
|
|
593
|
+
args = tokens[index + 1:]
|
|
594
|
+
if len(args) < 4 or args[0] != '--suite' or args[2] != '--':
|
|
595
|
+
refuse()
|
|
596
|
+
if not re.fullmatch(r'[A-Za-z0-9._-]{1,64}', args[1]):
|
|
597
|
+
refuse()
|
|
598
|
+
payload = args[3:]
|
|
599
|
+
if not payload[0] or payload[0].startswith('-') or any(re.search(r'[$`\x00\r\n]', value) for value in payload):
|
|
600
|
+
refuse()
|
|
601
|
+
source = raw.decode('utf8')
|
|
602
|
+
cleaned, count = re.subn(r'(?m)^(\s*)rm -rf "(\$_quarantine|\$_root|\$_entry|\$LISA_SCRATCH_ROOT)"', r'\1:', source)
|
|
603
|
+
if not re.fullmatch(r'[1-9][0-9]*', expected_count) or count != int(expected_count):
|
|
604
|
+
refuse()
|
|
605
|
+
print(json.dumps({'source': cleaned, 'original': source, 'payload': shlex.join(payload)}))
|
|
606
|
+
except (OSError, UnicodeError, ValueError):
|
|
607
|
+
refuse()
|
|
608
|
+
PY
|
|
609
|
+
)" || follow_refuse "$path" "supervisor source or public entry authority could not be authenticated"
|
|
610
|
+
rm_source="$(printf '%s' "$profile" | jq -er '.source')" || follow_refuse "$path" "invalid supervisor profile"
|
|
611
|
+
original="$(printf '%s' "$profile" | jq -er '.original')" || follow_refuse "$path" "invalid supervisor source"
|
|
612
|
+
payload="$(printf '%s' "$profile" | jq -er '.payload')" || follow_refuse "$path" "invalid supervisor payload"
|
|
613
|
+
follow_take "$path" "$depth" "$rm_source" "$original"
|
|
614
|
+
[ "$depth" -lt "$FOLLOW_MAX_DEPTH" ] || follow_refuse "$path" "supervisor payload exceeds inspection depth"
|
|
615
|
+
follow_scan "$payload" "$((depth + 1))"
|
|
616
|
+
return 0
|
|
617
|
+
}
|
|
530
618
|
|
|
531
619
|
FOLLOW_GUIDANCE="This command EXECUTES a file. These guards classify shell text, so the script an
|
|
532
620
|
invocation runs is read and scanned as part of the command — a destructive line
|
|
@@ -813,7 +901,7 @@ follow_locate() {
|
|
|
813
901
|
# executes, to a bounded depth. Full-line comments are dropped: a commented line
|
|
814
902
|
# is never executed, so scanning it can only manufacture a false refusal.
|
|
815
903
|
follow_take() {
|
|
816
|
-
local path="$1" depth="$2" size="" content=""
|
|
904
|
+
local path="$1" depth="$2" size="" content="" rm_content="${3-}"
|
|
817
905
|
case "$follow_paths" in
|
|
818
906
|
*$'\n'"$path"$'\n'*) return 0 ;;
|
|
819
907
|
esac
|
|
@@ -833,10 +921,22 @@ follow_take() {
|
|
|
833
921
|
if [ "$follow_bytes" -gt "$FOLLOW_MAX_TOTAL_BYTES" ]; then
|
|
834
922
|
follow_refuse "$path" "the files this command executes total more than the ${FOLLOW_MAX_TOTAL_BYTES}-byte inspection budget"
|
|
835
923
|
fi
|
|
836
|
-
|
|
837
|
-
|
|
924
|
+
if [ "$#" -eq 4 ]; then
|
|
925
|
+
# Scan the same complete bytes whose digest was authenticated, not a
|
|
926
|
+
# second file read that could pair changed source with the cleanup view.
|
|
927
|
+
content="$(printf '%s' "$4" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "authenticated source could not be read"
|
|
928
|
+
else
|
|
929
|
+
content="$(LC_ALL=C tr -d '\000' <"$path" | sed 's/^[[:space:]]*#.*$//')" \
|
|
930
|
+
|| follow_refuse "$path" "it could not be read"
|
|
931
|
+
fi
|
|
838
932
|
follow_names="$follow_names $path"
|
|
839
933
|
followed_text="$followed_text"$'\n'"$content"
|
|
934
|
+
if [ "$#" -lt 3 ]; then
|
|
935
|
+
rm_content="$content"
|
|
936
|
+
else
|
|
937
|
+
rm_content="$(printf '%s' "$rm_content" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "supervisor cleanup view could not be read"
|
|
938
|
+
fi
|
|
939
|
+
followed_rm_text="$followed_rm_text"$'\n'"$rm_content"
|
|
840
940
|
if [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ]; then
|
|
841
941
|
# A `cd` inside the followed script governs what THAT script goes on to
|
|
842
942
|
# execute, and nothing after it in the command that ran the script — the
|
|
@@ -855,6 +955,11 @@ follow_take() {
|
|
|
855
955
|
follow_target() {
|
|
856
956
|
local token="$1" depth="$2" path=""
|
|
857
957
|
[ -n "$token" ] || return 0
|
|
958
|
+
# Entry authority belongs to each invocation, not the file-read cache. Only
|
|
959
|
+
# follow_supervisor may admit the exact public interpreter/argv profile.
|
|
960
|
+
case "$token" in
|
|
961
|
+
*lisa-scratch-run.sh) follow_refuse "$token" "supervisor requires its authenticated public interpreter entry" ;;
|
|
962
|
+
esac
|
|
858
963
|
if ! follow_locate "$token"; then
|
|
859
964
|
if [ "$depth" -eq 0 ]; then
|
|
860
965
|
follow_refuse "$token" "no readable file could be resolved for it"
|
|
@@ -888,6 +993,9 @@ follow_direct() {
|
|
|
888
993
|
return 0
|
|
889
994
|
fi
|
|
890
995
|
path="$follow_located"
|
|
996
|
+
case "$path" in
|
|
997
|
+
*lisa-scratch-run.sh) follow_refuse "$path" "supervisor requires its authenticated public interpreter entry" ;;
|
|
998
|
+
esac
|
|
891
999
|
IFS= read -r -n 256 raw <"$path" 2>/dev/null || true
|
|
892
1000
|
while [ "$k" -lt "${#raw}" ]; do
|
|
893
1001
|
char="${raw:k:1}"
|
|
@@ -1219,7 +1327,9 @@ follow_scan() {
|
|
|
1219
1327
|
fi
|
|
1220
1328
|
follow_unwrap "${toks[j]}"
|
|
1221
1329
|
tj="$follow_unwrapped"
|
|
1222
|
-
|
|
1330
|
+
if ! follow_supervisor "$tj" "$depth" "$stmt"; then
|
|
1331
|
+
follow_target "$tj" "$depth"
|
|
1332
|
+
fi
|
|
1223
1333
|
cmd_pos=0
|
|
1224
1334
|
i=$((j + 1))
|
|
1225
1335
|
continue
|
|
@@ -1248,6 +1358,7 @@ follow_scan() {
|
|
|
1248
1358
|
}
|
|
1249
1359
|
|
|
1250
1360
|
follow_scan "$command_for_guards" 0
|
|
1361
|
+
rm_command_for_guards="$command_for_guards"$'\n'"$followed_rm_text"
|
|
1251
1362
|
if [ -n "$follow_names" ]; then
|
|
1252
1363
|
command_for_guards="$command_for_guards"$'\n'"$followed_text"
|
|
1253
1364
|
# A refusal must say WHERE the match is. Without this the operator reads
|
|
@@ -1266,6 +1377,8 @@ fi
|
|
|
1266
1377
|
# `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
|
|
1267
1378
|
normalized_command_str="$(printf '%s' "$command_for_guards" \
|
|
1268
1379
|
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
1380
|
+
rm_normalized_command_str="$(printf '%s' "$rm_command_for_guards" \
|
|
1381
|
+
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
1269
1382
|
|
|
1270
1383
|
# matches / matches_cs run an ERE against the guarded command text. matches is
|
|
1271
1384
|
# case-insensitive (the default for these guards); matches_cs is case-SENSITIVE
|
|
@@ -1570,7 +1683,7 @@ rm_assignment_value() {
|
|
|
1570
1683
|
# small lexer below preserves quoted right-hand sides such as V="/tmp/x"
|
|
1571
1684
|
# while excluding an argument such as echo "V=/tmp/x". Any ambiguity fails
|
|
1572
1685
|
# closed.
|
|
1573
|
-
RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$
|
|
1686
|
+
RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$rm_normalized_command_str" \
|
|
1574
1687
|
RM_ASSIGNMENT_STATEMENT="$rm_assignment_stmt" \
|
|
1575
1688
|
python3 - <<'PY'
|
|
1576
1689
|
import os
|
|
@@ -1769,7 +1882,7 @@ classify_rm_target() {
|
|
|
1769
1882
|
# process substitution cannot deny anything either — it ends that subshell, not
|
|
1770
1883
|
# this hook.
|
|
1771
1884
|
rm_segments_status=0
|
|
1772
|
-
rm_segments="$(printf '%s' "$
|
|
1885
|
+
rm_segments="$(printf '%s' "$rm_normalized_command_str" | tr '&|;' '\n' \
|
|
1773
1886
|
| grep -Ei -- "$RM_CMD"'([[:space:]]|$)')" || rm_segments_status=$?
|
|
1774
1887
|
case "$rm_segments_status" in
|
|
1775
1888
|
0 | 1) ;;
|
|
@@ -2281,9 +2394,10 @@ git_segments="$(printf '%s' "$normalized_command_str" \
|
|
|
2281
2394
|
-e 's/[0-9]*>&[0-9-]*/>/g' \
|
|
2282
2395
|
-e 's/[0-9]*<&[0-9-]*/</g' \
|
|
2283
2396
|
-e 's/||/;/g' \
|
|
2284
|
-
| tr '&;' '\n'
|
|
2397
|
+
| tr '&;' '\n' \
|
|
2398
|
+
| grep -E -- "$RM_RF_CLUSTER|$RM_RF_SPLIT")" || git_segments_status=$?
|
|
2285
2399
|
case "$git_segments_status" in
|
|
2286
|
-
0) ;;
|
|
2400
|
+
0 | 1) ;;
|
|
2287
2401
|
*) scan_failed "$git_segments_status" ;;
|
|
2288
2402
|
esac
|
|
2289
2403
|
while IFS= read -r git_stmt; do
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.71.
|
|
3
|
+
"version": "4.71.4",
|
|
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
|
+
"version": "4.71.4",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|