Files
Markus Fritsche 0383a8f5b2 Add deploy/bullpen-up — verified one-shot Incus room bring-up (#100)
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
2026-07-24 12:28:46 +02:00

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.

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-only model 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.conf is 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.