685 lines
38 KiB
Diff
685 lines
38 KiB
Diff
--- a/scripts/case2-unit.sh
|
|
+++ b/scripts/case2-unit.sh
|
|
@@ -19 +19,4 @@
|
|
-# dependencies. It renders the default instance only.
|
|
+# dependencies. The inner query supervisor runs after the uid drop: 5 s inside,
|
|
+# 7 s on root's wait. Fixed system Bash, setsid, sleep, timeout and head are
|
|
+# resolved and root-validated. Only the config query uses this supervisor.
|
|
+# It renders the default instance only.
|
|
@@ -153,0 +157,113 @@
|
|
+# row253-supervisor-begin
|
|
+IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
|
|
+set +m
|
|
+seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
|
|
+child="" timer="" verified=0
|
|
+stat_fields() {
|
|
+ local pid="$1" raw rest
|
|
+ if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
|
|
+ [ -e "/proc/$pid/stat" ] && return 1
|
|
+ return 3
|
|
+ fi
|
|
+ [ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
|
|
+ rest="${raw##*) }"
|
|
+ [ "$rest" != "$raw" ] || return 1
|
|
+ read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
|
|
+ [[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
|
|
+}
|
|
+clock_cs() {
|
|
+ local boot spare
|
|
+ read -r boot spare < /proc/uptime || return 1
|
|
+ [[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
|
|
+ CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
|
|
+}
|
|
+clean_session() {
|
|
+ local entry pid rc live until
|
|
+ clock_cs || return 1
|
|
+ until=$((CLOCK_CS + 25))
|
|
+ while :; do
|
|
+ live=0
|
|
+ for entry in /proc/[0-9]*/stat; do
|
|
+ pid="${entry%/stat}"; pid="${pid##*/}"
|
|
+ rc=0; stat_fields "$pid" || rc=$?
|
|
+ [ "$rc" != 3 ] || continue
|
|
+ [ "$rc" = 0 ] || return 1
|
|
+ [ "$ST_SESSION" = "$child" ] || continue
|
|
+ case "$ST_STATE" in Z|X) continue ;; esac
|
|
+ live=1
|
|
+ if ! kill -KILL "$pid" 2>/dev/null; then
|
|
+ rc=0; stat_fields "$pid" || rc=$?
|
|
+ [ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
|
|
+ fi
|
|
+ done
|
|
+ [ "$live" != 0 ] || return 0
|
|
+ clock_cs || return 1
|
|
+ [ "$CLOCK_CS" -lt "$until" ] || return 1
|
|
+ "$sleep_bin" 0.01
|
|
+ done
|
|
+}
|
|
+finish() {
|
|
+ local rc="$?"
|
|
+ trap - EXIT HUP INT TERM
|
|
+ if [ "$verified" = 1 ]; then
|
|
+ clean_session || rc=86
|
|
+ elif [ -n "$child" ]; then
|
|
+ kill -KILL "$child" 2>/dev/null || :
|
|
+ fi
|
|
+ [ -z "$child" ] || wait "$child" 2>/dev/null || :
|
|
+ if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
|
|
+ exit "$rc"
|
|
+}
|
|
+trap finish EXIT
|
|
+trap 'exit 88' HUP INT TERM
|
|
+"$sleep_bin" "$seconds" & timer=$!
|
|
+"$setsid_bin" "$@" & child=$!
|
|
+for ((attempt=0; attempt<100; attempt++)); do
|
|
+ rc=0; stat_fields "$child" || rc=$?
|
|
+ [ "$rc" = 0 ] || exit 85
|
|
+ if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
|
|
+ kill -0 "$timer" 2>/dev/null || exit 81
|
|
+ "$sleep_bin" 0.002
|
|
+done
|
|
+[ "$verified" = 1 ] || exit 85
|
|
+while kill -0 "$child" 2>/dev/null; do
|
|
+ kill -0 "$timer" 2>/dev/null || exit 81
|
|
+ "$sleep_bin" 0.01
|
|
+done
|
|
+kill -0 "$timer" 2>/dev/null || exit 81
|
|
+rc=0; wait "$child" || rc=$?
|
|
+[ "$rc" = 0 ] || exit 87
|
|
+exit 0
|
|
+MOSAIC_TESTER_SUPERVISOR
|
|
+# row253-supervisor-end
|
|
+
|
|
+tester_tools() {
|
|
+ local input target p st magic
|
|
+ TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
|
|
+ for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
|
|
+ target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
|
|
+ case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
|
|
+ [ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
|
|
+ p="$target"
|
|
+ while :; do
|
|
+ st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
|
|
+ [ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
|
|
+ [ "$p" != / ] || break
|
|
+ p="${p%/*}"; [ -n "$p" ] || p=/
|
|
+ done
|
|
+ magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
|
|
+ [ "$magic" = 7f454c46 ] || return 1
|
|
+ case "$input" in
|
|
+ /bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
|
|
+ /usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
|
|
+ /usr/bin/head) TS_HEAD="$target" ;;
|
|
+ esac
|
|
+ done
|
|
+}
|
|
+tester_capture_reason() {
|
|
+ case "$1" in
|
|
+ 81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
|
|
+ 84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
|
|
+ *) echo inner-command ;;
|
|
+ esac
|
|
+}
|
|
@@ -155,5 +271,11 @@
|
|
- local seconds="$1" maximum="$2" file child="" rc=0; shift 2
|
|
- file="$(mktemp)" || exit 1
|
|
- trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
|
|
- trap 'exit 1' HUP INT TERM
|
|
- timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
|
|
+ local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
|
|
+ tester_tools || exit 84
|
|
+ file="$(mktemp)" || exit 88
|
|
+ notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
|
|
+ trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
|
|
+ trap 'exit 88' HUP INT TERM
|
|
+ LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
|
|
+ set -o pipefail
|
|
+ maximum="$1"; head_bin="$2"; shift 2
|
|
+ "$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
|
|
+ ' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
|
|
@@ -163 +285,4 @@
|
|
- [ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
|
|
+ # Only GNU timeout and the trusted head can write this root-side channel.
|
|
+ if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
|
|
+ [ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
|
|
+ [ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
|
|
@@ -165,0 +291 @@
|
|
+
|
|
@@ -259 +385 @@
|
|
- query="$(tester_capture 5 4096 sudo -u "$USER_NAME" -H env -i "HOME=$HOME_DIR" "PATH=$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query failed"
|
|
+ query="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query $(tester_capture_reason "$?")"
|
|
--- a/scripts/case1-accept.sh
|
|
+++ b/scripts/case1-accept.sh
|
|
@@ -18,0 +19,4 @@
|
|
+# The config/probe supervisor is fixed root-validated system Bash plus setsid
|
|
+# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
|
|
+# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
|
|
+# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
|
|
@@ -110,0 +115,113 @@
|
|
+# row253-supervisor-begin
|
|
+IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
|
|
+set +m
|
|
+seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
|
|
+child="" timer="" verified=0
|
|
+stat_fields() {
|
|
+ local pid="$1" raw rest
|
|
+ if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
|
|
+ [ -e "/proc/$pid/stat" ] && return 1
|
|
+ return 3
|
|
+ fi
|
|
+ [ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
|
|
+ rest="${raw##*) }"
|
|
+ [ "$rest" != "$raw" ] || return 1
|
|
+ read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
|
|
+ [[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
|
|
+}
|
|
+clock_cs() {
|
|
+ local boot spare
|
|
+ read -r boot spare < /proc/uptime || return 1
|
|
+ [[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
|
|
+ CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
|
|
+}
|
|
+clean_session() {
|
|
+ local entry pid rc live until
|
|
+ clock_cs || return 1
|
|
+ until=$((CLOCK_CS + 25))
|
|
+ while :; do
|
|
+ live=0
|
|
+ for entry in /proc/[0-9]*/stat; do
|
|
+ pid="${entry%/stat}"; pid="${pid##*/}"
|
|
+ rc=0; stat_fields "$pid" || rc=$?
|
|
+ [ "$rc" != 3 ] || continue
|
|
+ [ "$rc" = 0 ] || return 1
|
|
+ [ "$ST_SESSION" = "$child" ] || continue
|
|
+ case "$ST_STATE" in Z|X) continue ;; esac
|
|
+ live=1
|
|
+ if ! kill -KILL "$pid" 2>/dev/null; then
|
|
+ rc=0; stat_fields "$pid" || rc=$?
|
|
+ [ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
|
|
+ fi
|
|
+ done
|
|
+ [ "$live" != 0 ] || return 0
|
|
+ clock_cs || return 1
|
|
+ [ "$CLOCK_CS" -lt "$until" ] || return 1
|
|
+ "$sleep_bin" 0.01
|
|
+ done
|
|
+}
|
|
+finish() {
|
|
+ local rc="$?"
|
|
+ trap - EXIT HUP INT TERM
|
|
+ if [ "$verified" = 1 ]; then
|
|
+ clean_session || rc=86
|
|
+ elif [ -n "$child" ]; then
|
|
+ kill -KILL "$child" 2>/dev/null || :
|
|
+ fi
|
|
+ [ -z "$child" ] || wait "$child" 2>/dev/null || :
|
|
+ if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
|
|
+ exit "$rc"
|
|
+}
|
|
+trap finish EXIT
|
|
+trap 'exit 88' HUP INT TERM
|
|
+"$sleep_bin" "$seconds" & timer=$!
|
|
+"$setsid_bin" "$@" & child=$!
|
|
+for ((attempt=0; attempt<100; attempt++)); do
|
|
+ rc=0; stat_fields "$child" || rc=$?
|
|
+ [ "$rc" = 0 ] || exit 85
|
|
+ if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
|
|
+ kill -0 "$timer" 2>/dev/null || exit 81
|
|
+ "$sleep_bin" 0.002
|
|
+done
|
|
+[ "$verified" = 1 ] || exit 85
|
|
+while kill -0 "$child" 2>/dev/null; do
|
|
+ kill -0 "$timer" 2>/dev/null || exit 81
|
|
+ "$sleep_bin" 0.01
|
|
+done
|
|
+kill -0 "$timer" 2>/dev/null || exit 81
|
|
+rc=0; wait "$child" || rc=$?
|
|
+[ "$rc" = 0 ] || exit 87
|
|
+exit 0
|
|
+MOSAIC_TESTER_SUPERVISOR
|
|
+# row253-supervisor-end
|
|
+
|
|
+tester_tools() {
|
|
+ local input target p st magic
|
|
+ TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
|
|
+ for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
|
|
+ target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
|
|
+ case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
|
|
+ [ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
|
|
+ p="$target"
|
|
+ while :; do
|
|
+ st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
|
|
+ [ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
|
|
+ [ "$p" != / ] || break
|
|
+ p="${p%/*}"; [ -n "$p" ] || p=/
|
|
+ done
|
|
+ magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
|
|
+ [ "$magic" = 7f454c46 ] || return 1
|
|
+ case "$input" in
|
|
+ /bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
|
|
+ /usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
|
|
+ /usr/bin/head) TS_HEAD="$target" ;;
|
|
+ esac
|
|
+ done
|
|
+}
|
|
+tester_capture_reason() {
|
|
+ case "$1" in
|
|
+ 81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
|
|
+ 84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
|
|
+ *) echo inner-command ;;
|
|
+ esac
|
|
+}
|
|
@@ -112,5 +229,11 @@
|
|
- local seconds="$1" maximum="$2" file child="" rc=0; shift 2
|
|
- file="$(mktemp)" || exit 1
|
|
- trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
|
|
- trap 'exit 1' HUP INT TERM
|
|
- timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
|
|
+ local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
|
|
+ tester_tools || exit 84
|
|
+ file="$(mktemp)" || exit 88
|
|
+ notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
|
|
+ trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
|
|
+ trap 'exit 88' HUP INT TERM
|
|
+ LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
|
|
+ set -o pipefail
|
|
+ maximum="$1"; head_bin="$2"; shift 2
|
|
+ "$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
|
|
+ ' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
|
|
@@ -120 +243,4 @@
|
|
- [ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
|
|
+ # Only GNU timeout and the trusted head can write this root-side channel.
|
|
+ if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
|
|
+ [ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
|
|
+ [ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
|
|
@@ -122,0 +249 @@
|
|
+
|
|
@@ -196 +323 @@
|
|
-QUERY="$(tester_capture 5 4096 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query failed"
|
|
+QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")"
|
|
@@ -536 +663 @@
|
|
- peer="$(tester_capture 3 128 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why=probe
|
|
+ peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")"
|
|
@@ -540 +667 @@
|
|
- else why=probe; fi
|
|
+ else [ -n "$why" ] || why=probe; fi
|
|
--- a/packages/cli/tests/case-sudo-fixture.mjs
|
|
+++ b/packages/cli/tests/case-sudo-fixture.mjs
|
|
@@ -3,0 +4,3 @@
|
|
+import { spawn } from 'node:child_process';
|
|
+import { readFileSync, readdirSync } from 'node:fs';
|
|
+
|
|
@@ -4,0 +8,13 @@
|
|
+case_sudo_entry() {
|
|
+ local record="$1" here sid; shift
|
|
+ here="$BASHPID"
|
|
+ read -r sid < <(/bin/ps -o sid= -p "$here")
|
|
+ printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
|
|
+ if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
|
|
+ { [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
|
|
+ trap '' TERM HUP
|
|
+ [ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
|
|
+ exec /bin/sleep 60
|
|
+ fi
|
|
+ exec "$@"
|
|
+}
|
|
@@ -6,2 +22,3 @@
|
|
- local record="$1" child="" rc=0; shift
|
|
- if [ "$(cat "$record/session-mode" 2>/dev/null)" != hop ]; then exec "$@"; fi
|
|
+ local record="$1" child="" rc=0 mode=exec; shift
|
|
+ [ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
|
|
+ if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
|
|
@@ -13,7 +30,2 @@
|
|
- /usr/bin/setsid /bin/bash -c '
|
|
- record="$1"; shift
|
|
- here="$BASHPID"
|
|
- read -r pid sid < <(/bin/ps -o pid=,sid= -p "$here")
|
|
- printf "%s %s\n" "$pid" "$sid" >> "$record/session-pids"
|
|
- exec "$@"
|
|
- ' -- "$record" "$@" &
|
|
+ export -f case_sudo_entry
|
|
+ /usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
|
|
@@ -31 +43,3 @@
|
|
- const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>Number(line.trim().split(/\s+/)[1])));
|
|
+ // Exec fake entries share the test runner's session. Only self-led sessions
|
|
+ // are isolated and safe to scan here; exec coverage also checks exact PIDs.
|
|
+ const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
|
|
@@ -41,0 +56,87 @@
|
|
+
|
|
+export function recordedPids(file) {
|
|
+ try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
|
|
+ catch(error){if(error.code==='ENOENT')return [];throw error;}
|
|
+}
|
|
+
|
|
+export function recordedLivePids(file) {
|
|
+ return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
|
|
+ try{
|
|
+ const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
|
|
+ return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
|
|
+ }catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
|
|
+ }).map(([pid])=>pid);
|
|
+}
|
|
+
|
|
+export function killRecordedPids(file) {
|
|
+ killPairs(recordedPids(file));
|
|
+}
|
|
+
|
|
+function killPairs(pairs) {
|
|
+ for(const [pid,sid] of sessionPairs(pairs)){
|
|
+ try{
|
|
+ const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
|
|
+ if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
|
|
+ }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
|
|
+ }
|
|
+}
|
|
+
|
|
+function sessionPairs(pairs) {
|
|
+ // Only self-led records authorize a session scan. Exec-mode records belong
|
|
+ // to the runner's session and authorize checking their exact PIDs only.
|
|
+ const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
|
|
+ const found=new Map(pairs.map(pair=>[pair[0],pair]));
|
|
+ if(sessions.size)for(const entry of readdirSync('/proc')){
|
|
+ if(!/^\d+$/.test(entry))continue;
|
|
+ try{
|
|
+ const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
|
|
+ if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
|
|
+ }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
|
|
+ }
|
|
+ return [...found.values()];
|
|
+}
|
|
+
|
|
+// Safety net for a removed-outer-bound change. It is fixture cleanup, never
|
|
+// product supervision. The guard reads only fixture records and checks sid
|
|
+// immediately before every kill. Normal outer-bound cases return before it.
|
|
+export function fixtureGuard(t,files,milliseconds=9500) {
|
|
+ const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
|
|
+ const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
|
|
+ const stop=async()=>{
|
|
+ if(child.exitCode!==null||child.signalCode!==null)return;
|
|
+ const exited=new Promise(resolve=>child.once('exit',resolve));
|
|
+ child.kill('SIGKILL');await exited;
|
|
+ };
|
|
+ t.after(stop);return stop;
|
|
+}
|
|
+
|
|
+export function cleanupFixture(t,files) {
|
|
+ let captured=[];
|
|
+ const capture=()=>{captured=files.flatMap(recordedPids);};
|
|
+ // tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
|
|
+ // memory before assertions, then verify the kernel sid again before killing.
|
|
+ t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
|
|
+ return capture;
|
|
+}
|
|
+
|
|
+export async function pollFixtureGone(files) {
|
|
+ for(let n=0;n<50;n++){
|
|
+ if(files.every(file=>recordedLivePids(file).length===0))return [];
|
|
+ await new Promise(resolve=>setTimeout(resolve,10));
|
|
+ }
|
|
+ return files.flatMap(recordedLivePids);
|
|
+}
|
|
+
|
|
+export function blockingQuerySource(records,kind='signals',member,proof) {
|
|
+ const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
|
|
+ return `import fs from 'node:fs';import {spawn} from 'node:child_process';
|
|
+const records=${JSON.stringify(records)};
|
|
+function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
|
|
+record(process.pid);
|
|
+export function loadSystem(){
|
|
+${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
|
|
+${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
|
|
+${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
|
|
+Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
|
|
+}`;
|
|
+}
|
|
--- a/packages/cli/tests/case2-unit.test.mjs
|
|
+++ b/packages/cli/tests/case2-unit.test.mjs
|
|
@@ -16 +16 @@
|
|
-import { CASE_SUDO_SESSION, liveRecordedSessions } from "./case-sudo-fixture.mjs";
|
|
+import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
|
|
@@ -96 +96 @@
|
|
- (trap '' TERM; exec sleep 60) </dev/null >/dev/null 2>&1 &
|
|
+ (trap '' TERM; here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here"); printf '%s %s\\n' "$here" "$sid" >'${root}/query-owned'; exec sleep 60) </dev/null >/dev/null 2>&1 &
|
|
@@ -189,2 +189,4 @@
|
|
-// Draft counterexample for DESIGN-C4. Written after sage-2307, not run.
|
|
-test('row253 C4 query cleanup handles both exec and sudo session-hop fakes', async t=>{
|
|
+// C4 cases run through both sudo process models. Exec shares the runner's
|
|
+// session, so its session-wide check would be vacuous; exact recorded PIDs
|
|
+// are checked in both modes. Hop additionally isolates the fake session.
|
|
+for(const kind of ['sleep','signals','member']) test(`row253 C4 ${kind} query has exact inner reason and cleans all members`,async t=>{
|
|
@@ -192 +194 @@
|
|
- const h=hub(t),pids=join(h.root,'session-pids'),queryPids=join(h.root,'query-pids'),copy=join(copyDir(h),'packages/cli/src/config.mjs');
|
|
+ const h=hub(t),sessions=join(h.root,'session-pids'),queries=join(h.root,'query-pids'),files=[sessions,queries];
|
|
@@ -194,21 +196,10 @@
|
|
- writeFileSync(pids,'');
|
|
- writeFileSync(copy,`import fs from 'node:fs';process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});fs.appendFileSync(${JSON.stringify(queryPids)},process.pid+'\\n');const s=fs.readFileSync('/proc/self/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);if(sid===process.pid)fs.appendFileSync(${JSON.stringify(pids)},process.pid+' '+sid+'\\n');export function loadSystem(){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);}`);
|
|
- t.after(()=>{
|
|
- if(!existsSync(pids))return;
|
|
- // Explicit fixture cleanup is safe only for these recorded fixture PIDs.
|
|
- for(const line of [read(pids),existsSync(queryPids)?read(queryPids):''].join('\n').trim().split('\n')){
|
|
- const pid=Number(line.trim().split(/\s+/)[0]);
|
|
- if(pid>0)try{process.kill(pid,'SIGKILL');}catch{}
|
|
- }
|
|
- });
|
|
- const r=unit(h,{profile:'factory'});refuses(r,/query/);
|
|
- for(let n=0;n<50;n++){
|
|
- if(!liveRecordedSessions(pids,read,()=>readdirSync('/proc')).length)break;
|
|
- await new Promise(resolve=>setTimeout(resolve,10));
|
|
- }
|
|
- assert.deepEqual(liveRecordedSessions(pids,read,()=>readdirSync('/proc')),[],mode);
|
|
- for(const pid of read(queryPids).trim().split('\n')){
|
|
- let state='gone';try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{}
|
|
- assert.ok(state==='gone'||state==='Z',`${mode}: query ${pid} still ${state}`);
|
|
- }
|
|
- assert.equal(existsSync(h.log),false,mode);
|
|
+ const member=join(h.root,'q ) '+String.fromCharCode(10)+'mem'),proof=join(h.root,'comm-proof');
|
|
+ if(kind==='member')symlinkSync('/usr/bin/sleep',member);
|
|
+ writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),blockingQuerySource(queries,kind,member,proof));
|
|
+ const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
|
|
+ const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
|
|
+ refuses(r,/^case2-unit: tester config query inner-timeout\n$/);
|
|
+ assert.ok(seconds<7,`${kind}/${mode}: ${seconds}s exceeds outer bound`);
|
|
+ assert.deepEqual(await pollFixtureGone(files),[],mode);
|
|
+ if(kind==='member')assert.ok(read(proof).includes('('+member.split('/').at(-1)+') '),'member comm with newline and closing parenthesis was observed');
|
|
+ assert.equal(existsSync(h.log),false,mode);await stopGuard();
|
|
@@ -215,0 +207,30 @@
|
|
+});
|
|
+
|
|
+test('row253 C4 overflow is output-limit in both sudo modes',async t=>{
|
|
+ for(const mode of ['exec','hop']){
|
|
+ const h=hub(t),files=[join(h.root,'session-pids')];writeFileSync(join(h.root,'session-mode'),mode);const capture=cleanupFixture(t,files);
|
|
+ writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:${JSON.stringify(h.dataRoot)}});`);
|
|
+ const r=unit(h,{profile:'factory'});capture();refuses(r,/^case2-unit: tester config query output-limit\n$/);
|
|
+ assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
|
|
+ assert.deepEqual(await pollFixtureGone(files),[]);
|
|
+ }
|
|
+});
|
|
+
|
|
+test('row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup',async t=>{
|
|
+ for(const mode of ['exec','hop']){
|
|
+ const h=hub(t),file=join(h.root,'session-pids'),files=[file];writeFileSync(join(h.root,'session-mode'),mode);
|
|
+ writeFileSync(join(h.root,'stall-config'),'');writeFileSync(join(h.root,'stall-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
|
|
+ const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
|
|
+ const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
|
|
+ refuses(r,/^case2-unit: tester config query outer-timeout\n$/);
|
|
+ assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s slack includes root preflight`);
|
|
+ assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
|
|
+ if(mode==='hop')assert.ok(recordedLivePids(file).length>0,'R3 residual exists before fixture cleanup');
|
|
+ killRecordedPids(file);assert.deepEqual(await pollFixtureGone(files),[], 'explicit fixture cleanup of R3 residual');await stopGuard();
|
|
+ }
|
|
+});
|
|
+
|
|
+test('row253 C4 inline supervisor is byte-identical in both host scripts',()=>{
|
|
+ const extract=s=>s.split('# row253-supervisor-begin\n')[1]?.split('# row253-supervisor-end')[0];
|
|
+ const a=extract(read(UNIT)),b=extract(read(join(REPO,'scripts/case1-accept.sh')));
|
|
+ assert.ok(a?.length>100);assert.equal(a,b);
|
|
@@ -222,2 +243,2 @@
|
|
- t.after(()=>{if(pid)try{process.kill(pid,'SIGKILL');}catch{}});
|
|
- const r=unit(h,{profile:'factory'});
|
|
+ const capture=cleanupFixture(t,[join(h.root,'query-owned')]);
|
|
+ const r=unit(h,{profile:'factory'});capture();
|
|
@@ -235,2 +256,2 @@
|
|
-test('row253 config parser is strict and output capture is bounded',()=>{
|
|
- const source=read(UNIT), body=source.slice(source.indexOf('tester_capture() ('),source.indexOf('tester_location() {'));
|
|
+test('row253 config parser is strict',()=>{
|
|
+ const source=read(UNIT), body=source.slice(source.indexOf('tester_facts() {'),source.indexOf('tester_location() {'));
|
|
@@ -244,2 +264,0 @@
|
|
- const r=spawnSync('/bin/bash',['-c',body+'\ntester_capture 1 32 /usr/bin/head -c 100 /dev/zero']);
|
|
- assert.notEqual(r.status,0);assert.equal(r.stdout.length,0);
|
|
--- a/packages/cli/tests/case1-accept.test.mjs
|
|
+++ b/packages/cli/tests/case1-accept.test.mjs
|
|
@@ -14 +14 @@
|
|
-import { CASE_SUDO_SESSION } from "./case-sudo-fixture.mjs";
|
|
+import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
|
|
@@ -90 +90,2 @@
|
|
-case_sudo_run "$HC" "$ROOT_DIR$cmd" "\${args[@]}"`,
|
|
+[ ! -e "$ROOT_DIR$cmd" ] || cmd="$ROOT_DIR$cmd"
|
|
+case_sudo_run "$HC" "$cmd" "\${args[@]}"`,
|
|
@@ -100 +101,5 @@
|
|
-[ -f "$HC/peer-hang" ] && { echo "$BASHPID" > "$HC/peer-pid"; exec sleep 60; }
|
|
+[ -f "$HC/peer-hang" ] && {
|
|
+ here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here")
|
|
+ echo "$here" > "$HC/peer-pid"; printf '%s %s\\n' "$here" "$sid" >> "$HC/query-pids"
|
|
+ trap '' TERM HUP; exec sleep 60
|
|
+}
|
|
@@ -303,0 +309,58 @@
|
|
+test('row253 C4 acceptance query inner reason holds under both sudo models',async t=>{
|
|
+ for(const mode of ['exec','hop']){
|
|
+ const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];
|
|
+ put(join(h.hc,'session-mode'),mode);
|
|
+ put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),blockingQuerySource(files[1]));
|
|
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
|
|
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
|
|
+ assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query inner-timeout\n');
|
|
+ assert.ok(seconds<7,`${mode}: ${seconds}s`);assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
|
|
+ assert.deepEqual(await pollFixtureGone(files),[]);await stop();
|
|
+ }
|
|
+});
|
|
+
|
|
+test('row253 C4 acceptance query outer reason discards stdout and names residual cleanup',async t=>{
|
|
+ for(const mode of ['exec','hop']){
|
|
+ const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-config'),'');
|
|
+ put(join(h.hc,'stall-output'),JSON.stringify({dataRoot:HOME+'/.mosaic-dev',socket:true}));
|
|
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
|
|
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
|
|
+ assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query outer-timeout\n');
|
|
+ assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s preflight slack`);
|
|
+ assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
|
|
+ if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'R3 session remains before fixture cleanup');
|
|
+ killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 fixture cleanup');await stop();
|
|
+ }
|
|
+});
|
|
+
|
|
+test('row253 C4 acceptance query overflow is output-limit in both models',async t=>{
|
|
+ for(const mode of ['exec','hop']){
|
|
+ const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);const capture=cleanupFixture(t,files);
|
|
+ put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:'${HOME}/.mosaic-dev'});`);
|
|
+ const r=run(h);capture();assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query output-limit\n');
|
|
+ assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);assert.deepEqual(await pollFixtureGone(files),[]);
|
|
+ }
|
|
+});
|
|
+
|
|
+for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} reason holds under both sudo models`,async t=>{
|
|
+ for(const mode of ['exec','hop']){
|
|
+ const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];put(join(h.hc,'session-mode'),mode);
|
|
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files,7000);
|
|
+ if(fault==='inner')put(join(h.hc,'peer-hang'),'');
|
|
+ if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
|
|
+ if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
|
|
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
|
|
+ assert.equal(r.status,0,r.out+r.err);
|
|
+ const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
|
|
+ assert.match(r.out,new RegExp('P10 tester-socket-squat unnamed boundary=failed reason=probe-'+reason+' '));
|
|
+ assert.doesNotMatch(r.out,/boundary=observed/);
|
|
+ if(fault==='inner')assert.ok(seconds<4,`${mode}: ${seconds}s includes functional checks`);
|
|
+ if(fault==='outer'){
|
|
+ assert.ok(seconds>=4&&seconds<6,`${mode}: ${seconds}s; 2s functional-check slack`);
|
|
+ if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'stalled supervisor is R3 residual');
|
|
+ for(const file of files)killRecordedPids(file);
|
|
+ }
|
|
+ assert.deepEqual(await pollFixtureGone(files),[],fault==='outer'?'explicit R3 cleanup':mode);await stop();
|
|
+ }
|
|
+});
|
|
+
|
|
--- a/packages/webui/README.md
|
|
+++ b/packages/webui/README.md
|
|
@@ -128 +128 @@
|
|
-need `--port`, and 7330, 7332 or 7333 as `--port` or `--tester-port` exits 4
|
|
+need `--port`, and 7330, 7332 or 7333 as the main or legacy tester port exits 4
|
|
@@ -130 +130 @@
|
|
-`--tester-port` that another installed Console unit in the unit directory
|
|
+legacy tester port that another installed Console unit in the unit directory
|
|
@@ -144 +144 @@
|
|
-`serve --sso [--send] [--tester-port N]` makes the Console an OpenID Connect
|
|
+`serve --sso [--send] [--tester-socket]` makes the Console an OpenID Connect
|
|
@@ -176 +176 @@
|
|
-- With `--tester-port`, the tester listener refuses any request carrying an
|
|
+- With `--tester-socket`, the tester listener refuses any request carrying an
|
|
--- a/docs/plans/DEFERRED.md
|
|
+++ b/docs/plans/DEFERRED.md
|
|
@@ -273,14 +272,0 @@
|
|
-- **Gate runs mount the host's pi auth file.** `compose.yaml:43` mounts
|
|
- `${PI_AUTH_FILE:-/home/jwoltje/.pi/agent/auth.json}` read-only into
|
|
- the agent container. No suite sets `PI_AUTH_FILE`, so the
|
|
- `test-task.sh` cases that call `scripts/run-task.sh`, and
|
|
- `test-release.sh` through `release.sh` `health_check` (`:41-54`), run
|
|
- Compose with the host file. Row 197's lane `cleanEnv`
|
|
- (`scripts/testing/gate-lane.mjs:109`) keeps the default too: it neither
|
|
- sets nor passes `PI_AUTH_FILE`. The mount is read-only, and the legacy
|
|
- gate already worked this way before row 197. `test-auth.sh` uses its
|
|
- own fixtures but calls `auth.sh`, not Compose. The fix: a fixture
|
|
- `PI_AUTH_FILE` for the Compose-reaching cases, set by the suites or by
|
|
- the lane's `cleanEnv`, and a test that no gate step mounts the host
|
|
- default. It needs its own row. Webby N4 on the row 197 proof driver
|
|
- (bba870ee9), traced by Herb (herb-0051). (2026-10-21)
|
|
@@ -362,0 +349,34 @@
|
|
+- **Console library routes: four library codes still map to 502.**
|
|
+ (Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
|
|
+ gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
|
|
+ `library-mapping-invalid`, `library-remote-invalid`,
|
|
+ `library-state-invalid` and `library-git` still fall through
|
|
+ `busStatus` to 502. The row that adds the Console library routes
|
|
+ decides their statuses and gives its `humanCall` for `library.sync`
|
|
+ a timeout over 130 seconds. (2026-10-21)
|
|
+- **T27 sweep test leaves two `sleep 60` children running.**
|
|
+ (Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
|
|
+ r2, comment 30168.) The sweep test spawns two detached shells,
|
|
+ `planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
|
|
+ running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
|
|
+ the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
|
|
+ only the decoy shell. Neither kill reaches the `sleep` child, so each
|
|
+ one lives for up to 60 s after the test and trips any busy check that
|
|
+ follows. The fix is to kill the process group in both `t.after`
|
|
+ hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
|
|
+ detached, so it leads its own group. `exec sleep 60` doesn't work.
|
|
+ The sweep matches the run dir in `/proc/<pid>/cmdline`
|
|
+ (`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
|
|
+ planted process would survive and the "planted process killed" assert
|
|
+ would fail. This binds the next row that touches
|
|
+ `packages/bus/tests/host-run.test.mjs`. (2026-10-21)
|
|
+- **Real-model suite cases run outside the gate.** Row 244 moved
|
|
+ `test-task.sh` fork recall (M11) and user recall (M16) behind
|
|
+ `MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
|
|
+ adapter. Before any re-pin whose tree changes `packages/harness` or the
|
|
+ session fork code (`scripts/mosaic-task.mjs` session and
|
|
+ `sessionForkFrom` handling, the pi adapter), an operator runs
|
|
+ `MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
|
|
+ credential and a real `release.sh activate`, and records both results on
|
|
+ the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
|
|
+ r1 (#1757). (2026-10-21)
|
|
@@ -427,0 +448,14 @@
|
|
+- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
|
|
+ driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
|
|
+ suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
|
|
+ `guardDocker` refuses a Compose run without one, and
|
|
+ `packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
|
|
+ the host default. The real-model cases this moved off the gate are open
|
|
+ above.
|
|
+
|
|
+## Row253 F5: legacy Case AS_USER supervision
|
|
+
|
|
+Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
|
|
+Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
|
|
+
|
|
+case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
|