mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
Home Assistant pinned the MCP service account to its read-only group when it created the account and never again, so an account promoted to administrator by hand kept every mutating tool that scope denies while the deploy stayed green, because nothing looked at the existing case. The group is now asserted on every run and a refusal aborts instead of passing. Nextcloud minted an app password under a fixed managed name and never revoked the previous one, so every run that found the stored password rejected added another valid token while the old ones kept full account access, accumulating across deploys. The mint is now preceded by a listing and the revocation of every token carrying that name. Both new tasks mask their output, because the listing names the account and its token ids even though the JSON strips the secrets. Prometheus ran with no query timeout, so its two-minute default applied: a query the adapter abandoned after fifteen seconds kept the server busy for the rest of it. The flag is now derived from the adapter's own limit and doubled, which gives the server room past the adapter's deadline while keeping the two from drifting apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
147 lines
4.7 KiB
YAML
147 lines
4.7 KiB
YAML
---
|
|
- name: "🔍 List the accounts this instance knows"
|
|
ansible.builtin.command:
|
|
argv:
|
|
- container
|
|
- exec
|
|
- -u
|
|
- "{{ NEXTCLOUD_DOCKER_USER }}"
|
|
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
|
|
- php
|
|
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
|
|
- user:list
|
|
- --output=json
|
|
register: nextcloud_mcp_accounts
|
|
changed_when: false
|
|
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
|
|
run_once: true
|
|
|
|
- name: "👤 Create the account the MCP clients authenticate as"
|
|
ansible.builtin.command:
|
|
argv:
|
|
- container
|
|
- exec
|
|
- -u
|
|
- "{{ NEXTCLOUD_DOCKER_USER }}"
|
|
- -e
|
|
- "OC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
|
|
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
|
|
- php
|
|
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
|
|
- user:add
|
|
- --password-from-env
|
|
- "--display-name={{ NEXTCLOUD_MCP_USER_DISPLAY_NAME }}"
|
|
- "{{ NEXTCLOUD_MCP_USERNAME }}"
|
|
when: NEXTCLOUD_MCP_USERNAME not in (nextcloud_mcp_accounts.stdout | from_json)
|
|
changed_when: true
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
|
|
run_once: true
|
|
|
|
- name: "🔑 Set the account's password to the one this deployment declares"
|
|
ansible.builtin.command:
|
|
argv:
|
|
- container
|
|
- exec
|
|
- -u
|
|
- "{{ NEXTCLOUD_DOCKER_USER }}"
|
|
- -e
|
|
- "OC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
|
|
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
|
|
- php
|
|
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
|
|
- user:resetpassword
|
|
- --password-from-env
|
|
- "{{ NEXTCLOUD_MCP_USERNAME }}"
|
|
changed_when: true
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
|
|
run_once: true
|
|
|
|
- name: "🔍 List the app passwords the MCP account already holds"
|
|
ansible.builtin.command:
|
|
argv:
|
|
- container
|
|
- exec
|
|
- -u
|
|
- "{{ NEXTCLOUD_DOCKER_USER }}"
|
|
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
|
|
- php
|
|
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
|
|
- user:auth-tokens:list
|
|
- "{{ NEXTCLOUD_MCP_USERNAME }}"
|
|
- --output=json
|
|
register: nextcloud_mcp_tokens_held
|
|
changed_when: false
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
|
|
run_once: true
|
|
|
|
- name: "🧹 Revoke the app passwords an earlier run minted under the managed name"
|
|
ansible.builtin.command:
|
|
argv:
|
|
- container
|
|
- exec
|
|
- -u
|
|
- "{{ NEXTCLOUD_DOCKER_USER }}"
|
|
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
|
|
- php
|
|
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
|
|
- user:auth-tokens:delete
|
|
- "{{ NEXTCLOUD_MCP_USERNAME }}"
|
|
- "{{ item | string }}"
|
|
loop: >-
|
|
{{ nextcloud_mcp_tokens_held.stdout | from_json
|
|
| selectattr('name', 'equalto', NEXTCLOUD_MCP_TOKEN_NAME)
|
|
| map(attribute='id') | list }}
|
|
changed_when: true
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
|
|
run_once: true
|
|
|
|
- name: "🔑 Mint an app password for the MCP clients"
|
|
ansible.builtin.command:
|
|
argv:
|
|
- container
|
|
- exec
|
|
- -u
|
|
- "{{ NEXTCLOUD_DOCKER_USER }}"
|
|
- -e
|
|
- "NC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
|
|
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
|
|
- php
|
|
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
|
|
- user:auth-tokens:add
|
|
- "{{ NEXTCLOUD_MCP_USERNAME }}"
|
|
- --password-from-env
|
|
- "--name={{ NEXTCLOUD_MCP_TOKEN_NAME }}"
|
|
register: nextcloud_mcp_token_mint
|
|
changed_when: true
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
|
|
run_once: true
|
|
|
|
- name: "🔧 Take the minted app password from the command output"
|
|
ansible.builtin.set_fact:
|
|
nextcloud_mcp_token: "{{ nextcloud_mcp_token_mint.stdout_lines | last | trim }}"
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
run_once: true
|
|
|
|
- name: "🛑 Refuse an instance that issued no usable app password"
|
|
ansible.builtin.assert:
|
|
that:
|
|
- nextcloud_mcp_token | length >= 64
|
|
fail_msg: >-
|
|
occ user:auth-tokens:add returned no app password, so every MCP client
|
|
would be configured with an empty bearer and fail at first use.
|
|
|
|
- name: "💾 Persist the app password so MCP clients can read it"
|
|
ansible.builtin.include_role:
|
|
name: sys-token-store
|
|
tasks_from: write.yml
|
|
vars:
|
|
sys_token_store_user_key: "{{ lookup('config', application_id, 'mcp.credential.owner') }}"
|
|
sys_token_store_app: "{{ application_id }}"
|
|
sys_token_store_token: "{{ nextcloud_mcp_token }}"
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|