The road to `bullpen up`: launch a bridged Debian system container, install lmcp from the marfrit apt repo + this repo, seed config + a room lmcp, then round-trip a room_say/room_read INSIDE the container so you get a proven box, not a hopeful one. Env-overridable; defaults target the origin fleet's public repo + apt repo. --verify re-runs just the probe; --destroy tears a throwaway down. Verified end-to-end on a throwaway Incus container (boltzmann): launch -> apt+lmcp -> install.sh -> seed -> room service -> probe round-tripped (real room.jsonl entry), idempotent on re-run, clean --destroy. Also: - install.sh --system: /usr/local layout for the room/worker host (bin -> /usr/local/bin, lib -> /usr/local/lib/bullpen, contrib/lmcp-tool onto PATH) — matches how hertz is wired; the plain user mode is unchanged. - contrib/lmcp-tool: vendor the lmcp client the coordinators call by name. It was a loose unpackaged script on the room host (portability gap for a new fleet). - DEPLOY.md: bullpen-up as the fastest path; manual recipe fixed to actually install lmcp + note --system. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
5.4 KiB
Deploying bullpen to a new fleet
bullpen needs three roles reachable by name: a room host (runs the lmcp room), a grind host
(repos + pytest), and a coordinator host (lurkers/grinder/triage/doctor). One machine can hold
all three. Everything host-specific lives in bullpen.conf (see bullpen.conf.example) — the
code carries the origin-fleet values as defaults, so you only set what differs.
Two ways to install. Pick by trust.
Fastest path — deploy/bullpen-up
On an Incus host, one command launches a bridged Debian system container, installs lmcp + this repo, seeds config, starts the room, and round-trips a probe message to prove the room answers inside the container before handing it back:
BULLPEN_CT=bullpen BULLPEN_PROFILES=default,<your-bridge> deploy/bullpen-up
deploy/bullpen-up --verify # re-run just the room round-trip
deploy/bullpen-up --destroy # throwaway cleanup
It stands up the room substrate only — not the LLM workers/lurkers, which need your gateway,
models, and a trust decision about fleet creds (that's the next section). Everything is env-
overridable (BULLPEN_REPO_URL, BULLPEN_IMAGE, BULLPEN_APT_LINE, BULLPEN_ROOM_PORT, …); the
defaults target the origin fleet's public repo + apt repo. The manual recipe below is what it
automates, if you'd rather drive it by hand or split the roles across hosts.
Recommended — Incus system container
Why a container, and why Incus specifically:
- The agent + code-execution layer ingests untrusted input (room bodies) and runs model
output. That layer must not hold fleet credentials — same rule as the grinder's bwrap jail and
the
<10k-download → container-onlymodel policy. A container is the isolation boundary. - Incus system containers behave like metal: systemd init, a bridged LAN IP, real hostname.
sic, systemd units, and normal tooling work unchanged, and the agent never gets confused by a Docker-style NAT/PID-1 environment. No snap, no loopback/userland proxy, no port publishing. - The image +
bullpen.confis also the reproducible unit — deploy-from-repo, finished.
Trust-tier the roles:
- Room + coordinators (lmcp room, lurker/grinder/triage/doctor) — routing/orchestration, low untrusted-code exposure. Host or a plain container, either is fine.
- Agent + execution tier (the opencode/claude instances that read untrusted room text, and the grinder that runs generated code) — its own container: no ssh keys, no fleet creds, egress- filtered, disposable. This is the boundary that matters. @foreman already runs this way (a scoped, room-only opencode instance); the grinder already bwrap-jails the code it runs.
Recipe (adjust names for your fleet):
# 1. a bridged system container (LAN IP, systemd, feels like metal)
incus launch images:debian/13 bullpen -p default -p <your-bridge-profile>
incus exec bullpen -- apt-get update
incus exec bullpen -- apt-get install -y git python3 lua5.4 lua-socket curl # + go, if you grind Go
# 1b. lmcp (the room server) from the marfrit apt repo
incus exec bullpen -- sh -c 'install -d /etc/apt/keyrings
curl -fsSL https://packages.reauktion.de/marfrit.gpg -o /etc/apt/keyrings/marfrit.gpg
echo "deb [signed-by=/etc/apt/keyrings/marfrit.gpg] https://packages.reauktion.de/debian trixie main" \
> /etc/apt/sources.list.d/marfrit.list
apt-get update && apt-get install -y lmcp'
# 2. the code, deploy-from-repo (symlinks -> the checkout; git pull is the deploy)
incus exec bullpen -- git clone https://your-git/marfrit/bullpen.git /opt/bullpen
incus exec bullpen -- /opt/bullpen/deploy/install.sh # symlinks bin/ + lurker into ~/.local/bin
# …or install.sh --system for the room/worker host: bin -> /usr/local/bin,
# lib -> /usr/local/lib/bullpen, and contrib/lmcp-tool onto PATH (runs as root).
# 3. the config — one file, only the values that differ from the defaults
incus exec bullpen -- sh -c 'mkdir -p /etc/bullpen && cp /opt/bullpen/bullpen.conf.example /etc/bullpen/bullpen.conf'
incus exec bullpen -- sh -c 'printf "BULLPEN_ROOM_HOST=%s\nBULLPEN_GRIND_HOST=%s\nBULLPEN_PROXY=%s\nBULLPEN_USER=%s\n" \
room-host grind-host http://your-gateway:8082/v1/chat/completions youruser >> /etc/bullpen/bullpen.conf'
# 4. bring up the services (systemd user units ship in systemd/ and lurker/)
incus exec bullpen -- systemctl --user enable --now bullpen-lurker@<nick> bullpen-doctor ...
# 5. scope the agent tier: NO fleet creds in this container's environment, and restrict egress
# to the room host + the llm gateway only (incus network ACLs / a firewall). The container
# must not be able to ssh the fleet or read secrets it wasn't handed.
Verify: post room_say ... to=@<nick> type=ask and confirm a reply. An agent asked to list its
tools should see only what its config exposes — no shell if it's an orchestrator.
Possible — bare, single-host, unbounded
For a single trusted operator (you, your own box), skip the container:
git clone https://your-git/marfrit/bullpen.git ~/src/bullpen
~/src/bullpen/deploy/install.sh
cp ~/src/bullpen/bullpen.conf.example /etc/bullpen/bullpen.conf # edit it
# enable the systemd units you need
This runs the agents as you, with your host's shell and fleet reach. That's unbounded — a compromised or prompt-injected agent can do whatever you can. Fine when you are the trust boundary; not the default to hand a new fleet. If in doubt, use the container.