bullpen: make @dispatcher + one-shot worker family consumable by orchestrators
The bullpen_worker family (dispatcher/callboy/researcher/librarian/coder) + the triage supervisor were in cfg.TRUST but absent from the room's PRIVILEGED tables, so their replies stamped verified=None and an orchestrator's lurker (R4) refused them — @foreman could consult @dispatcher but never consume the routing (a silent dead-end that stranded it into a spin). They also addressed replies to a bare nick (no @). Fixed and verified end-to-end (a @dispatcher reply now lands to=@markus, verified=true): - lib/bullpen_worker.py: address replies as @<invoker>; read POST_SECRET and attach secret= to every room_say (the strict :8080 gate REJECTS a privileged post lacking the secret, not merely leaving it unverified). Workers run as root -> secret readable. - lmcp-tools/bullpen.lua (:8080) + bullpen-room.lua (:8081): add dispatcher/callboy/ researcher/librarian/coder/triage to PRIVILEGED. Keep the two tables in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
This commit is contained in:
+20
-3
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user