marvin a09b327ca9 Expose only the console; node binds 8081 guest-internal
The console routes to nodes (rendezvous), so it is the single public
port: server -> :8081 (no host forward), console -> :8080, 8090
forward dropped. Verified: node unreachable from host, routed
workstream creation lands on turnstone-vm-main at 127.0.0.1:8081,
route/command proxy works, cluster view tracks the workstream.
2026-09-28 22:20:15 +02:00

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)
  • 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

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
  • Console (proxy to the node; chat UI + admin panel): :8080 — login marvin / marvin-turnstone unless ADMIN_PASSWORD was overridden. The node binds :8081 guest-internal only (no host forward); the console routes to it, so all chat/admin goes through one port.
  • 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:

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

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.
S
Description
No description provided
Readme
115 KiB
0 Stars 0 Watchers 0 Forks
Languages
Python 49.6%
Shell 33.8%
Dockerfile 16.6%