--- # Example playbook. The role takes multica_pat as a VALUE — pick ONE of the # lookups below (or any other) and delete the rest. Changing secret provider # later is a change to this file only; the role is untouched. - name: Provision Multica agent hosts hosts: multica_agent_hosts become: true vars: multica_server_url: "https://multica.wdw-labs.cloud" multica_app_url: "https://multica.wdw-labs.cloud" multica_version: "0.6.5" multica_node_version: "24.22.1" multica_workspaces_root: "/opt/multica/workspaces" # No NAS mount under the workspaces root yet, so the mount guard is off. # When utilidor is mounted, switch both lines together — never set the # root onto a mount without also setting the guard, or a play that runs # while the mount is down fills the root filesystem instead of failing. # multica_workspaces_root: "/mnt/utilidor/multica/workspaces" # multica_workspaces_root_mount: "/mnt/utilidor" multica_agent_clis: - claude # Merged into the daemon user's ~/.claude/settings.json. Mirrors the model # mix already live on haunted-mansion (unpinned runs on sonnet, subagents # on haiku), and stops the npm install self-updating past the pin. multica_claude_settings: model: sonnet alwaysThinkingEnabled: false env: DISABLE_AUTOUPDATER: "1" CLAUDE_CODE_SUBAGENT_MODEL: haiku MAX_THINKING_TOKENS: "4000" BASH_MAX_OUTPUT_LENGTH: "20000" # --- Secret source ---------------------------------------------------- # This homelab keeps Ansible secrets in ansible-vault encrypted files, so # that is the live form. One PAT per host (named daemon-), held in # the global vault (group_vars/all/vault) as a dictionary keyed by short # hostname: # vault_multica_pats: { haunted-mansion: mul_…, spaceship-earth: mul_… } # A host with no entry fails the pre_task below, by name, before anything # runs. The indirection keeps the name greppable and the file swappable. multica_pat: "{{ vault_multica_pats[inventory_hostname_short] }}" # # Alternatives, if the backend ever consolidates. Each is a one-line # change here; the role is untouched by any of them. # # HashiCorp Vault (community.hashi_vault): # multica_pat: "{{ lookup('community.hashi_vault.vault_kv2_get', # 'multica/agent-host').secret.pat }}" # # 1Password (community.general): # multica_pat: "{{ lookup('community.general.onepassword', # 'Multica Agent Host PAT', field='credential') }}" # # Environment variable on the control node: # multica_pat: "{{ lookup('env', 'MULTICA_PAT') }}" # 1Password service account token for `op` (WDWL-37). One shared token, # vault_op_service_account_claude in group_vars/all/vault, scoped by # 1Password to the agents' vault. The role writes it to a 0600 file and # installs an `op` shim that reads it; it never reaches the daemon unit. multica_op_deliver_token: true multica_op_service_account_token: "{{ vault_op_service_account_claude }}" # The role's own PAT assert is no_log, so a missing dictionary entry would # surface there only as "censored". This checks key presence, never the # value, so its failure can name the host. pre_tasks: - name: Require a per-host Multica PAT in the vault ansible.builtin.assert: that: - vault_multica_pats is defined - vault_multica_pats is mapping - inventory_hostname_short in vault_multica_pats fail_msg: >- No Multica PAT for {{ inventory_hostname_short }}: add vault_multica_pats['{{ inventory_hostname_short }}'] to group_vars/all/vault. quiet: true tags: [always, multica_pat_check] roles: - role: multica_agent_host