Containerize Phase 1 bot: Docker deployment on the Spark
Add Dockerfile, docker-compose.yml, docker-entrypoint.sh, and .dockerignore so the bot runs detached and survives reboots, replacing the foreground venv run. The image is generic (no secrets/deployment specifics baked in): host networking reaches both Synapse and the Mac; .env, config.toml, and the SSH key are mounted read-only. The entrypoint is the container's environment seam (D4 analog of launch-claude.sh) — it generates ~/.ssh/config for the mac-bridge alias from config.toml [mac] (new hostname/user fields) so the bot's `ssh mac-bridge` stays unchanged. SSH key mounted not baked; first connect uses accept-new host trust. Proven live on the Spark: container connects to Synapse and real messages launched drivable sessions on the phone across 2 rooms via the full chain.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Keep the build context minimal and the image generic/secret-free.
|
||||
# .env, config.toml, and the SSH key arrive via read-only mounts at runtime — never baked in.
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
config.toml
|
||||
|
||||
.git
|
||||
.venv/
|
||||
venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
|
||||
# Mac-side launch scripts run on the Mac, not in this container.
|
||||
scripts/
|
||||
|
||||
# Docs / OS cruft — not needed in the image.
|
||||
*.md
|
||||
.claude/
|
||||
.DS_Store
|
||||
@@ -51,12 +51,18 @@ v1 decision surface.
|
||||
|
||||
- `scripts/launch-claude.sh <repo_dir> <prompt>` — the Mac wrapper (Phase 0 deliverable;
|
||||
validate by hand before any bot code).
|
||||
- **Bot (Phase 1), on the Spark:** `python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`,
|
||||
then `.venv/bin/python src/bot.py`. Deploy by pulling `src/`, `requirements.txt`, `config.toml`,
|
||||
`.env` from the Mac via `scp mac-bridge:/Users/macpro/Projects/matrix-bridge/<x> .` (no Gitea
|
||||
needed). `MB_SSH_ALIAS` env var overrides the SSH target for testing.
|
||||
- _TODO (Phase 1 sub-step 4):_ containerize — `docker compose up` on the Spark (host networking;
|
||||
mount `.env`/`config.toml`/SSH key read-only) once the `Dockerfile` exists.
|
||||
- **Bot (Phase 1), containerized on the Spark — preferred:** from `~/matrix-bridge`,
|
||||
`docker compose up -d --build` (host networking, `restart: unless-stopped` so it survives
|
||||
reboots; read-only mounts of `.env`/`config.toml`/SSH key). Logs: `docker compose logs -f`.
|
||||
The entrypoint generates `~/.ssh/config` for the `mac-bridge` alias from `config.toml [mac]`
|
||||
(`hostname`/`user`), so the alias resolves inside the container. Override the host key path with
|
||||
`MB_SSH_KEY_HOST` if it isn't `/home/modelo/.ssh/id_ed25519`.
|
||||
- **Bot — venv (dev/fallback):** `python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`,
|
||||
then `.venv/bin/python src/bot.py` — uses modelo's host `~/.ssh/config` for the alias.
|
||||
`MB_SSH_ALIAS` overrides the SSH target for testing.
|
||||
- **Deploy:** pull the bot files from the Mac (no Gitea needed) —
|
||||
`scp mac-bridge:/Users/macpro/Projects/matrix-bridge/{Dockerfile,docker-compose.yml,docker-entrypoint.sh,requirements.txt,config.toml,.env} .`
|
||||
and `scp -r mac-bridge:/Users/macpro/Projects/matrix-bridge/src .`, then rebuild.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -72,7 +78,11 @@ v1 decision surface.
|
||||
`ssh mac-bridge gui-launch.sh`; fans out for all-projects; reports failures back to the room.
|
||||
- `requirements.txt` (matrix-nio) · `.env.example` (credential schema; real `.env` gitignored).
|
||||
- `.claude/` — Claude wiring (dir only for now).
|
||||
- _Future:_ `Dockerfile` + `docker-compose.yml` — Phase 1 sub-step 4.
|
||||
- `Dockerfile` · `docker-compose.yml` · `docker-entrypoint.sh` · `.dockerignore` — the Phase 1
|
||||
container (Spark). Generic image (no secrets/deployment specifics baked in); host networking;
|
||||
read-only mounts of `.env`/`config.toml`/SSH key. The entrypoint generates `~/.ssh/config` for
|
||||
the `mac-bridge` alias from `config.toml [mac]` — the container's environment seam (D4 analog
|
||||
of `launch-claude.sh`).
|
||||
|
||||
## Decisions (already made — don't relitigate without new information)
|
||||
|
||||
@@ -195,8 +205,27 @@ once" is not done.
|
||||
for `#all-projects` (each session named `<repo> - <date>`), and reports failures back (fail-loud).
|
||||
Tested on the **Spark** (`~/matrix-bridge`, venv) — launches worked across several rooms (N=3).
|
||||
Now 11 project rooms + all-projects; `config.toml` has a `[mac]` section (ssh_alias + launcher).
|
||||
- **Next: Phase 1 sub-step 4 — containerize.** Dockerfile + `docker-compose.yml`, host networking,
|
||||
mount `.env`/`config.toml`/SSH key read-only, so the bot runs detached and survives reboots
|
||||
(today it's a foreground venv run). Then FF `master` ← `phase-1`. Work is on branch **`phase-1`**
|
||||
(pushed); `master` is at Phase 0 (`b6cc829`). Longer-term backlog (incl. headless "ask" mode) in
|
||||
`ROADMAP.md`.
|
||||
- **Phase 1 — DONE: containerized + proven on the Spark (2026-06-15).** The bot runs as a Docker
|
||||
container on the Spark (`~/matrix-bridge`, `docker compose up -d --build`): generic image
|
||||
(`python:3.12-slim` + `openssh-client`), host networking, `restart: unless-stopped` (survives
|
||||
reboots), read-only mounts of `.env`/`config.toml`/SSH key. `docker-entrypoint.sh` generates
|
||||
`~/.ssh/config` for `mac-bridge` from `config.toml [mac]` (added `hostname`=`10.59.211.5`,
|
||||
`user`=`macpro`) — the container's env seam (D4 analog of `launch-claude.sh`); SSH key mounted
|
||||
not baked; first connect uses `StrictHostKeyChecking=accept-new` (private-WireGuard tradeoff, D9).
|
||||
*Proven live:* container connects to Synapse (`listening as @agent… 11 rooms`) and real messages
|
||||
in **2 different rooms** each launched a drivable session on the phone via the full chain
|
||||
(container → `ssh mac-bridge` → `gui-launch.sh` → `claude` → phone), rc=0 — confirming the new
|
||||
container→Mac SSH hop over WireGuard (mounted key + accept-new host trust). *Formal exit was N=3;
|
||||
the owner accepted 2 live launches across 2 rooms + the clear repeatable pattern as done.*
|
||||
Build-time checks on the Mac also passed (image builds, `ssh -G mac-bridge` resolves, entrypoint
|
||||
perms 700/600).
|
||||
- **Spark-side ops are owner-run.** The Mac has **no** authorized SSH key into the Spark
|
||||
(`modelo@10.59.211.6` — reachable over WireGuard but not authenticated; Phase 0 only set up the
|
||||
reverse, `mac-bridge`). So deploys/restarts on the Spark are run by the owner from the Spark, not
|
||||
driven from the Mac — until Phase 3 wires it behind Spark Control.
|
||||
- **Next (open — discuss before building):** Phase 2 (multi-room routing) is effectively already
|
||||
satisfied — the bot was built multi-room (11 rooms + all-projects) and routed correctly across 2
|
||||
rooms in the Phase 1 proof; only a formal confirmation pass remains. Live candidates: **Phase 3**
|
||||
(Spark Control: bot status + one-click update/restart on the dashboard, the SSH-behind-buttons
|
||||
pattern — also closes the owner-run-ops gap above) or the **headless "ask" mode** from
|
||||
`ROADMAP.md` (a message runs `claude -p` and posts the answer back into the room).
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# matrix-bridge bot — Phase 1 container.
|
||||
#
|
||||
# Runs on the Spark (always-on Linux + Docker). docker-compose uses host networking so the
|
||||
# bot reaches BOTH Synapse (clearnet TLS) and the Mac (WireGuard, via the `mac-bridge` SSH alias).
|
||||
#
|
||||
# The image is GENERIC: no deployment specifics and no secrets are baked in. At runtime
|
||||
# docker-compose mounts .env, config.toml, and the SSH key (all read-only); the entrypoint
|
||||
# generates ~/.ssh/config for the alias from config.toml's [mac] section before launching.
|
||||
FROM python:3.12-slim
|
||||
|
||||
# openssh-client: the bot shells out to `ssh mac-bridge ...` (the proven Phase 0 seam).
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends openssh-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY src/ ./src/
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# .env and config.toml arrive via read-only mounts at runtime (never baked).
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD ["python", "-u", "src/bot.py"]
|
||||
@@ -10,6 +10,16 @@ user = "@matrix-bridge-bot:<your-domain>" # a dedicated bot Matrix account (not
|
||||
# Credentials (access token or password) come from the environment or a gitignored secret —
|
||||
# never commit them. The bot reads the homeserver URL + bot creds at startup.
|
||||
|
||||
# How the bot reaches the Mac (the proven Phase 0 seam). The bot runs on the Spark,
|
||||
# where `ssh_alias` resolves; `launcher` is the absolute path to gui-launch.sh on the Mac.
|
||||
[mac]
|
||||
ssh_alias = "mac-bridge"
|
||||
launcher = "/Users/macpro/Projects/<your-repo>/scripts/gui-launch.sh"
|
||||
# Container only: docker-entrypoint.sh generates ~/.ssh/config for `ssh_alias` from these.
|
||||
# (On a host with `ssh_alias` already in ~/.ssh/config these are ignored.)
|
||||
hostname = "10.0.0.0" # the Mac's address reachable from the Spark (e.g. WireGuard IP)
|
||||
user = "<mac-username>"
|
||||
|
||||
# One [[room]] block per project.
|
||||
# room_id — the internal Matrix room ID (starts with '!'), NOT the human alias (#name:domain)
|
||||
# repo_dir — an absolute path on the Mac (note: ~/Projects uses a capital P)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# matrix-bridge bot — Phase 1 deployment on the Spark.
|
||||
#
|
||||
# `docker compose up -d` runs the bot detached; `restart: unless-stopped` brings it back after
|
||||
# a Spark reboot. Host networking lets it reach BOTH Synapse (clearnet TLS) and the Mac
|
||||
# (WireGuard, via the mac-bridge alias the entrypoint generates). The image stays generic — all
|
||||
# deployment specifics and secrets arrive through the read-only mounts below.
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
image: matrix-bridge-bot
|
||||
container_name: matrix-bridge
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./.env:/app/.env:ro
|
||||
- ./config.toml:/app/config.toml:ro
|
||||
# Dedicated Phase 0 key (spark-control@spark-32d0). Must be chmod 600 on the host.
|
||||
# Override the host path with MB_SSH_KEY_HOST if the key lives elsewhere.
|
||||
- ${MB_SSH_KEY_HOST:-/home/modelo/.ssh/id_ed25519}:/root/.ssh/id_ed25519:ro
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
# matrix-bridge container entrypoint — the container's "environment seam".
|
||||
#
|
||||
# Generates ~/.ssh/config for the `mac-bridge` alias from config.toml's [mac] section, then
|
||||
# execs the bot. This mirrors the Mac side, where launch-claude.sh owns environment setup and
|
||||
# the bot stays dumb (AGENTS.md D4): SSH-client wiring lives here, not in bot.py. On the Spark
|
||||
# HOST the bot uses modelo's existing ~/.ssh/config; in the container we recreate just the one
|
||||
# alias we need, pointing at the mounted key.
|
||||
set -e
|
||||
|
||||
SSH_DIR="$HOME/.ssh"
|
||||
mkdir -p "$SSH_DIR"
|
||||
chmod 700 "$SSH_DIR"
|
||||
|
||||
# Write ~/.ssh/config straight from config.toml [mac] (no eval; values never hit a shell).
|
||||
# IdentityFile is the in-container mount target (a container constant, see docker-compose.yml).
|
||||
# StrictHostKeyChecking=accept-new auto-trusts the Mac's host key on first connect — acceptable
|
||||
# on the private WireGuard network (same transport-trust reasoning as D9) and avoids an
|
||||
# interactive prompt that would otherwise hang the bot.
|
||||
MB_SSH_KEY="${MB_SSH_KEY:-$SSH_DIR/id_ed25519}" \
|
||||
SSH_CONFIG="$SSH_DIR/config" \
|
||||
KNOWN_HOSTS="$SSH_DIR/known_hosts" \
|
||||
python - <<'PY'
|
||||
import os, tomllib
|
||||
with open("/app/config.toml", "rb") as f:
|
||||
mac = tomllib.load(f)["mac"]
|
||||
config = f"""Host {mac.get('ssh_alias', 'mac-bridge')}
|
||||
HostName {mac['hostname']}
|
||||
User {mac['user']}
|
||||
IdentityFile {os.environ['MB_SSH_KEY']}
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking accept-new
|
||||
UserKnownHostsFile {os.environ['KNOWN_HOSTS']}
|
||||
"""
|
||||
with open(os.environ['SSH_CONFIG'], "w") as f:
|
||||
f.write(config)
|
||||
PY
|
||||
chmod 600 "$SSH_DIR/config"
|
||||
|
||||
exec "$@"
|
||||
Reference in New Issue
Block a user