skills/ carries the SKILL.md files; scripts/import_skills.py runs inside the VM via turnstone's own storage layer (SKILL.md parser + risk scanner) so rows match what the console admin API would write. Server must be restarted after import (stale WAL snapshot).
151 lines
5.9 KiB
Markdown
151 lines
5.9 KiB
Markdown
# 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: <http://localhost:8080> (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.
|