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.
68 lines
3.1 KiB
Python
Executable File
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")
|