# skaldsong Wizard + reader surface for multi-agent storytelling. Single FastAPI process serves both the SvelteKit-built SPA (static, baked into the image) and the wizard backend. Talks to Worldtree as a Bifrost consumer — Worldtree issues per-dispatch JWTs (post-v0.3) that skaldsong's `/bifrost` endpoint verifies against the shared HS256 secret. **Server:** ana-docker **URL:** `http://10.250.50.70:8300` (configurable via `.env`) **Upstream repo:** [vh/skaldsong](https://gitea.phasefinal.com/vh/skaldsong) **Image:** `gitea.phasefinal.com/vh/skaldsong:` — built + pushed by vh/skaldsong's CI on every push to `main` and on tag. ## Path layout (on ana-docker) | Host path | Container path | Purpose | Restic? | |---|---|---|---| | `/opt/docker/compose/skaldsong/` | — | compose.yaml + .env | included (via `/opt/docker`) | | `/opt/docker/conf/skaldsong/db/` | `/app/state/db` | SQLite (`skaldsong-ui.db` + WAL/SHM) — wizard state, story rows | **included** | | `/opt/docker/conf/skaldsong/runs/` | `/app/state/runs` | Per-story generation pipeline artifacts (spec, state, output, logs, manuscripts) | **included** | ## Network model Internal tooling, LAN-only. Container's :8000 published on host :8300 (configurable via `SKALDSONG_BIND` / `SKALDSONG_PORT`); access direct at `http://10.250.50.70:8300`. No Traefik, no TLS terminator, no public hostname. Skaldsong talks to two services on the LAN: - **Worldtree** at `http://10.250.50.152:8080` (corviduo-dev, LAN-direct from ana-docker — same subnet). - **Kokoro TTS** at `http://10.100.79.3:8193` (irv-ml1, reached via the WireGuard tunnel on ana-wg — same path asset-engine uses). Worldtree calls back to skaldsong's `/bifrost` endpoint at `http://10.250.50.70:8300` for tool-dispatch — that URL must be in Worldtree's `BIFROST_CLIENT_ALLOWED_HOSTS`. Host:port changes require a coordinated allowlist update on Worldtree-side (see [corviduo-dev README](../../servers/corviduo-dev/README.md) for the docker-as-root edit pattern). ## Deploy Two paths — automated (preferred) and manual (escape hatch). ### Automated (Gitea Actions, push-to-main) vh/skaldsong ships `.gitea/workflows/deploy.yaml`. Push to main + manual `workflow_dispatch` triggers: 1. Build + push `gitea.phasefinal.com/vh/skaldsong:${{ github.sha }}` and `:latest` to the gitea registry. 2. Checkout this management repo using `MGMT_REPO_TOKEN`. 3. Run `scripts/elway ana-docker --playbook playbooks/deploy-skaldsong.yaml --var ref=${{ github.sha }}` via `DEPLOY_SSH_KEY`. Three secrets required on vh/skaldsong's Actions settings: - `DEPLOY_SSH_KEY` — private key authorized on ana-docker for lkraven. - `MGMT_REPO_TOKEN` — Gitea PAT with `read:repository` on this repo. - `REGISTRY_USER` + `REGISTRY_TOKEN` — for the docker login step (token needs `write:package` scope). ### Manual (elway from a workstation) ```bash # Deploy a specific SHA (CI just pushed it): scripts/elway ana-docker --playbook playbooks/deploy-skaldsong.yaml \ --var ref= # Or pin to a release tag: scripts/elway ana-docker --playbook playbooks/deploy-skaldsong.yaml \ --var ref=v0.1.0 ``` ## Env-var contract | Var | In compose? | In `.env`? | Notes | |---|---|---|---| | `SKALDSONG_IMAGE` | ✓ (image:) | ✓ | Full image URI; CI passes SHA-pin via elway `--var ref=` → playbook → this var. | | `SKALDSONG_PORT` | ✓ | ✓ | Host-side port (8300 default). Internal always 8000. | | `SKALDSONG_BIND` | ✓ | ✓ | Host bind address (0.0.0.0 default). | | `SKALDSONG_DB_DIR` | ✓ (mount) | ✓ | Host path for SQLite. | | `SKALDSONG_RUNS_DIR_HOST` | ✓ (mount) | ✓ | Host path for generation artifacts. | | `SKALDSONG_BIFROST_JWT_KEY` | ✓ (anchor) | ✓ | Shared HS256 secret w/ Worldtree's `WORLDTREE_SKALDSONG_USER_KEY`. Same value as `WORLDTREE_TOKEN`. | | `SKALDSONG_HOST_BIFROST_ENDPOINT_URL` | ✓ (anchor) | ✓ | URL Worldtree calls back to (must match allowlist). | | `WORLDTREE_TOKEN` | ✓ (anchor) | ✓ | Bearer for outbound HTTP to Worldtree. Same value as `SKALDSONG_BIFROST_JWT_KEY`. | | `WORLDTREE_BASE_URL` | ✓ (anchor) | ✓ | URL skaldsong calls Worldtree at. | | `SKALDSONG_HOST_WIZARD_AGENT_ID` | ✓ (anchor) | ✓ | Existing agent slot ID (`skaldsong:wizard-v2`). Blank burns a Heimdall quota slot on first boot. | | `SKALDSONG_HOST_CORS_ORIGINS` | ✓ (anchor) | ✓ | Comma-separated CORS origins. | | `SKALDSONG_TTS_ENGINE` | ✓ (anchor) | ✓ | `kokoro` (only engine in v1). | | `SKALDSONG_TTS_BASE_URL` | ✓ (anchor) | ✓ | Kokoro URL (WG-routed). | `&skaldsong-env` anchor declares all `SKALDSONG_*` vars via `${VAR:-}` substitution. Same env-anchor pattern as Worldtree's `&worldtree-env` (see [Worldtree #175 heuristic](https://gitea.phasefinal.com/vh/worldtree/issues/175)); new var additions must land in BOTH the `.env` AND the anchor. ## Restart semantics Per skaldsong's `INV-006` (host CLAUDE.md): process restart marks any `running` generation rows `partial` and never auto-resumes. Deploy mid-run is safe — the SPA's reader treats partials like a power outage. Cancellation of in-flight runs on deploy is acceptable for v1. `docker compose up -d --force-recreate --pull always` is the playbook's apply step (or `--pull never` when called with a specific `--var ref=` where the image was pre-pulled). Same kill-signal + graceful-shutdown pattern as Worldtree. ## Worldtree allowlist coordination The `SKALDSONG_HOST_BIFROST_ENDPOINT_URL` value MUST be in Worldtree's `BIFROST_CLIENT_ALLOWED_HOSTS` env var on corviduo-dev. As of deploy-time the value is `10.250.50.70:8300`. If you change `SKALDSONG_PORT` or move skaldsong off ana-docker, the allowlist needs a paired update on corviduo-dev. Reference: the edit-via-docker-as-root pattern in [servers/corviduo-dev/README.md](../../servers/corviduo-dev/README.md#permissions-model--the-docker-as-root-pattern).