diff --git a/tools/fleet/deploy-gate.sh b/tools/fleet/deploy-gate.sh new file mode 100755 index 000000000..4bebe1298 --- /dev/null +++ b/tools/fleet/deploy-gate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# The fleet's deploy gate (coordinator, 6 October 2026 23:0xZ, after three comment-on-a-line faults): every script pushed to a box +# passes master's defaults-line check and a syntax check first; lib/box.py's put() calls this for any .sh/.py source under +# tools/fleet and refuses the push on red. Usage: deploy-gate.sh (exit 0 = clean) +set -u; rc=0; HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; MASTER_CI="${IGNEUM_MASTER_CI:-/Users/joshm/Projects/igneum/tools/ci}" +for f in "$@"; do + case "$f" in + *.sh) bash -n "$f" 2>&1 | head -2 | sed "s|^|$f: |" ; [ "${PIPESTATUS[0]}" = 0 ] || rc=1 ;; + *.py) python3 -m py_compile "$f" 2>&1 | head -2 | sed "s|^|$f: |"; [ "${PIPESTATUS[0]}" = 0 ] || rc=1 ;; + esac + # a comment inserted mid-line swallows the rest of the line: no code may follow a comment that follows code + if [ -f "$MASTER_CI/defaults-line-check.sh" ]; then bash "$MASTER_CI/defaults-line-check.sh" "$f" 2>&1 | grep -v "no assignment hides" | sed "s|^|$f: |" | head -3; [ "${PIPESTATUS[0]}" = 0 ] || rc=1; fi + if grep -nE '^[^#"'"'"']*[^ #]\s+#\s[^"]*\S+.*(\$\{|>>|&$|;\s*[a-zA-Z_]+=)' "$f" 2>/dev/null | grep -vE '^\s*[0-9]+:\s*#' | grep -qE '#.*(>> |&$|; [A-Z_]+=)'; then echo "$f: a comment with code after it (the 17:18Z/22:10Z/22:52Z class)"; rc=1; fi +done +[ $rc = 0 ] && echo "deploy-gate: clean ($# files)" || echo "deploy-gate: RED, the push is refused" +exit $rc diff --git a/tools/fleet/lib/box.py b/tools/fleet/lib/box.py index 02c790483..c4e488f3b 100644 --- a/tools/fleet/lib/box.py +++ b/tools/fleet/lib/box.py @@ -53,6 +53,11 @@ class Box: if rc != 0: raise SshError(f"{self.label}: rc {rc}: {err.strip()[:200]}") return out def put(self, files, dest, tries=3, timeout=900): + # the deploy gate: a fleet script (.sh/.py under tools/fleet) goes to a box only when deploy-gate.sh reads clean + srcs = [f for f in files if (f.endswith(".sh") or f.endswith(".py")) and "/tools/fleet/" in os.path.abspath(f)] + if srcs: + g = subprocess.run(["bash", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "deploy-gate.sh")] + srcs, capture_output=True, text=True, timeout=120) + if g.returncode != 0: raise SshError(f"{self.label}: deploy gate RED, push refused: {(g.stdout + g.stderr).strip()[:300]}") for i in range(tries): r = subprocess.run(["scp"] + SSH_OPTS + ["-P", str(self.port)] + list(files) + [f"{self.user}@{self.host}:{dest}"], capture_output=True, text=True, timeout=timeout) if r.returncode == 0: return True diff --git a/tools/fleet/lib/standing.py b/tools/fleet/lib/standing.py index 745027462..803b97999 100644 --- a/tools/fleet/lib/standing.py +++ b/tools/fleet/lib/standing.py @@ -135,6 +135,17 @@ def weight_check(labels, limit=0.10, hours=1.0): if ok: with open(WEIGHT_LOG, "a") as f: f.write(json.dumps({"ts": time.time(), "labels": labels, "share": want}) + "\n") return verdict +def table_signed_pct(): + """The signed share of the frozen voter table at the hub's last lock (the "% of total" of the LOCKED line), or None.""" + hub = next((v for v in Registry.load().values() if v.get("hub") and v.get("state") != "destroyed"), None) + if not hub: return None + out = Box(hub["ssh_host"], hub["ssh_port"], "hub-1", None, None, hub.get("provider")).run("grep -E 'Finality: checkpoint [0-9]+ LOCKED:' /root/fleet/node.log | tail -1 | grep -oE '[0-9.]+% of total' | head -1", 30)[1].strip() + try: return float(out.split("%")[0]) + except Exception: return None +def table_gate(min_pct=75.0): + """The coordinator's rule (6 October 2026, 23:0xZ): nothing of the fleet's moves on a standing box, pool slices included, until the + table reads over min_pct signed. Returns (ok, pct).""" + pct = table_signed_pct(); return (pct is not None and pct > min_pct), pct def loop(every=600): dead = {} while True: @@ -161,6 +172,8 @@ if __name__ == "__main__": elif a[0] == "update": print(update(a[1])) elif a[0] == "rerent": print(rerent(a[1])) elif a[0] == "loop": loop(int(a[1]) if len(a) > 1 else 600) + elif a[0] == "table_gate": + ok, pct = table_gate(float(a[1]) if len(a) > 1 else 75.0); print(json.dumps({"ok": ok, "signed_pct_of_table": pct, "min": float(a[1]) if len(a) > 1 else 75.0})); sys.exit(0 if ok else 1) elif a[0] == "weight_check": v = weight_check(a[1:]); print(json.dumps(v)); sys.exit(0 if v["ok"] else 1) elif a[0] == "weights": diff --git a/tools/fleet/winddown.py b/tools/fleet/winddown.py index 66f539649..0e22a0c0a 100644 --- a/tools/fleet/winddown.py +++ b/tools/fleet/winddown.py @@ -8,7 +8,7 @@ line ten minutes later. Candidates: the wave pods, then the 8x rig (one voter), for the proving agent's word. Log: ~/Desktop/fleet/winddown.log; schedule and finality reads in ~/Desktop/fleet/winddown.jsonl.""" import sys, os, time, json, datetime sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))); from lib import Box, Registry, standing -LIMIT = float(os.environ.get("LIMIT", "0.095")); HOLD_PCT = float(os.environ.get("HOLD_PCT", "66.7")); EVERY = int(os.environ.get("EVERY", "3600")) +LIMIT = float(os.environ.get("LIMIT", "0.095")); HOLD_PCT = float(os.environ.get("HOLD_PCT", "75.0")) # the coordinator's rule of 23:0xZ: nothing moves on a standing box, pool slices included, until the table reads over 75 percent signed; EVERY = int(os.environ.get("EVERY", "3600")) ROOT = os.path.expanduser("~/Desktop/fleet"); J = os.path.join(ROOT, "winddown.jsonl") def st(): return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) def log(**row): row["t"] = st(); open(J, "a").write(json.dumps(row) + "\n"); print(row["t"], {k: v for k, v in row.items() if k != "t"}, flush=True)