Files
bullpen/bin/bullpen-coder
T
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

68 lines
3.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""@coder — bullpen worker: turn a request into a Lua file, write it, run it, report.
Reactive. Engine = dspark via the gated proxy. It ONLY ever writes into its sandbox and
runs `lua5.4` there as an unprivileged user with a timeout — nothing else (mechanical bound).
bullpen-coder # room loop (systemd)
bullpen-coder --once "a lua function that reverses a string, with a self-test"
"""
import json, os, re, subprocess, sys, urllib.request
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_worker as bw
import bullpen_config as cfg
SANDBOX = cfg.CODER_DIR
ENGINE = cfg.PROXY # gated proxy (gate+failover)
MODEL = cfg.MODEL
MAXBODY = 1400
SYS = ("You are a Lua coder. Given a task and any reference material, output ONLY valid Lua "
"source that solves it AND includes a small self-test with sample data printed to stdout. "
"No markdown, no prose, no code fences — just the Lua.")
def gen_code(request):
"""Returns (code, served_model). The proxy may fail over, so the model actually SERVING
the request isn't necessarily MODEL — the response body's own `model` field is ground
truth; report that, not the ask."""
payload = json.dumps({"model": MODEL,
"messages": [{"role": "system", "content": SYS}, {"role": "user", "content": request}],
"temperature": 0.2, "max_tokens": 1400}).encode()
req = urllib.request.Request(ENGINE, payload, {"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=200) as r:
d = json.load(r)
txt = d["choices"][0]["message"]["content"]
m = re.search(r"```(?:lua)?\s*(.*?)```", txt, re.S) # strip fences if the model adds them
return (m.group(1) if m else txt).strip(), d.get("model")
def run_lua(path):
os.chmod(path, 0o644)
r = subprocess.run(
["timeout", "10", "su", "-s", "/bin/sh", "nobody", "-c", f"cd {SANDBOX} && lua5.4 {path}"],
capture_output=True, text=True)
out = r.stdout.strip()
if r.stderr.strip():
out += ("\n[stderr] " + r.stderr.strip())
return out.strip(), r.returncode
def dispatch(msg):
req, rid = msg.get("body", ""), msg.get("id")
os.makedirs(SANDBOX, exist_ok=True)
try:
code, served = gen_code(req)
except Exception as e:
return f"codegen error: {e}"
path = f"{SANDBOX}/job{rid}.lua"
open(path, "w").write(code + "\n")
out, rc = run_lua(path)
status = "ran OK" if rc == 0 else f"exit {rc}"
model_tag = f"asked {MODEL}, served by {served}" if served and served != MODEL else f"model {MODEL}"
body = (f"wrote {path} ({len(code)} chars, {model_tag}) — {status}. "
f"output:\n{out[:400]}\n--- code ---\n{code}")
return body
if __name__ == "__main__":
if len(sys.argv) > 2 and sys.argv[1] == "--once":
print(dispatch({"body": sys.argv[2], "id": 0}))
else:
bw.run("coder", dispatch, online="coder online — writes+runs Lua (sandboxed). @coder <task>",
ack="…coding")