Files
marfrit f385070dbf bullpen: no fleet host in a runtime string; entrypoints run from either layout (#100)
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.
2026-08-02 00:06:26 +02:00

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()