# homelab-ansible-lxc-meridian Ansible config for the Meridian LXC (CTID 457 on pve01, `192.168.1.164`). ## What it is [Meridian](https://github.com/rynfar/meridian) is a local Anthropic API server backed by the Claude Code SDK. It translates `/v1/messages` calls into Claude Code SDK `query()` calls so any Anthropic-compatible client can run against Chuck's Claude Max subscription instead of paid API tokens. Primary consumer: HAOS's built-in `anthropic` conversation integration (via a custom_component fork that adds `CONF_BASE_URL`). ## Architecture - Debian 12 LXC, Node 22 from NodeSource apt repo - `@rynfar/meridian` installed globally via `npm` - systemd unit `meridian.service` running as user `meridian`, binding to `0.0.0.0:3456` - OAuth credentials live at `/opt/meridian/.claude/` (transferred manually after first deploy — see Bootstrap below) - **No auth at the Meridian layer.** LAN-only reachability is the entire security model — no Caddy public vhost, no Cloudflare tunnel. ## Bootstrap 1. Provision the LXC via `homelab-terraform/lxc` (`terraform apply`). 2. Run the LXC bootstrap one-liner from `feedback_lxc_bootstrap_user`: ``` ssh root@192.168.1.164 'apt-get update && apt-get install -y sudo && useradd -m -s /bin/bash cbalders && echo "cbalders ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/90-cbalders && chmod 440 /etc/sudoers.d/90-cbalders' ``` (Plus authorized_keys for cbalders.) 3. Local first deploy (Semaphore can't reach a fresh host): ``` ./deploy.sh ``` Expect: Node 22 installed, `@rynfar/meridian` installed, systemd unit deployed and enabled but **not started** (no creds yet — `claude_creds.stat.exists` gates the start task). 4. On Chuck's Mac: ``` npm i -g @anthropic-ai/claude-code claude login # browser flow → ~/.claude/.credentials.json scp -r ~/.claude cbalders@192.168.1.164:/tmp/.claude-bootstrap ``` 5. On the LXC: ``` sudo cp -r /tmp/.claude-bootstrap/. /opt/meridian/.claude/ sudo chown -R meridian:meridian /opt/meridian/.claude/ sudo systemctl start meridian ``` 6. Smoke from a LAN host: ``` curl http://192.168.1.164:3456/v1/messages \ -H 'Content-Type: application/json' \ -H 'x-api-key: placeholder' \ -H 'anthropic-version: 2023-06-01' \ -d '{"model":"claude-sonnet-4-5","max_tokens":100,"messages":[{"role":"user","content":"hi"}]}' ``` ## Operations - **Subsequent deploys**: via Semaphore template "Meridian Deploy" (added to the sync-semaphore-state.py manifest). - **Token refresh**: handled automatically by the Claude Code SDK; if it ever fails, `sudo -u meridian /usr/bin/meridian refresh-token` from the LXC. - **Restart after creds change**: `sudo systemctl restart meridian`. - **Logs**: `journalctl -u meridian -f`. ## Files ``` roles/meridian/ Node 22 install + npm i meridian + systemd unit roles/node_exporter/ Prometheus exporter for fleet metrics vars/main.yml base packages, ssh keys, meridian config site.yml playbook entrypoint inventory.ini single host (192.168.1.164) deploy.sh wrapper for local first-run ``` ## Memory pointers - `project_meridian` — overall design, OAuth model, consumers - `feedback_local_dns_only` — DNS convention (no public CF for services) - `feedback_lxc_bootstrap_user` — root bootstrap pattern for fresh LXCs - `feedback_fresh_host_bootstrap` — Semaphore can't reach fresh hosts