# marvin-turnstone Local turnstone deployment as a **container-VM**: a podman image is turned into a bootable VM with [boco](https://git.nubo.sh/hulthe/boco) (custom init + ext4 qcow2 + QEMU direct kernel boot). Replaces the older opencode-in-a-container setup (`~/marvin`). ``` host podman build ──> boco build qcow2 ──> boco boot │ QEMU/KVM (virtio, user-net) ┌─────────────────────┴───────────────────┐ │ VM (Arch) │ │ /boco/init → start-turnstone.sh (root) │ │ ├─ sshd (ops channel) │ │ └─ setpriv marvin │ │ └─ turnstone-server :8080 PID 1 │ └─────────────────────────────────────────┘ host :8080 ──hostfwd──> web UI host :2222 ──> guest sshd :22 guest ──HTTPS (double slirp egress)──> api.tensorx.ai (LLM) ``` ## Prerequisites Host (Arch): - `podman`, `qemu-base`, `virtiofsd`, `croc` (only if fetching the kernel) - `boco` CLI on PATH and `boco-init` at `~/.local/share/boco/boco-init` (build from the boco repo) - Kernel at `~/.local/share/boco/registry/vmlinuz` (x86 bzImage with `CONFIG_VIRTIO_FS=y`; transfer via croc) VM image: - Arch base with python + turnstone (venv at `/opt/turnstone`), git, helix, podman (rootless, subuid range), openssh, build toolchain - `marvin` user; sshd + turnstone-server started by `start-turnstone.sh` (server runs as `marvin`, PID 1) ## Build ```sh scripts/build.sh ``` Creates `configs/ssh/id_ed25519` (ops keypair, once), builds the podman image, then packs it as `boco` qcow2 into the boco registry. ## Run ```sh scripts/run.sh ``` - Web UI: (login `marvin` / `marvin-turnstone` unless `ADMIN_PASSWORD` was overridden — change it) - Ops SSH: `ssh -i configs/ssh/id_ed25519 -p 2222 marvin@localhost` - QEMU runs foreground; quit with `ctrl-a x` ### Access from another machine (tailcat) `localhost` above is the machine running QEMU. To reach the VM from elsewhere, run `scripts/expose.sh` next to it — a tailcat tunnel (WireGuard over Tailscale's DERP relays, outbound-only, no account): ``` host $ scripts/expose.sh # 🐈 Server listening with saved key "default": tcpGFwWCCH... your $ tailcat forward tcpGFwWCCH... 8080:8080 2222:2222 ``` Then browse `http://localhost:8080` / `ssh -p 2222 marvin@localhost` from *your* machine. The address is stable across VM restarts (saved key) and is a bearer secret — share it only privately. ### LLM backend `[models.tensorx]` in `configs/turnstone/config.toml` points at the TensorX OpenAI-compatible gateway (`https://api.tensorx.ai/v1`, same key this machine's opencode uses — see `~/.local/share/opencode/auth.json`). Guest egress to HTTPS endpoints works through both slirp layers; no relay needed. Self-hosted provider later: add a `[models.*]` entry with any OpenAI-compatible endpoint reachable from the guest (see `config.toml.example`). ## Shares (virtiofs, survives reboots) | Host path | Guest path | Purpose | |------------------------|--------------------------------|--------------------------| | `configs/turnstone/` | `/home/marvin/.config/turnstone` | turnstone config.toml | | `data/` | `/var/lib/turnstone` | SQLite database | | `work/` | `/home/marvin/work` | agent workspace | Guest-written files appear on the host owned by subuid-range UIDs (virtiofsd runs under `podman unshare`) — cosmetic, and stable across boots. ## Configuration Edit `configs/turnstone/config.toml` (gitignored; see `config.toml.example`): - `[models.tensorx]` — TensorX gateway; `api_key` must be set (currently mirrored from opencode's `auth.json`). - `[auth] jwt_secret` — required, generate once. - `[database] path` — SQLite on the `data/` share. - `[tools]` — `workspace_dir` surfaced to the model (`/home/marvin/work`, mounted from `./work`). ### Personas Built-ins (Engineer, Writer, Researcher, Scribe, Manager, Executive) are seeded by turnstone's migration. This deployment adds a custom default persona `marvin` (VM-specific: persistence rules, Gitea remotes, podman) via `scripts/persona.py` — the prompt text is versioned there and re-applied after a DB reset: ```sh python3 scripts/persona.py # --db data/turnstone.db default ``` (Without a console deployment, persona CRUD is a direct SQLite write; with `turnstone-console` running, use its admin API/Personas tab.) ### Skills The 13 skills from the old opencode setup (`~/marvin/configs/opencode/skills`, copied into this repo under `skills/`) are imported into turnstone's DB-backed catalog via `scripts/import_skills.py`. The script runs INSIDE the VM (uses turnstone's storage layer + SKILL.md parser + risk scanner): ```sh scp -O -i configs/ssh/id_ed25519 -P 2222 -r skills scripts/import_skills.py \ marvin@localhost:/tmp/ ssh -i configs/ssh/id_ed25519 -p 2222 marvin@localhost \ /opt/turnstone/bin/python /tmp/import_skills.py --dir /tmp/skills ``` Idempotent (`--update` re-imports + appends version rows). **Reboot the VM after importing** — the running server holds a stale WAL snapshot and won't see new rows until restart. ## Notes / trade-offs - boco boots with `-snapshot`: **only the virtiofs shares persist.** Agent-installed packages, git clones outside `work/`, and podman image storage inside the VM are discarded on shutdown. - boco hardcodes `-smp 8`; memory via `VM_MEMORY=...M scripts/run.sh`. - The old setup's Traefik is dropped — hostfwd replaces it.