# 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:

```sh
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-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):

```sh
# 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:

```sh
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.
