f385070dbf
Follows the architecture pass on #100. Three classes of change: 1. sys.path — the ten Python entrypoints used two idioms: seven pinned /usr/local/lib/bullpen (system layout only), three resolved ../lib from realpath(__file__) first. The second form is a superset: it works in both layouts and still falls back to /usr/local/lib. Unified on it. Proven: on noether, where /usr/local/lib/bullpen does not exist, `bullpen-dispatcher --roster` now runs straight from the checkout — it could not before. 2. Role hosts from config — deus-seize, bullpen-selfimprove, bullpen-up's ROSTER keys, and the lurker's opencode host/model now come from bullpen_config (env > /etc/bullpen/bullpen.conf > origin-fleet default), so behaviour here is unchanged. bullpen-up in particular was a silent no-op on any other fleet because its ROSTER was keyed by literal hostnames. 3. Advice the system gives BACK — @dispatcher answered every routing question with `sic hertz room-ask …` and @callboy reported artifacts as `sic hertz cat …`. On a foreign fleet that is an instruction to talk to a machine that does not exist. Now cfg.ROOM_HOST. tests/test_no_hardcoded_hosts.py enforces (3) by AST, not grep: prose in docstrings and comments stays (why a thing runs on boltzmann is worth keeping), but a hostname inside a string the program actually uses fails the test. It caught three sites I had missed by eye — the dispatcher's proxy default, its "on <host>" routing regex, and the grinder's timeout message. The routing regex is the interesting one: those names are real fleet knowledge, not addresses, so they moved to cfg.FLEET_HOSTS rather than being deleted. Verified both directions — with the default list "on boltzmann" matches and "on buildbox" does not; with BULLPEN_FLEET_HOSTS="buildbox chatbox" it is exactly reversed. Also verified: DRY_RUN=1 bullpen-selfimprove composes a brief with zero mentions of the origin fleet under overridden hosts; deus-seize still runs; 52/52 across both portability suites. NOT fixed, flagged: deus-seize and bullpen-selfimprove pass the room post-secret as an lmcp-tool argv value, so it is visible in `ps` on the room host (and in curl's argv inside lmcp-tool). lmcp-tool has no stdin form for argument values; closing it means changing lmcp-tool, which is deployed fleet-wide.
115 lines
5.5 KiB
Python
Executable File
115 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""deus-seize <nick> — full campaign stop: revoke the lease + abort in-flight work across
|
|
every host an orchestrator could be running on. Design item (a), 2026-07-27 — "Deus hat
|
|
selbst keinen Token, kann ihn aber im Bedarfsfall entziehen, und ALLE laufenden Prompts
|
|
werden dabei abgebrochen."
|
|
|
|
Real fan-out, not a flag: this was the manual, ad-hoc sequence used earlier this session to
|
|
stop a stuck @foreman (restart opencode-foreman.service, hand-run oc-rpc abort) — bundled
|
|
into one command instead of a `systemctl stop` sledgehammer.
|
|
|
|
deus-seize foreman
|
|
deus-seize foreman --by deus
|
|
"""
|
|
import json, os, re, shutil, subprocess, sys
|
|
sys.path[:0] = [p for p in (os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "lib"), "/usr/local/lib/bullpen") if os.path.isdir(p)]
|
|
import bullpen_config as cfg
|
|
|
|
# Resolve via PATH so this works wherever sic is installed; the absolute path stays as the
|
|
# fallback because deus-seize may be run from a stripped environment (a systemd unit, a cron).
|
|
SIC = shutil.which("sic") or "/usr/local/bin/sic"
|
|
|
|
|
|
def sic(host, *args, timeout=30):
|
|
try:
|
|
return subprocess.run([SIC, host, *args], capture_output=True, text=True, timeout=timeout)
|
|
except Exception as e:
|
|
class R: returncode = 1; stdout = ""; stderr = str(e)
|
|
return R()
|
|
|
|
|
|
def _post_secret():
|
|
# local copies first (works if run ON the room host as root, or from a host with its own copy)
|
|
for p in (cfg.SECRET_FILE, os.path.expanduser("~/.config/bullpen/post-secret")):
|
|
try:
|
|
s = open(p).read().strip()
|
|
if s:
|
|
return s
|
|
except OSError:
|
|
pass
|
|
# otherwise fetch it from the room host over sic — the file is root-only there, and
|
|
# deus-seize is meant to run as a normal user (with working sic/SSH), not as root (root has
|
|
# no sic identity of its own, which is what broke this the first time it was run: `sudo
|
|
# deus-seize` makes EVERY internal sic call below try to SSH out as root too).
|
|
r = sic(cfg.ROOM_HOST, "sudo", "cat", cfg.SECRET_FILE)
|
|
s = (r.stdout or "").strip()
|
|
return s or None
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("usage: deus-seize <nick> [--by <name>]", file=sys.stderr)
|
|
sys.exit(2)
|
|
nick = sys.argv[1]
|
|
if not re.fullmatch(r"[A-Za-z0-9_-]+", nick):
|
|
# the nick is interpolated into a remote `sh -c` below (step 3) — keep it a bare word
|
|
print(f"deus-seize: refusing suspicious nick {nick!r}", file=sys.stderr)
|
|
sys.exit(2)
|
|
by = "deus"
|
|
if "--by" in sys.argv:
|
|
by = sys.argv[sys.argv.index("--by") + 1]
|
|
|
|
secret = _post_secret()
|
|
if not secret:
|
|
print("deus-seize: no post-secret readable — cannot authenticate lease_seize", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(f"1. revoking {nick}'s campaign lease...")
|
|
# NOTE: the secret travels in argv, so it is visible in `ps` on the room host (and in
|
|
# curl's own argv inside lmcp-tool). lmcp-tool has no stdin form for argument values;
|
|
# closing this needs a change to lmcp-tool itself, which is fleet-wide. Tracked, not fixed here.
|
|
r = sic(cfg.ROOM_HOST, "lmcp-tool", "lease_seize", f"nick={nick}", f"by={by}", f"secret={secret}")
|
|
print(f" {(r.stdout or r.stderr).strip()}")
|
|
|
|
print(f"2. restarting {nick}'s lurker on {cfg.COORD_HOST} (kills any in-flight subprocess/HTTP call)...")
|
|
r = sic(cfg.COORD_HOST, "systemctl", "--user", "restart", f"bullpen-lurker@{nick}.service")
|
|
print(" restarted" if r.returncode == 0 else f" FAILED: {(r.stderr or r.stdout).strip()}")
|
|
|
|
print(f"3. aborting {nick}'s opencode session...")
|
|
# ~ expands to the remote user's home — no username baked in. nick is validated above.
|
|
r = sic(cfg.COORD_HOST, "sh", "-c", f"cat ~/{nick}_lurker/.session")
|
|
sid = (r.stdout or "").strip()
|
|
if sid and r.returncode == 0:
|
|
# oc-rpc talks straight to the opencode server's HTTP API (cfg.OC_URL) — it runs
|
|
# LOCALLY (wherever deus-seize itself runs), not sic'd onto that host (it needs no
|
|
# shell there at all, and that host has no copy of oc-rpc anyway — this bit for real
|
|
# on first test).
|
|
OC_RPC = shutil.which("oc-rpc") or os.path.expanduser("~/.local/bin/oc-rpc")
|
|
try:
|
|
r2 = subprocess.run([OC_RPC, "abort", sid], capture_output=True, text=True, timeout=30)
|
|
print(f" session {sid}: {(r2.stdout or r2.stderr).strip()}")
|
|
except FileNotFoundError:
|
|
print(f" session {sid}: oc-rpc not found at {OC_RPC} on this host — abort skipped")
|
|
else:
|
|
print(" no cached session id found — nothing to abort")
|
|
|
|
print(f"4. checking {cfg.GRIND_HOST} for active grind processes (NOT auto-killed — the grind "
|
|
f"command line doesn't carry the dispatching nick, so attribution isn't safe to "
|
|
f"automate; kill by hand if one of these is this campaign's):")
|
|
r = sic(cfg.GRIND_HOST,
|
|
"sh", "-c",
|
|
r"""for p in $(pgrep -f '[b]ullpen-grinder --once' 2>/dev/null); do """
|
|
r"""a=$(ps -o etimes= -p $p 2>/dev/null | tr -d ' '); """
|
|
r"""c=$(ps -o args= -p $p 2>/dev/null | cut -c1-100); """
|
|
r"""echo " pid=$p age=${a}s cmd=$c"; done""")
|
|
out = (r.stdout or "").strip()
|
|
print(out if out else " none running")
|
|
|
|
print(f"\n{nick} seized by {by}. Lease revoked — its next `ask` dispatch is rejected "
|
|
f"until lease_acquire runs again (the lurker does this automatically on its next "
|
|
f"legitimate poke, so this is a real stop, not a permanent ban).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|