feat: manage coding agents independently

This commit is contained in:
Fabio Scotto di Santolo
2026-08-31 10:02:32 +02:00
parent 7f6a45f614
commit 671581d717
12 changed files with 204 additions and 46 deletions

View File

@@ -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 <host> --tags emacs --check --diff -e emacs_enabled=true`
- AI coding agents: `ansible-playbook ansible/site.yml --limit <host> --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.<agent>` 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

View File

@@ -165,6 +165,43 @@ Emacs is enabled on Fedora/GNOME and workstation profiles. `dotfiles_common` dep
ansible-playbook ansible/site.yml --limit <host> --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. |

View File

@@ -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

View File

@@ -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'}
]

View File

@@ -85,5 +85,3 @@ fedora_flatpak_packages:
- com.spotify.Client
- org.onlyoffice.desktopeditors
- org.telegram.desktop
fedora_npm_packages: "{{ ai_agents_npm_packages }}"

View File

@@ -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:

View File

@@ -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 + [] }}"

View File

@@ -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]

View File

@@ -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]

View File

@@ -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:

View File

@@ -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

View File

@@ -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"