@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
|
@@ -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, 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, 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
|
-
|
|
834
|
-
|
|
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
|
-
|
|
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="$
|
|
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' "$
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
750
|
-
#
|
|
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
|
-
#
|
|
758
|
-
#
|
|
759
|
-
|
|
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="$
|
|
766
|
-
|
|
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
|
|
package/scripts/build-plugins.sh
CHANGED
|
@@ -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
|
+
}
|