--- # ============================================================================== # Meridian LXC — Site Playbook # ============================================================================== # Local Anthropic API powered by Chuck's Claude Max OAuth subscription. # Bridges the Claude Code SDK to /v1/messages so HAOS's anthropic conversation # integration (and any Anthropic-compatible client) can use the Max subscription # instead of paid API tokens. # # Security: # - Meridian itself has no auth layer; LAN-only reachability is the security model. # - LiteLLM sits in front for clients that speak OpenAI (e.g. Pulse). It does # require a master key (Infisical /meridian/vault_litellm_master_key). # # OAuth bootstrap is one-time, paste-code flow run directly on the LXC # (see homelab-docs services/meridian.md). Don't scp ~/.claude/ from Mac — # Mac stores the refresh token in Keychain, scp can't see it. # # Secrets: the playbook reads /meridian from Infisical itself (pre_tasks below), # so Semaphore and local deploys are identical — no per-runner env wiring. The # Infisical machine-identity client secret lives in vars/vault.yml (ansible-vault). # # Usage: # ./deploy.sh # full deploy (prompts for vault password) # ./deploy.sh --tags meridian # meridian role only # ./deploy.sh --tags litellm # litellm role only # ============================================================================== - name: Deploy Meridian LXC hosts: all become: true vars_files: - vars/main.yml - vars/vault.yml pre_tasks: - name: Deploy banner debug: msg: "===== {{ ansible_play_name }} → {{ inventory_hostname }} ({{ ansible_host | default(inventory_hostname) }}) =====" - name: Install infisicalsdk on controller pip: name: infisicalsdk state: present extra_args: --break-system-packages delegate_to: localhost become: false run_once: true - name: Authenticate with Infisical infisical.vault.login: url: "{{ infisical_url }}" auth_method: universal_auth universal_auth_client_id: "{{ infisical_client_id }}" universal_auth_client_secret: "{{ infisical_client_secret }}" register: infisical_login delegate_to: localhost become: false run_once: true - name: Read meridian secrets infisical.vault.read_secrets: login_data: "{{ infisical_login.login_data }}" project_id: "{{ infisical_project_id }}" env_slug: "prod" path: "/meridian" as_dict: true register: meridian_secrets delegate_to: localhost become: false run_once: true # Provider keys are optional (direct_* models). default('') keeps the play # idempotent before a key exists — litellm.env then writes a placeholder and # that provider's direct_* models 401 until the real key lands in /meridian. - name: Map secrets to vars set_fact: litellm_master_key: "{{ meridian_secrets.secrets.vault_litellm_master_key }}" litellm_openai_api_key: "{{ meridian_secrets.secrets.vault_openai_api_key | default('') }}" litellm_gemini_api_key: "{{ meridian_secrets.secrets.vault_gemini_api_key | default('') }}" - name: Sanity-check the LiteLLM master key resolved assert: that: litellm_master_key is defined and litellm_master_key != 'CHANGE_ME' and (litellm_master_key | length) >= 24 fail_msg: | vault_litellm_master_key did not resolve from Infisical /meridian. Check the 828d2cc8 machine identity can read /meridian (env prod), or pass -e litellm_master_key="..." for an ad-hoc run. roles: - { role: meridian, tags: ['meridian'] } - { role: litellm, tags: ['litellm'] } - { role: alloy, tags: ['alloy'] }