Installer step 4b: vault first, then set, then a PVE login (R-1145); host key off curl's argv (review F4); harness green and a gate (R-1146); hub 0.158.0 deploy evidence; independent review + verdicts
gates / gates (push) Successful in 7m23s
gates / gates (push) Successful in 7m23s
Live on demo-hp (--rotate-recovery-only): vault time moved, new value 200, old 401. Harness 52/0; the R1145 cases fail against HEAD. Publishes only with the next installer tag.
This commit is contained in:
@@ -1,3 +1,19 @@
|
||||
## 2026-10-18 — step 4b vaults FIRST, then sets the password (R-1145); the host key off curl's argv; the harness is a gate (R-1146); not yet tagged
|
||||
|
||||
- **R-1145:** step 4b (the full install and `--rotate-recovery-only`) now runs generate → PUT to the hub vault (200 =
|
||||
stored) → chpasswd (one retry) → a PVE API login with the new value. A refused vault (a stale key included — the
|
||||
independent review's F1) changes nothing; a set that fails twice after the vault leaves root on its previous password
|
||||
and says so (its shadow line is fingerprinted before and after, never read out); a refused PVE login is said too.
|
||||
The host key cannot read the vault back (by design), so the previous value cannot be re-vaulted — the next run fixes
|
||||
that state. Live on demo-hp (`--rotate-recovery-only`): vault time moved, the new value 200, the old one 401.
|
||||
- **Review F4:** the host key rides a 0600 header file (`-H @file`), never curl's argv.
|
||||
- **R-1146:** shellcheck SC2066 fixed (a one-file loop became an `if`, same effect); GL8-F6's stale check now judges
|
||||
where a line is (the uninstall's own dnsmasq, R-300, and the gate's hint text are allowed); the harness runs as gate
|
||||
`hostinstall-harness` (`hostinstall_harness_gate.py`, fast; skips out loud on a runner without bash) with two decoys.
|
||||
Harness: 52 passed, 0 failed (was 45/2). New cases R1145-a..e run the REAL step with fake commands; red-proofed against
|
||||
HEAD (all five fail there).
|
||||
- Pushing to `main` publishes nothing — this rides the next `installer-v…` tag. SCRIPT_VERSION unchanged.
|
||||
|
||||
## 2026-10-18 — `felhom-host-install.sh --rotate-recovery-only` (R-1141); not yet tagged
|
||||
|
||||
A new flag runs step 4b ALONE: a new random root@pam password, set with chpasswd and re-vaulted to the hub with the
|
||||
|
||||
@@ -1628,6 +1628,11 @@ run_rescope_acl() {
|
||||
# --rotate-recovery (a re-set would strand the operator's saved copy).
|
||||
# Defined HERE, ahead of the argument parse (R-1141): --rotate-recovery-only is dispatched before the
|
||||
# install's mode validation and calls it. Moved unchanged; the install still calls it from Main.
|
||||
# _root_shadow_fingerprint — sha256 of root's /etc/shadow line (never the line itself): did the password change?
|
||||
# FELHOM_SHADOW_FILE is a test-harness-only override (hostinstall-mode-harness.sh R1145-*).
|
||||
_root_shadow_fingerprint() {
|
||||
grep '^root:' "${FELHOM_SHADOW_FILE:-/etc/shadow}" 2>/dev/null | sha256sum | cut -c1-64
|
||||
}
|
||||
step_break_glass() {
|
||||
log_step "4b/8 break-glass credential (root@pam console password → hub vault)"
|
||||
if [[ -z "${HOST_ID:-}" || -z "${HOST_API_KEY:-}" ]]; then
|
||||
@@ -1635,38 +1640,74 @@ step_break_glass() {
|
||||
return 0
|
||||
fi
|
||||
if $DRY_RUN; then
|
||||
log_dry "openssl rand → strong root@pam password (never logged) ; chpasswd ; PUT $HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential (Bearer host key)"
|
||||
log_dry "openssl rand → strong root@pam password (never logged) ; PUT $HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential (Bearer host key) FIRST ; then chpasswd ; then a PVE login check (R-1145)"
|
||||
_state_mark break_glass; return 0
|
||||
fi
|
||||
if _state_has break_glass && ! $ROTATE_RECOVERY; then
|
||||
log_skip " recovery credential already vaulted (use --rotate-recovery to regenerate)"
|
||||
return 0
|
||||
fi
|
||||
# R-1145 (2026-10-18): VAULT FIRST, THEN SET. Until 1.33.0 the password was set before the vault; a vault that
|
||||
# then failed left root@pam with a password nobody holds — and the console is the last door into a broken box.
|
||||
# Now: generate → PUT to the hub (200 = stored: the hub answers only after its DB write) → chpasswd (one retry)
|
||||
# → a PVE API login with the new value. The possible end states, each logged in words, never the value:
|
||||
# vault refused → nothing changed (root keeps its password, the hub its old copy) — die
|
||||
# vault ok, set ok → root and the hub agree — then the PVE login is checked
|
||||
# vault ok, set failed ×2 → the hub holds a password root does NOT have; root keeps its previous one (checked:
|
||||
# its shadow line did not change). A host key cannot read the vault back (by design),
|
||||
# so the previous value cannot be re-vaulted — die, and the next run fixes it.
|
||||
# set ok, PVE login fails → root and the hub agree on the shell; the web console refuses — die, say so.
|
||||
# Pinned by hostinstall-mode-harness.sh R1145-a..d (the step run with fake commands, call order recorded).
|
||||
# Strong password: 24 url-safe bytes (~144 bits). Kept ONLY in a local shell var, never on disk.
|
||||
local newpw
|
||||
local newpw before after code attempt set_ok=false
|
||||
newpw=$(openssl rand -base64 24 2>/dev/null | tr -d '\n' | tr '+/' '-_')
|
||||
[[ ${#newpw} -ge 24 ]] || die "failed to generate a strong recovery password"
|
||||
# Set root@pam (= the Linux root user on PVE) via chpasswd on STDIN — no argv, no log.
|
||||
if ! printf 'root:%s\n' "$newpw" | chpasswd 2>/dev/null; then
|
||||
newpw="" # scrub
|
||||
die "chpasswd failed to set the root@pam recovery password"
|
||||
fi
|
||||
# Vault to the hub over the host-key-authenticated channel; password only on stdin (-d @-).
|
||||
local code
|
||||
before=$(_root_shadow_fingerprint)
|
||||
# 1. Vault to the hub over the host-key-authenticated channel; password only on stdin (-d @-). The host key rides
|
||||
# a 0600 header FILE (`-H @file`, written by the printf builtin), never curl's argv — `ps` showed it there
|
||||
# (independent review 2026-10-18, F4).
|
||||
local hdr
|
||||
hdr=$(mktemp) || die "cannot create a temp file for the vault request; the root@pam password was NOT changed"
|
||||
chmod 600 "$hdr"
|
||||
printf 'Authorization: Bearer %s\n' "$HOST_API_KEY" > "$hdr"
|
||||
code=$(printf '{"username":"root@pam","password":"%s"}' "$newpw" \
|
||||
| curl -sS -o /dev/null -w '%{http_code}' -X PUT \
|
||||
"$HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential" \
|
||||
-H "Authorization: Bearer $HOST_API_KEY" -H 'Content-Type: application/json' -d @- 2>/dev/null)
|
||||
newpw="" # scrub the plaintext from the shell var the moment it is vaulted
|
||||
-H @"$hdr" -H 'Content-Type: application/json' -d @- 2>/dev/null)
|
||||
rm -f "$hdr"
|
||||
case "$code" in
|
||||
200) log_success " root@pam password set + vaulted to the hub (retrieve via the operator /admin path; never logged here)"
|
||||
# Drill F-8: say this OUT LOUD — the operator was live-surprised by a 401 at the PVE
|
||||
# GUI. Pointer only, never the credential.
|
||||
log_warn " NOTE: the root@pam password just CHANGED — the old one now fails at the PVE web GUI (:8006)."
|
||||
log_warn " Retrieve the new one at hub → host page (vaulted recovery credential)." ;;
|
||||
401|403) die "recovery-credential vault rejected ($code) — host key/authorization problem" ;;
|
||||
*) die "recovery-credential vault failed (HTTP $code)" ;;
|
||||
200) log_info " recovery password vaulted at the hub (stored) — now setting it on root@pam" ;;
|
||||
401|403) newpw=""; die "recovery-credential vault rejected ($code) — host key/authorization problem; the root@pam password was NOT changed" ;;
|
||||
*) newpw=""; die "recovery-credential vault failed (HTTP ${code:-000}); the root@pam password was NOT changed" ;;
|
||||
esac
|
||||
# 2. Set root@pam (= the Linux root user on PVE) via chpasswd on STDIN — no argv, no log. One retry.
|
||||
for attempt in 1 2; do
|
||||
if printf 'root:%s\n' "$newpw" | chpasswd 2>/dev/null; then set_ok=true; break; fi
|
||||
[[ $attempt -eq 1 ]] && { log_warn " chpasswd failed — retrying once"; sleep 2; }
|
||||
done
|
||||
after=$(_root_shadow_fingerprint)
|
||||
if ! $set_ok && [[ -n "$after" && "$after" != "$before" ]]; then
|
||||
set_ok=true # chpasswd reported failure but root's shadow line changed: the password IS the new one
|
||||
fi
|
||||
if ! $set_ok; then
|
||||
newpw=""
|
||||
log_error " STATE: the hub now holds a recovery password that root@pam does NOT have; root@pam keeps its previous password (its shadow line is unchanged)."
|
||||
die "chpasswd failed twice after the vault — re-run (--rotate-recovery-only on an installed box): it vaults a new password first again"
|
||||
fi
|
||||
# 3. The proof a person needs: the PVE web login takes the new value (password on stdin, never argv).
|
||||
code=$(printf '%s' "$newpw" | curl -sk -o /dev/null -w '%{http_code}' --max-time 20 \
|
||||
--data-urlencode 'username=root@pam' --data-urlencode 'password@-' \
|
||||
"https://127.0.0.1:8006/api2/json/access/ticket" 2>/dev/null)
|
||||
newpw="" # scrub the plaintext from the shell var
|
||||
if [[ "$code" != "200" ]]; then
|
||||
log_error " STATE: root@pam and the hub hold the same NEW password, but the PVE web login answered HTTP ${code:-000}."
|
||||
die "the PVE login with the new recovery password failed (HTTP ${code:-000}) — check pveproxy (:8006); the shell login uses the new password"
|
||||
fi
|
||||
log_success " root@pam password vaulted, set, and the PVE login takes it (retrieve via the operator /admin path; never logged here)"
|
||||
# Drill F-8: say this OUT LOUD — the operator was live-surprised by a 401 at the PVE
|
||||
# GUI. Pointer only, never the credential.
|
||||
log_warn " NOTE: the root@pam password just CHANGED — the old one now fails at the PVE web GUI (:8006)."
|
||||
log_warn " Retrieve the new one at hub → host page (vaulted recovery credential)."
|
||||
_state_mark break_glass
|
||||
}
|
||||
|
||||
@@ -2556,12 +2597,12 @@ align_apt_repos() {
|
||||
log_info " disabled $(basename "$f") (Enabled: no)"
|
||||
done
|
||||
# 1b. legacy .list enterprise line → commented
|
||||
for f in "$d/pve-enterprise.list"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
grep -qE '^[[:space:]]*deb[[:space:]].*enterprise\.proxmox\.com' "$f" || continue
|
||||
# (R-1146: one file, so no loop — the old one-item `for` was shellcheck SC2066; same file, same effect)
|
||||
f="$d/pve-enterprise.list"
|
||||
if [[ -f "$f" ]] && grep -qE '^[[:space:]]*deb[[:space:]].*enterprise\.proxmox\.com' "$f"; then
|
||||
sed -i 's|^[[:space:]]*\(deb[[:space:]].*enterprise\.proxmox\.com.*\)$|# \1 # felhom: no-subscription|' "$f"; changed=1
|
||||
log_info " commented enterprise line in $(basename "$f")"
|
||||
done
|
||||
fi
|
||||
# 2. add pve-no-subscription if it is not already present in any list/sources
|
||||
if ! grep -rqsE 'download\.proxmox\.com/debian/pve([[:space:]/]|$)' "$d"/*.sources "$d"/*.list 2>/dev/null; then
|
||||
cat > "$d/pve-no-subscription.sources" <<SRC
|
||||
|
||||
@@ -325,12 +325,27 @@ fi
|
||||
|
||||
# GL8-F6: the byo :53 gate REFUSES (dies) with the remediation and mutates nothing — no
|
||||
# stop/mask/kill of a :53 service anywhere in the script.
|
||||
# R-1146 (2026-10-18): the check is judged by WHERE a line is, not by a fixed window. The gate's R-300 hint grew the
|
||||
# byo `if` beyond 6 lines above the refusal, and R-300 made the UNINSTALL turn off Felhom's OWN dnsmasq on purpose.
|
||||
# Allowed: a disable inside run_uninstall (ours, R-300) or inside the gate's _hint="…" text (an instruction to the
|
||||
# owner, never executed). Any other stop/mask/disable/kill of a resolver, anywhere, still FAILS.
|
||||
gl8_f6_offenders=$(awk '
|
||||
/^[a-zA-Z_]+\(\) *\{/ { fn = $1 }
|
||||
/_hint="/ { inhint = 1 }
|
||||
!/^[[:space:]]*#/ && /(systemctl (stop|mask|disable)|kill|pkill).*(dnsmasq|:53|resolv)/ {
|
||||
if (fn != "run_uninstall()" && !inhint) print NR": "$0
|
||||
}
|
||||
inhint && /"[[:space:]]*$/ && !/_hint="[[:space:]]*$/ { inhint = 0 }
|
||||
' "$SCRIPT")
|
||||
gl8_f6_byo_before=$(awk '
|
||||
/if \[\[ "\$MODE" == "byo" \]\]; then/ { last = NR }
|
||||
/die "a resolver is already bound to :53 on this host — Felhom needs/ { print (last && NR - last <= 40) ? "yes" : "no"; exit }
|
||||
' "$SCRIPT")
|
||||
if grep -q 'a resolver is already bound to :53 on this host — Felhom needs the guest reachable' "$SCRIPT" \
|
||||
&& grep -B6 'a resolver is already bound to :53 on this host — Felhom needs' "$SCRIPT" | grep -q 'MODE" == "byo"' \
|
||||
&& ! grep -vE '^[[:space:]]*#' "$SCRIPT" | grep -E '(systemctl (stop|mask|disable)|kill|pkill).*(dnsmasq|:53|resolv)' >/dev/null; then
|
||||
&& [[ "$gl8_f6_byo_before" == "yes" ]] && [[ -z "$gl8_f6_offenders" ]]; then
|
||||
verdict PASS "GL8-F6 byo :53 gate refuses+instructs, never mutates the owner's resolver"
|
||||
else
|
||||
verdict FAIL "GL8-F6 byo :53 gate refuses+instructs, never mutates the owner's resolver"
|
||||
verdict FAIL "GL8-F6 byo :53 gate refuses+instructs, never mutates the owner's resolver" "byo-gate=${gl8_f6_byo_before:-none} offenders=${gl8_f6_offenders:-none}"
|
||||
fi
|
||||
|
||||
# GL8-F1 / R-275: the agent config and EVERY copy of it go — the agent's own config dir is purged as a
|
||||
@@ -427,6 +442,71 @@ else
|
||||
verdict FAIL "R1141-k usage documents --rotate-recovery-only" "rc=$rc"
|
||||
fi
|
||||
|
||||
# ── R-1145 (2026-10-18): step 4b is VAULT FIRST, THEN SET. The REAL function (cut out of the script) runs with fake
|
||||
# openssl/curl/chpasswd/sleep on PATH and a fake shadow file; each fake appends one word to a calls file, so the
|
||||
# ORDER is what is judged. The fake password must never appear in the step's output or any fake's argv.
|
||||
r1145_dir="$WORK/r1145"; mkdir -p "$r1145_dir/bin"
|
||||
R1145_PW="FAKEPW-r1145-AAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
cat > "$r1145_dir/bin/openssl" <<EOF
|
||||
#!/bin/bash
|
||||
echo "$R1145_PW"
|
||||
EOF
|
||||
cat > "$r1145_dir/bin/curl" <<'EOF'
|
||||
#!/bin/bash
|
||||
cat >/dev/null # the body (password) arrives on stdin and goes nowhere
|
||||
echo "argv: $*" >> "$R1145_DIR/argv"
|
||||
case "$*" in
|
||||
*recovery-credential*) echo vault >> "$R1145_DIR/calls"; printf '%s' "$FAKE_VAULT" ;;
|
||||
*access/ticket*) echo login >> "$R1145_DIR/calls"; printf '%s' "$FAKE_LOGIN" ;;
|
||||
esac
|
||||
EOF
|
||||
cat > "$r1145_dir/bin/chpasswd" <<'EOF'
|
||||
#!/bin/bash
|
||||
cat >/dev/null
|
||||
echo chpasswd >> "$R1145_DIR/calls"
|
||||
[[ "$FAKE_CHPASSWD" == ok ]] || exit 1
|
||||
echo "root:\$y\$changed:20000::::::" > "$R1145_SHADOW"
|
||||
EOF
|
||||
printf '#!/bin/bash\nexit 0\n' > "$r1145_dir/bin/sleep"
|
||||
chmod +x "$r1145_dir/bin/"*
|
||||
sed -n '/^_root_shadow_fingerprint() {/,/^}/p; /^step_break_glass() {/,/^}/p' "$SCRIPT" > "$r1145_dir/step.sh"
|
||||
# r1145_run <vault code> <chpasswd ok|fail> <login code> — sets r_rc, r_out, r_calls, r_shadow_changed
|
||||
r1145_run() {
|
||||
: > "$r1145_dir/calls"; : > "$r1145_dir/argv"; echo 'root:$y$old:20000::::::' > "$r1145_dir/shadow"
|
||||
set +e
|
||||
r_out=$(PATH="$r1145_dir/bin:$PATH" R1145_DIR="$r1145_dir" R1145_SHADOW="$r1145_dir/shadow" \
|
||||
FELHOM_SHADOW_FILE="$r1145_dir/shadow" FAKE_VAULT="$1" FAKE_CHPASSWD="$2" FAKE_LOGIN="$3" bash -c '
|
||||
log_step() { echo "STEP $*"; }; log_info() { echo "INFO $*"; }; log_warn() { echo "WARN $*"; }
|
||||
log_error() { echo "ERROR $*"; }; log_success() { echo "OK $*"; }; log_skip() { echo "SKIP $*"; }; log_dry() { echo "DRY $*"; }
|
||||
die() { echo "DIE $*"; exit 1; }; _state_mark() { echo "MARK $*"; }; _state_has() { return 1; }
|
||||
DRY_RUN=false; ROTATE_RECOVERY=true; HOST_ID=h-test; HOST_API_KEY=k-test; HUB_URL=http://127.0.0.1:9
|
||||
source "'"$r1145_dir/step.sh"'"; step_break_glass' 2>&1)
|
||||
r_rc=$?
|
||||
set -e
|
||||
r_calls=$(tr '\n' ' ' < "$r1145_dir/calls")
|
||||
grep -q 'old' "$r1145_dir/shadow" && r_shadow_changed=no || r_shadow_changed=yes
|
||||
}
|
||||
r1145_check() { # <name> <want rc 0|1> <want calls> <want shadow changed> <want output substring>
|
||||
local ok=true why=""
|
||||
[[ "$r_rc" == "$2" || ( "$2" == 1 && "$r_rc" != 0 ) ]] || { ok=false; why+="rc=$r_rc "; }
|
||||
[[ "$r_calls" == "$3" ]] || { ok=false; why+="calls='$r_calls' want '$3' "; }
|
||||
[[ "$r_shadow_changed" == "$4" ]] || { ok=false; why+="shadow changed=$r_shadow_changed "; }
|
||||
[[ "$r_out" == *"$5"* ]] || { ok=false; why+="missing '$5' "; }
|
||||
if [[ "$r_out" == *"$R1145_PW"* ]] || grep -qF "$R1145_PW" "$r1145_dir/argv"; then ok=false; why+="the password leaked "; fi
|
||||
if grep -qF "k-test" "$r1145_dir/argv" || [[ "$r_out" == *"k-test"* ]]; then ok=false; why+="the host key is in curl's argv or the output "; fi
|
||||
if $ok; then verdict PASS "$1"; else verdict FAIL "$1" "$why"; fi
|
||||
}
|
||||
r1145_run 500 ok 200
|
||||
r1145_check "R1145-a vault fails → the password is NOT touched" 1 "vault " no "the root@pam password was NOT changed"
|
||||
r1145_run 401 ok 200
|
||||
r1145_check "R1145-e a stale host key (vault 401) → the password is NOT touched (review F1)" 1 "vault " no "vault rejected (401)"
|
||||
r1145_run 200 ok 200
|
||||
r1145_check "R1145-b vault, then set, then the PVE login — in that order" 0 "vault chpasswd login " yes "the PVE login takes it"
|
||||
r1145_run 200 fail 200
|
||||
r1145_check "R1145-c set fails twice after the vault → the state is said, root unchanged" 1 "vault chpasswd chpasswd " no "STATE: the hub now holds a recovery password that root@pam does NOT have"
|
||||
r1145_run 200 ok 401
|
||||
r1145_check "R1145-d the PVE login refuses the new value → die, the state is said" 1 "vault chpasswd login " yes "the PVE web login answered HTTP 401"
|
||||
|
||||
echo ""
|
||||
echo "--- F5 appliance guest auto-sizing (VALIDATION-n100) ---"
|
||||
# The auto-size log line is emitted right after mode validation (BEFORE any PVE/hub contact), so it
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""hostinstall_harness_gate.py — runs scripts/hostinstall-mode-harness.sh as a gate (R-1146, 2026-10-18).
|
||||
|
||||
WHY. The harness (bash -n, shellcheck, the argument-refusal matrix, the mode invariants, the --rotate-recovery-only cases
|
||||
R1141-*, and the vault-first order R1145-*) was RED on `main` for weeks — SC2066 and a stale GL8-F6 check — because no
|
||||
gate ran it: `repo_gates.py` ran only `hostinstall_gates.py`. The R-29 shape. It is fast (no network: its hub is a closed
|
||||
port; no container; its PVE tier skips itself off a PVE host), so it runs in `--fast` and in the pre-push hook.
|
||||
|
||||
THE CI RUNNER HAS NO bash (python3 and git only, `.gitea/workflows/gates.yml`). There the gate says so and exits 0 — the
|
||||
same out-loud skip as check-onboarding.py's absent sibling — and the pre-push hook on DooPlex is where it bites.
|
||||
Exit 0 harness green (or no bash, said) · 1 a harness case failed.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
HARNESS = os.path.join(ROOT, "scripts", "hostinstall-mode-harness.sh")
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.isfile(HARNESS):
|
||||
print("hostinstall-harness: FAIL — %s is missing" % HARNESS)
|
||||
return 1
|
||||
bash = shutil.which("bash")
|
||||
if not bash:
|
||||
print("hostinstall-harness: NOT CHECKED here — no bash on this runner; the pre-push hook on DooPlex runs it")
|
||||
return 0
|
||||
p = subprocess.run([bash, HARNESS, os.path.join(ROOT, "scripts", "felhom-host-install.sh")], cwd=ROOT,
|
||||
capture_output=True, text=True, errors="replace")
|
||||
out = p.stdout + p.stderr
|
||||
fails = [l for l in out.splitlines() if l.startswith("FAIL ")]
|
||||
tail = [l for l in out.splitlines() if l.startswith("=== ") and "passed" in l]
|
||||
for l in fails:
|
||||
print(" " + l)
|
||||
print("hostinstall-harness: %s" % (tail[-1] if tail else "(no summary line)"))
|
||||
if p.returncode != 0 or fails or not tail:
|
||||
print("hostinstall-harness: CONVICTED (rc=%d, %d failing case(s))" % (p.returncode, len(fails)))
|
||||
return 1
|
||||
print("hostinstall-harness OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -10,6 +10,7 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
|
||||
|
||||
1. site website HTML: BOM, emoji, nav/footer, analytics, CDN, tokens, cache-busting
|
||||
2. hostinstall felhom-host-install.sh's five drill-swept invariants (+ R-94's absent-version)
|
||||
2b. hostinstall-harness scripts/hostinstall-mode-harness.sh as a gate (R-1146; skips out loud without bash)
|
||||
3. hub-confirm no native confirm()/prompt() in hub templates
|
||||
4. manifest-bearer no bearer-shaped literal anywhere in manifests/
|
||||
5. reuse-refs every path cited by this repo's REUSE.md still resolves
|
||||
@@ -137,6 +138,8 @@ SCRIPTS = os.path.join(ROOT, "scripts")
|
||||
GATES = [
|
||||
("site", os.path.join(SCRIPTS, "site_gates.py"), [], True, False),
|
||||
("hostinstall", os.path.join(SCRIPTS, "hostinstall_gates.py"), [], True, False),
|
||||
# R-1146 — the installer's own harness (incl. R-1141's and R-1145's cases) was red for weeks and run by nothing.
|
||||
("hostinstall-harness", os.path.join(SCRIPTS, "hostinstall_harness_gate.py"), [], True, False),
|
||||
("hub-confirm", os.path.join(SCRIPTS, "hub_confirm_gate.py"), [], True, False),
|
||||
("manifest-bearer", os.path.join(SCRIPTS, "manifest_bearer_gate.py"), [], True, False),
|
||||
("reuse-refs", os.path.join(SCRIPTS, "reuse_refs_check.py"), [ROOT], True, False),
|
||||
|
||||
@@ -103,6 +103,8 @@ COVERS = {
|
||||
"controller clone must be INCONCLUSIVE, never a pass"),
|
||||
"hostinstall": ("R-426: R-185's regression with only its dry-run echo left, the age install commented "
|
||||
"out, and a version const in a new hub sub-package; a comment naming the identifier passes"),
|
||||
"hostinstall-harness": ("R-1146: the vault-after-set order of step 4b put back (R-1145's hole) must convict "
|
||||
"with R1145-b; a chpasswd outside step_break_glass convicts INV-2"),
|
||||
"due-checks": ("R-426: a dated check whose id survives only in prose (orphan), the due-TODAY boundary "
|
||||
"(and due-tomorrow, which must pass), and the block's marker named in prose (exit 2)"),
|
||||
"one-register": ("R-426: an open roadmap row whose state NAMES another row's CLOSED (a live hole until "
|
||||
@@ -718,6 +720,18 @@ decoy("hostinstall/comment-names-identifier", "hostinstall_gates.py",
|
||||
u'package zzr426decoy\n\n// hostInstallVersion was deleted on purpose (R-94).\n'),
|
||||
expect="accept")
|
||||
|
||||
# ── hostinstall-harness (R-1146, 2026-10-18) ────────────────────────────────────────────────────────
|
||||
# The label without the fact: step 4b keeps every message and every call, but sets the password BEFORE it vaults it
|
||||
# (the shape until installer 1.33.0, R-1145) — the harness must name R1145-b. And a chpasswd outside the step's body.
|
||||
decoy("hostinstall-harness/set-before-vault", "hostinstall_harness_gate.py",
|
||||
replace_in(_HI, " # 1. Vault to the hub over the host-key-authenticated channel; password only on stdin (-d @-). The host key rides\n",
|
||||
" printf 'root:%s\\n' \"$newpw\" | chpasswd 2>/dev/null || die \"chpasswd failed\"\n"
|
||||
" # 1. Vault to the hub over the host-key-authenticated channel; password only on stdin (-d @-). The host key rides\n"),
|
||||
must="R1145-b")
|
||||
decoy("hostinstall-harness/chpasswd-outside-the-step", "hostinstall_harness_gate.py",
|
||||
replace_in(_HI, "step_break_glass() {\n", "_decoy_r1146() { printf 'root:x\\n' | chpasswd; }\nstep_break_glass() {\n"),
|
||||
must="INV-2")
|
||||
|
||||
# ── due-checks (R-426, 2026-10-06) ──────────────────────────────────────────────────────────────
|
||||
#
|
||||
# The 2026-09-01 sweep's attempt was a no-op and was withdrawn. These are the shapes a session really
|
||||
|
||||
Reference in New Issue
Block a user