# corviduo-dev Worldtree-team development VM. **PFI-hosted, Worldtree-team-managed at the OS / application layer.** Runs the demo + personal + pinned Worldtree deployments that vor, /vor-frame, and asset-engine talk to over the LAN. ## Client context - **Client / team:** Worldtree (architect's team is the upstream owner) - **PFI role:** hosting + emergency-ops backstop. Ownership of OS configuration, application data, and deploy workflows lives with the Worldtree team. PFI provides networking, IP space, hypervisor capacity, and emergency-access pathways. Treat data-affecting work the same way we treat the SureFire `sf-*` hosts — coordinate via the architect over [althing](../../scripts/elway) before touching anything beyond a documented runbook. ## Network - **LAN IP:** `10.250.50.152` - **Hostname (OS):** `corviduo-dev` - **FQDN:** not in DNS — reach by IP. Add to workstation `/etc/hosts` if you'll be SSH'ing frequently: ``` 10.250.50.152 corviduo-dev ``` - **SSH:** `vh@10.250.50.152` (this dir's `ssh-target` carries that) - **Site:** Anaheim (PFI colo), `10.250.0.0/16` subnet ## Infrastructure - **Hypervisor:** assumed `pfi-pve` (10.250.250.31). Not yet enumerated — verify via `qm list` on pfi-pve when you next have a reason to touch the hypervisor. - **OS:** Debian (verified via `hostname` + standard tooling; not yet inspected with `server_inspect.sh`). ## Deployments ### Worldtree (Worldtree-team-managed, under `/opt/`) Three `docker compose` projects under `/opt/`: | Project dir | Container name | Role | |---|---|---| | `/opt/worldtree/` | `worldtree-worldtree-api-1` (port 8080) | Demo deployment — the "shared" Worldtree instance vor + asset-engine talk to. | | `/opt/worldtree-personal/` | `worldtree-personal-worldtree-api-1` (port 8081) | Personal deployment — separate compose project, persistent state independent of demo. | | `/opt/worldtree-pinned/` | `worldtree-pinned-worldtree-api-1` (port 8082) | Pinned deployment — intentionally lags behind `demo` for regression/comparison purposes. | > **Deploys are CI/CD-only — NOT an infra-ops job.** All three > deployments (demo, pinned, personal) are deployed exclusively by the > Worldtree team's CI/CD pipeline. infra-ops does **not** deploy these > manually. A peer "deploy Worldtree vX.Y.Z to " request > sent to infra-ops is **misrouted** — point them back to their > pipeline; do not run the docker-as-root pattern to do it. The > docker-as-root pattern below is for **emergency-ops only** > (provider-key restore), not routine deploys. Each project has: - `compose.yaml` (image, env wiring, healthcheck, port mapping) - `.env` (provider keys, ports, image tag — `deploy:deploy mode 600`) The image is pulled from `gitea.phasefinal.com/vh/worldtree`, SHA-pinned per deploy after Worldtree's CI workflow commit `8ef3801` (health-gated `:latest` advance — `:latest` only moves on successful `/health` probes). ### PFI monitoring agents (infra-ops-managed, under `/home/vh/`) Bootstrap 2026-05-24. Live at `/home/vh/docker/compose/{beszel,dozzle-agent}/`, **not** under `/opt/docker/compose/` — corviduo-dev's `/opt/` is owned by `deploy:deploy` (Worldtree team) and `vh` lacks passwordless sudo to create the fleet-standard path. Functionally identical; documented here so future infra-ops sessions find them. | Project dir | Container | Role | |---|---|---| | `/home/vh/docker/compose/beszel/` | `beszel-agent` (host port 45876, `network_mode: host`) | Agent for the Beszel hub at ana-docker:8090. Hub SSH-polls inbound using the seeded ed25519 pubkey. | | `/home/vh/docker/compose/dozzle-agent/` | `dozzle-agent` (host port 7007) | Agent for the Dozzle hub at ana-docker:8088. Hub connects inbound over mTLS (auto-generated certs on first run). | | `/home/vh/docker/compose/dockge/` | `dockge` (host port 5001) | Per-host compose UI. Scoped to PFI-managed stacks under `/home/vh/docker/compose/` — does NOT see `/opt/worldtree*/` (worldtree-team boundary). `DOCKGE_HOST_STACKS_ROOT=/home/vh/docker` overrides the fleet-default `/opt/docker`. | The empty `traefik-net` external docker network was created on this host as a side effect of dozzle-agent's compose (which declares it external). Future PFI services landing here can reuse it. After bootstrap, the dozzle hub's `.env` on ana-docker had `10.250.50.152:7007` appended to `DOZZLE_REMOTE_AGENT` and was force-recreated. The beszel hub needs corviduo-dev added via the **"Add System"** UI action (host=10.250.50.152 port=45876) — pure-API equivalent uses pocketbase admin auth which we didn't script. One-time operator step. ## Permissions model + the docker-as-root pattern `vh` is the SSH user for ops. **`vh` does NOT have passwordless sudo.** The `.env` files are `deploy:deploy mode 600` — vh cannot read or write them through the normal POSIX path. `vh` *is* in the `docker` group on this VM. That's effectively root for any operation that can be expressed as "run a container with a bind mount": ```bash # Apply an edit to /opt/worldtree/.env (which vh can't read directly) # without sudo. Run as vh. ssh vh@10.250.50.152 'docker run --rm -i \ -v /opt/worldtree:/wt \ -v /var/run/docker.sock:/var/run/docker.sock \ docker:cli sh -s' <<'SH' sed -i '/^Z_AI_API_KEY=/d' /wt/.env # idempotent strip echo "Z_AI_API_KEY=" >> /wt/.env cd /wt && docker compose up -d --force-recreate --pull never SH ``` **Security warning:** docker-group membership is effectively unrestricted root via bind mounts. This is a well-known property of Linux docker installs, not a misconfiguration. Treat the existence of vh's docker-group membership as a privilege grant equivalent to sudo, and don't expand it casually. Notable gotchas: - `docker compose up -d` from inside the docker:cli sandbox does NOT carry the host's registry credentials, so it will fail to pull authenticated images. Workaround: `--pull never` if the image is already cached on the host (Worldtree's deploy workflow always pre-pulls the SHA-tagged image before the SSH `compose up` step). - The bare `compose.yaml` defaults `WORLDTREE_IMAGE` to `gitea.phasefinal.com/vh/worldtree:latest` if the env var is unset. Inside the docker:cli sandbox the env var doesn't carry, so emergency-ops `compose up` may pin to `:latest` rather than the intended SHA. Pass `-e WORLDTREE_IMAGE=...:` to the docker run invocation when this matters. ## Emergency runbook: provider-key restore When Worldtree's startup validator fails because provider keys are missing or empty (this happened 2026-05-12 to `Z_AI_API_KEY`): 1. Get the key values from the architect (via althing) — both `Z_AI_API_KEY` (LLM provider auth, `providers.zai.api_key`) and `ZAI_API_KEY` (MCP service key for web reads/searches). These are distinct keys. 2. Apply via the docker-as-root pattern above. Idempotent: strip any existing line for the same name before appending. 3. Force-recreate: `docker compose up -d --force-recreate --pull never` from the docker:cli sandbox. 4. Verify: `GET http://10.250.50.152:8080/health` returns 200; boot log shows "Application startup complete". 5. Treat the keys as compromised-by-disclosure if they passed over althing (multi-agent bus). Ask the architect to rotate; re-apply the rotated values via the same flow. ## Backup coverage **Currently none — known gap.** This VM is not in PBS, not in `backrest`, not in the fleet restic profiles. Failure modes: - VM hardware loss → app rebuild from Worldtree's git repo + pipeline; data loss on `/data/state`, `/data/kb`, `/app/sessions` volumes. - VM filesystem corruption → same. Worth coordinating with the architect on whether the data volumes (`worldtree-kb`, `worldtree-state`, `worldtree-sessions`) need external backup, and whether PFI hosts that via the standard restic pattern or whether the architect's team handles it via application-level snapshotting. ## Refresh state ```bash scripts/refresh-server-info.sh corviduo-dev ``` Reads via this dir's `ssh-target` (`vh@10.250.50.152`). The inspect script is read-only — safe to run any time. ## History - **2026-05-12 outage:** Worldtree image `98e4ca0` introduced a startup validator that fail-fasts on empty required provider keys (`Z_AI_API_KEY` referenced by `providers.zai`). Demo `.env` had the keys present-but-empty, container entered a restart loop. Architect shipped a validator-scoping fix-forward (`70703b0`) and the actual key values; infra applied the .env adds via the docker-as-root pattern documented above. Same outage exposed a `:latest` tag-drift bug that architect closed with commit `8ef3801` (health-gated `:latest` advance in the deploy workflow). Full thread: althing thread `01KRFZ349NSY8FEQDSM6TNEK7M`. - **2026-05-25 config-as-bind-mount (Worldtree #205, v0.25.2):** `/app/config/` is now a bind-mounted host dir (`${WORLDTREE_CONFIG_DIR:-./config}:/app/config` in compose). Per-instance edits persist across image upgrades — no more in-container hacks for things like rate-limit defaults or per-agent profile overrides. Entrypoint shim runs as root, chowns + seeds the bind dir from `/app/config-defaults/` on first boot when empty, then drops privileges via `gosu` to the worldtree user. Idempotent against pre-existing populated dirs. Default `per_user_tokens_per_hour` raised image-side from 1M to 1B at the same time. Shipped after a v0.25.1 attempt that wedged on a uid-mismatch in the cp+chown sequence (host bind dir was root-owned, container user uid 1000 couldn't write); v0.25.2 fixed via root-then-drop pattern. Full thread: `01KSEBNJBVPZ29M7PD9FJECYQP`.