diff --git a/lib/bullpen_worker.py b/lib/bullpen_worker.py index 68d0fd2..88e9665 100644 --- a/lib/bullpen_worker.py +++ b/lib/bullpen_worker.py @@ -20,11 +20,27 @@ Reliability (reviewer findings #2/#4/#5): - #5: the offset is advanced AND persisted per fully-handled message, so a restart mid-batch resumes exactly, never re-dispatching completed work. """ -import json, subprocess, sys, time +import json, os, subprocess, sys, time import bullpen_config as cfg ROOM_DIR = cfg.STATE_DIR +# R4: the room's :8080 gate REJECTS a PRIVILEGED nick that posts without the shared secret +# (bullpen.lua). The one-shot worker family (dispatcher/callboy/…) is now PRIVILEGED so its +# replies stamp verified:true and orchestrators' lurkers will accept them — which means every +# post MUST carry the secret. Read it once (workers run as root; /etc/bullpen/post-secret is +# root-readable); empty is fine for a non-privileged nick (the gate ignores the arg then). +def _post_secret(): + for p in ("/etc/bullpen/post-secret", os.path.expanduser("~/.config/bullpen/post-secret")): + try: + s = open(p).read().strip() + if s: + return s + except OSError: + pass + return "" +POST_SECRET = _post_secret() + def lmcp(tool, **kw): return subprocess.run(["lmcp-tool", tool] + [f"{k}={v}" for k, v in kw.items()], capture_output=True, text=True) @@ -55,6 +71,7 @@ def run(nick, dispatch, online=None, ack="…on it", poll=2.0): kw = {"from": nick, "body": body, "type": typ} if to: kw["to"] = to if in_reply_to: kw["in_reply_to"] = in_reply_to + if POST_SECRET: kw["secret"] = POST_SECRET # R4: privileged nicks must present the secret return _ok(lmcp("room_say", **kw)) # #2: report whether the post landed def addressed(m): @@ -84,14 +101,14 @@ def run(nick, dispatch, online=None, ack="…on it", poll=2.0): if m.get("from") == nick or m.get("type") in ("ack", "reply", "system") or not addressed(m): since = mid; persist(since); continue # #5: advance+persist per message rid, asker = mid, m.get("from", "") - say(ack, to=asker, typ="ack", in_reply_to=rid) + say(ack, to=f"@{asker}", typ="ack", in_reply_to=rid) try: body = dispatch(m) except Exception as e: body = f"error: {e}" # #2/#3: only step past this message once its reply actually posts; on a failed # post leave `since` put so the next loop retries — never a silent drop. - if say(body or "(no result)", to=asker, typ="reply", in_reply_to=rid): + if say(body or "(no result)", to=f"@{asker}", typ="reply", in_reply_to=rid): since = mid; persist(since) else: sys.stderr.write(f"{nick}: reply post FAILED for id {rid}; retrying next loop\n") diff --git a/lmcp-tools/bullpen-room.lua b/lmcp-tools/bullpen-room.lua index a6692a0..53a35d7 100644 --- a/lmcp-tools/bullpen-room.lua +++ b/lmcp-tools/bullpen-room.lua @@ -14,7 +14,12 @@ local ROOM_LOG = ROOM_DIR .. "/room.jsonl" local COUNTER = ROOM_DIR .. "/counter" local SECRET_FILE = os.getenv("BULLPEN_SECRET_FILE") or "/etc/bullpen/post-secret" -local PRIVILEGED = { markus = true, noether = true, foreman = true, herder = true, py = true, reviewer = true, testdesigner = true, godev = true, jsdev = true } +-- Includes the bullpen_worker one-shot family (dispatcher/callboy/researcher/librarian/coder) +-- and the triage supervisor: they're already in cfg.TRUST and consulted by @foreman, but a +-- consumer's lurker requires verified:true (R4), and only PRIVILEGED nicks get that stamp. Left +-- out, @dispatcher's routing replies were structurally unconsumable — @foreman asked its router +-- and never heard back (the "dispatcher -> foreman, verified=None" dead-end, 2026-07-24). +local PRIVILEGED = { markus = true, noether = true, foreman = true, herder = true, py = true, reviewer = true, testdesigner = true, godev = true, jsdev = true, dispatcher = true, callboy = true, researcher = true, librarian = true, coder = true, triage = true } local function read_counter() local f = io.open(COUNTER, "r") diff --git a/lmcp-tools/bullpen.lua b/lmcp-tools/bullpen.lua index 92d311c..81081c8 100644 --- a/lmcp-tools/bullpen.lua +++ b/lmcp-tools/bullpen.lua @@ -11,7 +11,12 @@ local ROOM_LOG = ROOM_DIR .. "/room.jsonl" local COUNTER = ROOM_DIR .. "/counter" local SECRET_FILE = os.getenv("BULLPEN_SECRET_FILE") or "/etc/bullpen/post-secret" -local PRIVILEGED = { markus = true, noether = true, foreman = true, herder = true, py = true, reviewer = true, testdesigner = true, godev = true, jsdev = true } +-- The bullpen_worker one-shot family (dispatcher/callboy/researcher/librarian/coder) + the triage +-- supervisor: promoted so their replies stamp verified:true and orchestrators' lurkers (R4) accept +-- them — without this, @foreman could consult @dispatcher but never consume the routing (the +-- "verified=None dead-end", 2026-07-24). NOTE: this endpoint (bullpen.lua, :8080) REJECTS a +-- privileged post lacking the secret, so bullpen_worker was taught to send it (POST_SECRET). +local PRIVILEGED = { markus = true, noether = true, foreman = true, herder = true, py = true, reviewer = true, testdesigner = true, godev = true, jsdev = true, dispatcher = true, callboy = true, researcher = true, librarian = true, coder = true, triage = true } local function read_counter() local f = io.open(COUNTER, "r")