Files
homelab-ansible-lxc-meridian/README.md
T
2026-05-19 11:01:41 -04:00

93 lines
4.2 KiB
Markdown

# 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. OAuth bootstrap — run `claude auth login --claudeai` **directly on the LXC** via the bundled binary. **Do not** scp `~/.claude/` from your Mac — macOS keeps the refresh token in the Keychain and the snapshot 401s as soon as the short-lived access token expires (incident write-up: 2026-05-17 → 2026-05-19, see [project_meridian](../homelab-docs/claude-memory/project_meridian.md)).
```bash
# Stop the service so it's not racing the auth writer.
ssh cbalders@192.168.1.164 sudo systemctl stop meridian
# Paste-code flow as the meridian user (needs -t for TTY).
ssh -t cbalders@192.168.1.164 \
'sudo -u meridian -H /usr/lib/node_modules/@rynfar/meridian/node_modules/@anthropic-ai/claude-code/bin/claude.exe auth login --claudeai'
# → prints https://claude.com/cai/oauth/authorize?... — paste into a Mac
# browser, log in with the Max account, paste the code back.
# → ends with: Login successful.
ssh cbalders@192.168.1.164 sudo systemctl start meridian
# Verify (expect loggedIn: true, subscriptionType: max):
ssh cbalders@192.168.1.164 \
'sudo -u meridian -H /usr/lib/node_modules/@rynfar/meridian/node_modules/@anthropic-ai/claude-code/bin/claude.exe auth status'
```
5. Smoke from a LAN host:
```bash
curl http://192.168.1.164:3456/v1/messages \
-H 'Content-Type: application/json' \
-H 'anthropic-version: 2023-06-01' \
-d '{"model":"claude-haiku-4-5","max_tokens":40,"messages":[{"role":"user","content":"reply with the single word: pong"}]}'
```
## 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