Server-only deployment has no console admin API, so the persona is upserted directly into the SQLite DB via scripts/persona.py with the prompt text versioned in the repo (VM persistence rules, Gitea/GitHub remotes, rootless podman). Built-in personas are seeded by migration 063; this sets marvin as the interactive default.
marvin-turnstone
Local turnstone deployment as a container-VM: a podman image is turned
into a bootable VM with 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)bocoCLI on PATH andboco-initat~/.local/share/boco/boco-init(build from the boco repo)- Kernel at
~/.local/share/boco/registry/vmlinuz(x86 bzImage withCONFIG_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 marvinuser; sshd + turnstone-server started bystart-turnstone.sh(server runs asmarvin, PID 1)
Build
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
scripts/run.sh
- Web UI: http://localhost:8080 (login
marvin/marvin-turnstoneunlessADMIN_PASSWORDwas 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_keymust be set (currently mirrored from opencode'sauth.json).[auth] jwt_secret— required, generate once.[database] path— SQLite on thedata/share.[tools]—workspace_dirsurfaced 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:
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.)
Notes / trade-offs
- boco boots with
-snapshot: only the virtiofs shares persist. Agent-installed packages, git clones outsidework/, and podman image storage inside the VM are discarded on shutdown. - boco hardcodes
-smp 8; memory viaVM_MEMORY=...M scripts/run.sh. - The old setup's Traefik is dropped — hostfwd replaces it.