From 671581d717ac9d2fec6bd946205868306c75b32e Mon Sep 17 00:00:00 2001 From: Fabio Scotto di Santolo Date: Mon, 31 Aug 2026 10:02:32 +0200 Subject: [PATCH] feat: manage coding agents independently --- AGENTS.md | 9 ++- README.md | 39 +++++++++++- ansible/inventory/group_vars/all.yml | 59 +++++++++++++++---- ansible/inventory/group_vars/desktop.yml | 3 +- .../inventory/group_vars/desktop_gnome.yml | 2 - ansible/inventory/group_vars/server.yml | 2 +- ansible/inventory/group_vars/workstation.yml | 2 - ansible/roles/dotfiles_common/tasks/main.yml | 32 +++++++--- ansible/roles/packages_fedora/tasks/main.yml | 27 +++++++-- .../profile_desktop_common/tasks/main.yml | 24 ++++++++ .../tasks/main.yml | 39 +++++++++--- ansible/site.yml | 12 ++++ 12 files changed, 204 insertions(+), 46 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 22b1e8d..17f29f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, FreeBS - Atlas NAS: `ansible-playbook ansible/site.yml --limit atlas --check --diff` - Focused checks: - Emacs is disabled by default; temporary Emacs check: `ansible-playbook ansible/site.yml --limit --tags emacs --check --diff -e emacs_enabled=true` + - AI coding agents: `ansible-playbook ansible/site.yml --limit --tags ai_agents --check --diff` - Mail bootstrap: `sh -n scripts/bootstrap_mail.sh` and `shellcheck scripts/bootstrap_mail.sh` - Server compose render: `docker compose -f /opt/docker/server/docker-compose.yml config` @@ -127,10 +128,12 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i are validated; do not make it a dependency of the Atlas baseline. ## Coding Agent Notes -- Shared agent packages live in `ai_agents_npm_packages` in `ansible/inventory/group_vars/all.yml`. +- Shared agent definitions and lifecycle flags live in `ai_agents` in `ansible/inventory/group_vars/all.yml`. - Shared agent dotfiles live in `ai_agents_dotfiles`; rendered configs live in `ai_agents_templates`. -- Desktop and WSL profiles consume the shared agent package list; do not duplicate package entries in profile-specific vars. -- `dotfiles_common` copies common dotfiles plus `ai_agents_dotfiles`, then renders `ai_agents_templates`. +- Every `ai_agents.` entry has independent `install_enabled`, `deploy_enabled`, and `uninstall_enabled` flags. Installation and removal must not both be true for the same agent; the common pre-task fails before changes when they conflict. +- Fedora, Void desktop, and WSL workstation profiles consume the shared agent definitions; do not duplicate package entries in profile-specific vars. IBM Bob on the workstation follows its own flags. +- `dotfiles_common` deploys `ai_agents_dotfiles` and renders `ai_agents_templates` only when deployment is enabled. +- Removal is limited to the managed npm packages and `/usr/local/bin/bob`; never remove agent dotfiles, instructions, credentials, or user data. - Keep `.config/ai/` as the common instruction source; update agent-specific entrypoints to reference it rather than duplicating instruction text. ## Tooling Notes diff --git a/README.md b/README.md index 864316a..c7781c3 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,43 @@ Emacs is enabled on Fedora/GNOME and workstation profiles. `dotfiles_common` dep ansible-playbook ansible/site.yml --limit --tags emacs -e emacs_enabled=true ``` +## AI coding agents + +The shared npm-managed agents are OpenCode, Claude Code, Codex, Gemini CLI, and +GitHub Copilot; IBM Bob is also managed on `deadalus`. Each agent has its own +lifecycle flags in `ansible/inventory/group_vars/all.yml`, so one agent can be +installed, configured, or removed without affecting the others: + +```yaml +ai_agents: + opencode: + npm_package: opencode-ai + install_enabled: true + deploy_enabled: true + uninstall_enabled: false +``` + +Installation uses the npm `latest` state; deployment copies/renders only the +configuration belonging to each enabled agent. Removal deletes only the selected +managed npm package or, for IBM Bob, `/usr/local/bin/bob`; it preserves dotfiles, +instructions, credentials, and user data. Installation and removal are mutually +exclusive per agent: the playbook fails before making changes when both flags are +true for the same agent. Servers set `ai_agents: {}` and therefore manage none. + +Run a focused dry run with: + +```bash +ansible-playbook ansible/site.yml --limit ikaros --tags ai_agents --check --diff +ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --diff +``` + +To preview removal, set `install_enabled: false` and `uninstall_enabled: true` +only in the entry for the agent being removed, then run: + +```bash +ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --diff +``` + ## Main roles | Role | What it does | @@ -277,7 +314,7 @@ ansible-playbook ansible/site.yml --list-tags | Tag | Main scope | | --- | --- | | `always` | Common pre-tasks, including optional vault loading. | -| `ai_agents` | Shared AI agent installation on Fedora and WSL. | +| `ai_agents` | AI coding-agent install, configuration deployment, and managed-binary removal. | | `dotfiles` | User configuration across all profiles. | | `dotfiles:common` | Shared dotfiles. | | `dotfiles:desktop` | Void and Fedora/GNOME desktop dotfiles. | diff --git a/ansible/inventory/group_vars/all.yml b/ansible/inventory/group_vars/all.yml index ca019a7..4003e50 100644 --- a/ansible/inventory/group_vars/all.yml +++ b/ansible/inventory/group_vars/all.yml @@ -90,40 +90,75 @@ common_dotfiles: dest: .config/bat/ mode: preserve -ai_agents_npm_packages: - - name: "opencode-ai" - state: latest - - name: "@anthropic-ai/claude-code" - state: latest - - name: "@openai/codex" - state: latest - - name: "@google/gemini-cli" - state: latest - - name: "@github/copilot" - state: latest +# AI coding-agent lifecycle controls are independent for every agent. +# Installation and removal are mutually exclusive per agent; the common +# pre-task enforces this before any changes are made. +ai_agents: + opencode: + npm_package: opencode-ai + install_enabled: true + deploy_enabled: true + uninstall_enabled: false + claude_code: + npm_package: "@anthropic-ai/claude-code" + install_enabled: false + deploy_enabled: false + uninstall_enabled: true + codex: + npm_package: "@openai/codex" + install_enabled: true + deploy_enabled: true + uninstall_enabled: false + gemini_cli: + npm_package: "@google/gemini-cli" + install_enabled: true + deploy_enabled: true + uninstall_enabled: false + github_copilot: + npm_package: "@github/copilot" + install_enabled: true + deploy_enabled: true + uninstall_enabled: false + ibm_bob: + install_enabled: true + deploy_enabled: false + uninstall_enabled: false -ai_agents_enabled: true +ai_agents_deploy_enabled_names: >- + {{ ai_agents | dict2items | selectattr('value.deploy_enabled') | map(attribute='key') | list }} ai_agents_dotfiles: - name: AI common config src: .config/ai/ dest: .config/ai/ mode: preserve + agents: + - opencode + - codex + - gemini_cli - name: Gemini CLI config src: .gemini/ dest: .gemini/ mode: preserve + agents: + - gemini_cli - name: OpenCode config src: .config/opencode/ dest: .config/opencode/ mode: preserve + agents: + - opencode - name: Claude Code memory src: .claude/ dest: .claude/ mode: preserve + agents: + - claude_code ai_agents_templates: - name: Codex config src: .codex/config.toml.j2 dest: .codex/config.toml mode: "0644" + agents: + - codex diff --git a/ansible/inventory/group_vars/desktop.yml b/ansible/inventory/group_vars/desktop.yml index 372bb5b..c364eb3 100644 --- a/ansible/inventory/group_vars/desktop.yml +++ b/ansible/inventory/group_vars/desktop.yml @@ -31,8 +31,7 @@ desktop_binary_tools: [] desktop_npm_packages: >- {{ - ai_agents_npm_packages - + [ + [ {'name': '@mermaid-js/mermaid-cli', 'state': 'latest'}, {'name': 'vscode-langservers-extracted', 'state': 'latest'} ] diff --git a/ansible/inventory/group_vars/desktop_gnome.yml b/ansible/inventory/group_vars/desktop_gnome.yml index ea90ad9..3453ecb 100644 --- a/ansible/inventory/group_vars/desktop_gnome.yml +++ b/ansible/inventory/group_vars/desktop_gnome.yml @@ -85,5 +85,3 @@ fedora_flatpak_packages: - com.spotify.Client - org.onlyoffice.desktopeditors - org.telegram.desktop - -fedora_npm_packages: "{{ ai_agents_npm_packages }}" diff --git a/ansible/inventory/group_vars/server.yml b/ansible/inventory/group_vars/server.yml index 7a25183..cbdeada 100644 --- a/ansible/inventory/group_vars/server.yml +++ b/ansible/inventory/group_vars/server.yml @@ -6,7 +6,7 @@ effective_username: "{{ server_username }}" effective_user_group: "{{ server_user_group }}" effective_user_home: "{{ server_user_home }}" server_container_stack_dir: /opt/docker/server -ai_agents_enabled: false +ai_agents: {} vim_plugins_enabled: false profile_packages: diff --git a/ansible/inventory/group_vars/workstation.yml b/ansible/inventory/group_vars/workstation.yml index 1ab6b32..d5a8d07 100644 --- a/ansible/inventory/group_vars/workstation.yml +++ b/ansible/inventory/group_vars/workstation.yml @@ -1,5 +1,3 @@ --- -workstation_manage_opencode: true workstation_manage_ibm_bob: true workstation_ibm_bob_install_url: "https://bob.ibm.com/download/bobshell.sh" -workstation_npm_packages: "{{ ai_agents_npm_packages + [] }}" diff --git a/ansible/roles/dotfiles_common/tasks/main.yml b/ansible/roles/dotfiles_common/tasks/main.yml index abb4c6e..8e4af38 100644 --- a/ansible/roles/dotfiles_common/tasks/main.yml +++ b/ansible/roles/dotfiles_common/tasks/main.yml @@ -86,13 +86,25 @@ group: "{{ effective_user_group }}" mode: "{{ item.mode }}" loop: >- - {{ - (common_dotfiles | default([])) - + ((ai_agents_dotfiles | default([])) if (ai_agents_enabled | default(false)) else []) - }} + {{ common_dotfiles | default([]) }} loop_control: label: "{{ item.dest }}" +- name: Deploy AI coding-agent dotfiles + tags: [dotfiles, dotfiles:common, ai_agents] + ansible.builtin.copy: + src: "{{ playbook_dir }}/../dotfiles/common/{{ item.src }}" + dest: "{{ effective_user_home }}/{{ item.dest }}" + owner: "{{ effective_username }}" + group: "{{ effective_user_group }}" + mode: "{{ item.mode }}" + loop: "{{ ai_agents_dotfiles | default([]) }}" + loop_control: + label: "{{ item.dest }}" + when: + - (item.agents | intersect(ai_agents_deploy_enabled_names)) | length > 0 + - (ai_agents_dotfiles | default([])) | length > 0 + - name: Install distro packages for Vim plugins tags: [packages, vim, fzf] ansible.builtin.package: @@ -141,7 +153,7 @@ - (vim_plugin_source_plugins | default([])) | length > 0 - name: Ensure AI config directories exist - tags: [dotfiles, dotfiles:common] + tags: [dotfiles, dotfiles:common, ai_agents] ansible.builtin.file: path: "{{ effective_user_home }}/{{ item }}" state: directory @@ -151,11 +163,11 @@ loop: - .codex when: - - ai_agents_enabled | default(false) + - "'codex' in ai_agents_deploy_enabled_names" - (ai_agents_templates | default([])) | length > 0 -- name: Render AI agent templates - tags: [dotfiles, dotfiles:common] +- name: Render AI coding-agent templates + tags: [dotfiles, dotfiles:common, ai_agents] ansible.builtin.template: src: "{{ playbook_dir }}/../dotfiles/common/{{ item.src }}" dest: "{{ effective_user_home }}/{{ item.dest }}" @@ -165,7 +177,9 @@ loop: "{{ ai_agents_templates | default([]) }}" loop_control: label: "{{ item.dest }}" - when: ai_agents_enabled | default(false) + when: + - (item.agents | intersect(ai_agents_deploy_enabled_names)) | length > 0 + - (ai_agents_templates | default([])) | length > 0 - name: Refresh bat cache tags: [dotfiles, dotfiles:common] diff --git a/ansible/roles/packages_fedora/tasks/main.yml b/ansible/roles/packages_fedora/tasks/main.yml index 783c057..7314f12 100644 --- a/ansible/roles/packages_fedora/tasks/main.yml +++ b/ansible/roles/packages_fedora/tasks/main.yml @@ -90,16 +90,31 @@ - "'workstation_dev_fedora' in group_names" - (fedora_docker_packages | default([])) | length > 0 -- name: Install Fedora npm packages +- name: Install shared AI coding agents on Fedora tags: [packages, npm, ai_agents] community.general.npm: - name: "{{ item.name }}" + name: "{{ item.value.npm_package }}" global: true - state: "{{ item.state | default('present') }}" - loop: "{{ fedora_npm_packages | default([]) }}" - when: (fedora_npm_packages | default([])) | length > 0 + state: latest + loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}" + when: + - item.value.install_enabled | bool + - "'workstation_dev_fedora' not in group_names" loop_control: - label: "{{ item.name }}" + label: "{{ item.key }}" + +- name: Uninstall shared AI coding agents on Fedora + tags: [packages, npm, ai_agents] + community.general.npm: + name: "{{ item.value.npm_package }}" + global: true + state: absent + loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}" + when: + - item.value.uninstall_enabled | bool + - "'workstation_dev_fedora' not in group_names" + loop_control: + label: "{{ item.key }}" - name: Define Fedora Flatpak applications tags: [packages] diff --git a/ansible/roles/profile_desktop_common/tasks/main.yml b/ansible/roles/profile_desktop_common/tasks/main.yml index 150d99e..b4b0654 100644 --- a/ansible/roles/profile_desktop_common/tasks/main.yml +++ b/ansible/roles/profile_desktop_common/tasks/main.yml @@ -556,6 +556,30 @@ loop_control: label: "{{ item.name }}" +- name: Install shared AI coding agents on Void desktop + tags: [packages, npm, ai_agents] + community.general.npm: + name: "{{ item.value.npm_package }}" + global: true + state: latest + loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}" + when: + - item.value.install_enabled | bool + loop_control: + label: "{{ item.key }}" + +- name: Uninstall shared AI coding agents on Void desktop + tags: [packages, npm, ai_agents] + community.general.npm: + name: "{{ item.value.npm_package }}" + global: true + state: absent + loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}" + when: + - item.value.uninstall_enabled | bool + loop_control: + label: "{{ item.key }}" + - name: Install desktop Python tools with uv tags: [packages] ansible.builtin.command: diff --git a/ansible/roles/profile_workstation_dev_common/tasks/main.yml b/ansible/roles/profile_workstation_dev_common/tasks/main.yml index b8852fa..1282e8b 100644 --- a/ansible/roles/profile_workstation_dev_common/tasks/main.yml +++ b/ansible/roles/profile_workstation_dev_common/tasks/main.yml @@ -35,19 +35,31 @@ loop_control: label: "{{ item.dest }}" -- name: Install workstation npm packages - tags: [packages, npm] +- name: Install shared AI coding agents on workstation + tags: [packages, npm, ai_agents] community.general.npm: - name: "{{ item.name }}" + name: "{{ item.value.npm_package }}" global: true - state: "{{ item.state | default('present') }}" + state: latest become: true - loop: "{{ workstation_npm_packages | default([]) }}" + loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}" when: - - workstation_manage_opencode | default(false) - - workstation_npm_packages | length > 0 + - item.value.install_enabled | bool loop_control: - label: "{{ item.name }}" + label: "{{ item.key }}" + +- name: Uninstall shared AI coding agents on workstation + tags: [packages, npm, ai_agents] + community.general.npm: + name: "{{ item.value.npm_package }}" + global: true + state: absent + become: true + loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}" + when: + - item.value.uninstall_enabled | bool + loop_control: + label: "{{ item.key }}" - name: Install IBM Bob coding agent tags: [packages, ai_agents] @@ -59,4 +71,15 @@ become: true when: - workstation_manage_ibm_bob | default(false) + - ai_agents.ibm_bob.install_enabled | bool - workstation_ibm_bob_install_url | length > 0 + +- name: Uninstall IBM Bob coding agent binary + tags: [packages, ai_agents] + ansible.builtin.file: + path: /usr/local/bin/bob + state: absent + become: true + when: + - workstation_manage_ibm_bob | default(false) + - ai_agents.ibm_bob.uninstall_enabled | bool diff --git a/ansible/site.yml b/ansible/site.yml index 4a1bd66..267b0e2 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -19,6 +19,18 @@ lookup('ansible.builtin.fileglob', playbook_dir + '/../secrets/vault.local.yml', errors='ignore', wantlist=True) | length > 0 + - name: Reject conflicting AI coding-agent lifecycle flags + tags: [always, ai_agents] + ansible.builtin.assert: + that: + - not (item.value.install_enabled | bool and item.value.uninstall_enabled | bool) + fail_msg: >- + {{ item.key }} has both install_enabled and uninstall_enabled set to true. + Choose either installation or removal before running the playbook. + loop: "{{ ai_agents | dict2items }}" + loop_control: + label: "{{ item.key }}" + roles: - role: dotfiles_common when: "'platform_rocky' not in group_names"