Files
infra/AGENTS.md
Fabio Scotto di Santolo eab66b6d3d Add dormant Rocky server profile (#2)
* Add dormant Rocky server profile

* feat: activate Rocky server profile for prometheus

* fix: complete Rocky server migration tooling

* Remove FreeBSD profile

* feat: add Aegis Fedora CoreOS profile

* docs: define Git Flow branch prefixes

* Add Aegis TPM-backed btrfs storage

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Preserve existing Aegis filesystem tables

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Remove unsupported Aegis TPM storage

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Configure Aegis pi Btrfs storage

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Mount Aegis Btrfs as root

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Handle Maven on Mise

* Feature/aegis coreos (#7)

* feat(aegis): add ARM image bootstrap workflow

* feat(aegis): manage firewall and SSH hardening

* feat(aegis): harden Fedora IoT services

* chore(aegis): remove local TLS configuration

* fix(aegis): restore supported Fedora IoT bootstrap

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>
2026-09-08 10:36:29 +02:00

15 KiB

AGENTS.md

Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora IoT, WSL, a Rocky Linux 9 server, and an Atlas NAS.

Source Of Truth

  • Main orchestration: ansible/site.yml
  • Inventory and layering inputs: ansible/inventory/hosts.yml, ansible/inventory/group_vars/*.yml, ansible/inventory/host_vars/*.yml
  • Dotfiles live under dotfiles/
  • AI agent instructions (bootstrap, rules, knowledge) are centralized in dotfiles/common/.config/ai/ and shared between OpenCode, Codex, and Gemini CLI.
  • OpenCode loads its entrypoint configuration from dotfiles/common/.config/opencode/opencode.json.
  • Codex config is rendered from dotfiles/common/.codex/config.toml.j2 so model_instructions_file points to the deployed ~/.config/ai/bootstrap.md.

Topology

  • Current personal desktop: ikaros = platform_fedora + role_personal_workstation + graphical_desktop + desktop_gnome
  • Current laptop: nymph = platform_fedora + graphical_desktop + desktop_gnome
  • Void desktop profile is also the base for other future/reference hosts via platform_void + graphical_desktop
  • Workstation: deadalus is Windows + Fedora WSL.
  • Rocky server: prometheus belongs to rocky_server.
  • NAS: atlas (Rocky Linux 9, reached through SSH)
  • Always-on LAN node: aegis (Fedora IoT on Raspberry Pi 4, reached through SSH)
  • Hosts intentionally belong to multiple groups; trust ansible/site.yml over hostname assumptions.
  • Inventory axes are independent: platform_*, role_*, and desktop_*. Legacy void and desktop remain compatibility parents.

Working Rules

  • Preserve layering all -> platform -> role -> desktop -> host.
  • Keep ansible/site.yml small; orchestration belongs there, implementation belongs in roles.
  • Prefer minimal, targeted edits. Preserve idempotency and existing ordering.
  • Use Git Flow branch prefixes: feature/ for new functionality, bugfix/ for non-urgent fixes, hotfix/ for urgent production fixes, release/ for release preparation, and support/ for maintained release lines. Do not use abbreviated prefixes such as feat/.
  • Desktop and WSL hosts use ansible_connection: local; remote infrastructure hosts use SSH.
  • Treat secrets/ as sensitive. Never print secret values.
  • Tmux plugins are bootstrapped by TPM on the host; the repo only keeps tmux config and custom helper scripts.
  • Read the relevant role tasks, templates, vars, and deployed dotfiles before editing.

Validation

  • Default minimum:
    • ansible-playbook ansible/site.yml --syntax-check
  • Repo-wide checks:
    • ansible-lint ansible/site.yml
    • ansible-lint ansible/roles
    • yamllint ansible/
  • Host-focused dry runs:
    • Fedora desktop work: ansible-playbook ansible/site.yml --limit ikaros --check --diff
    • Fedora laptop work: ansible-playbook ansible/site.yml --limit nymph --check --diff
    • WSL workstation dev: ansible-playbook ansible/site.yml --limit deadalus --check --diff
    • Server: ansible-playbook ansible/site.yml --limit prometheus --check --diff
    • Rocky server after activation: ansible-playbook ansible/site.yml --limit <host> --check --diff
    • Atlas NAS: ansible-playbook ansible/site.yml --limit atlas --check --diff
    • Aegis IoT: ansible-playbook ansible/site.yml --limit aegis --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

Conventions

  • Use FQCN Ansible modules.
  • Prefer declarative modules over command/shell; when shell is required, make idempotency and failure behavior explicit.
  • Start YAML files with ---, use 2-space indentation, and keep file modes quoted like "0644".
  • Keep booleans as booleans and structured vars as YAML lists/maps.
  • Put host-specific overrides in host_vars, not shared group_vars.
  • Use no_log: true for secret-bearing task inputs or outputs.

Desktop Notes

  • desktop_profile names independently selectable desktop groups such as desktop_gnome, desktop_sway, and desktop_niri. Keep platform-specific session bootstrap in platform-specific roles.
  • desktop_environment is fixed to minimal for Void desktops. profile_desktop_common owns shared Void bootstrap; profile_desktop_sway and profile_desktop_niri manage the enabled sessions, while profile_desktop_gnome copies shared desktop dotfiles for Fedora/GNOME without managing GNOME settings. desktop_sessions_enabled and desktop_default_session apply to the minimal mode.
  • Emacs has one authoring-oriented .emacs.d, deployed by dotfiles_common when emacs_enabled is true. Fedora/GNOME desktops and workstation profiles enable it; keep platform dependencies in package group vars rather than branching in Emacs Lisp.
  • NTFS filesystem support is provided by ntfs-3g in ansible/inventory/group_vars/void.yml.
  • Void user services are managed by turnstile and live under dotfiles/desktop/.config/service/.
  • ssh-agent keeps the stable socket ~/.local/state/ssh-agent/socket.
  • Critical session entrypoints:
    • dotfiles/desktop/.config/sway/config plus host.conf and session-env deployed via host_sway_dotfiles (sway / Wayland)
    • dotfiles/desktop/.config/niri/config.kdl and session-env deployed via desktop_niri_dotfiles (Niri / Wayland)
  • Void Niri lives in profile_desktop_niri, gated on 'niri' in desktop_sessions_enabled; it installs the emptty niri.desktop session, the /usr/local/bin/start-niri launcher, and the xdg-desktop-portal config, mirroring profile_desktop_sway.
  • Fedora GNOME (desktop_gnome) assumes GNOME comes from the Fedora Workstation base install; Ansible deploys shared desktop dotfiles and git/GPG config for ikaros and nymph, not GNOME settings.
  • Do not switch or restart the display manager during a playbook run from an active graphical session.
  • nymph is the Fedora/GNOME laptop target; keep GNOME settings unmanaged for now and add host-specific tuning only after real use.

Void Package And Dotfile Bucket Rules

platform_void is the reusable Void platform selection. The legacy void group remains a compatibility parent so existing group_vars/void.yml and when: "'void' in group_names" checks keep working during the transition. The Void desktop package lists in ansible/inventory/group_vars/void.yml are kept disjoint by role:

  • void_packages_base — system runtime only (init/services, kernel, audio core, networking, filesystem, firewall, hardware daemons, runit logging).
  • desktop_common_packages — GUI infrastructure shared by the minimal desktop mode.
  • desktop_minimal_packages — applications, integration components, and the emptty display manager.
  • desktop_sway_packages — binaries specific to the Sway session. profile_packages remains the shared package bucket for Void and Fedora profiles. Rocky uses rocky_profile_packages so RPM-specific names do not leak back into the other platforms; do not move desktop-specific Void entries through either bucket. The dotfile vars follow the same split: desktop_common_dotfiles carries mode-independent content and desktop_minimal_dotfiles carries Thunar, Udiskie, and MIME defaults. desktop_void_dotfiles remains reserved for files that need the Void runtime.

Workstation Notes

  • deadalus is modeled as Windows + Fedora WSL and is the sole workstation target.
  • Fedora WSL belongs to platform_fedora, workstation_dev_fedora, and the shared WSL layer. It must not receive Flatpak or Snap runtimes.
  • Fedora WSL installs Mise from the official jdxcode/mise COPR and uses its pinned Temurin Java 11 JDK; update the declared Mise version deliberately.
  • Windows applications are installed manually and are not managed from the WSL profile.

Rocky Server Notes

  • rocky_server is a child of both platform_rocky and server; prometheus is its active target.
  • The target must already provide server_username with local sudo access before the profile runs.
  • The Rocky profile installs Docker CE, uses firewalld, preserves SELinux enforcement, and renders the same server Compose stack. It does not transfer data, start containers, update DNS, or cut over traffic.
  • scripts/migrate_prometheus_data.sh is the separate, source-host-run migration path. It dry-runs by default and requires explicit source-stack quiescing before copying persistent Docker data with rsync.
  • Atlas-only OpenZFS, NFS, Samba, Cockpit, and Syncthing stay selected through Atlas host variables and must not leak into rocky_server.

Atlas NAS Notes

  • atlas is a remote Rocky Linux 9 NAS. Keep its connection, LAN, pool and mountpoint values in host_vars/atlas.yml. Bootstrap it once with -e atlas_connection_username=<existing-admin>; subsequent runs use the dedicated Atlas account.
  • The pool is pre-existing: never add pool creation, disk partitioning, RAIDZ creation, rollback, or destruction to the Atlas profile.
  • atlas_manage_storage and atlas_manage_firewall remain false until their placeholders are replaced; only then may the profile manage datasets, shares and LAN-restricted firewall rules.
  • Atlas requires vault_atlas_authorized_ssh_keys, vault_atlas_admin_password_hash for Cockpit and, when storage is enabled, vault_atlas_samba_password. Never print these values.
  • Atlas uses NFSv4 for Linux and SMB for Windows/WSL, restricted to the configured LAN. Snapshot, Borg/Hetzner offsite backup, Prometheus pull and USB backup automation are intentionally deferred.

Atlas NAS TODO

  • Replace every Atlas CHANGEME value, provide the required Vault variables and validate the first remote bootstrap on the real Rocky Linux 9 host. Enable atlas_manage_storage first and atlas_manage_firewall only after confirming the pool, mountpoints, LAN subnet and firewalld zone.
  • Validate the complete baseline on the target: OpenZFS kmod loading, existing pool import, dataset mounts, SSH reconnect, Cockpit and all selected 45Drives plugins, NFSv4, SMB and Syncthing.
  • Finalize dataset properties and the shared UID/GID, group and POSIX ACL model; test the same files through both NFS and SMB before considering multiprotocol access complete.
  • Add Ansible-managed ZFS snapshot retention and scrub timers. Use Cockpit Scheduler for visibility or manual operations, not as the only source of configuration, and never automate snapshot rollback.
  • Manage the Syncthing star topology, device IDs, folders, folder modes, ignore rules and protected GUI or API access for the selected clients.
  • Add Tailscale or WireGuard and corresponding LAN/VPN-only firewalld rules before enabling remote services; never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding.
  • Add the least-privilege Prometheus backup flow: remote dump generation, dedicated SSH identity, pinned host key, atomic pull, verification, retention and an Atlas systemd service/timer.
  • Add the encrypted offsite backup with Borg to a Hetzner Storage Box: use a dedicated SSH identity, pin the host key, keep Borg repository credentials and encryption material in Vault, use snapshot-consistent sources, and manage retries, logging, pruning, repository checks and restores.
  • Add the UUID-bound offline USB backup with versioned rsync, locking, capacity checks, verification, safe unmounting and a tested restore procedure; never trigger it for an arbitrary USB disk.
  • Add monitoring and alerting for pool health, scrub/resilver, SMART data, temperatures, free space and failed backup timers, plus a controlled Rocky kernel/OpenZFS update and reboot procedure.
  • Document and test disaster recovery: rebuild Atlas with Ansible, import the existing pool, restore from snapshot/USB/Hetzner, preserve Vault and Borg recovery material offline, and define RPO/RTO.
  • Optionally design iCloud photo ingestion as a separate workflow after the storage and backup layers are validated; do not make it a dependency of the Atlas baseline.

Coding Agent Notes

  • 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.
  • 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

  • Install local tooling with:
    • python3 -m pip install ansible ansible-lint yamllint shellcheck-py
    • ansible-galaxy collection install -r ansible/collections/requirements.yml
  • Required collections currently include ansible.posix and community.general.
  • .yamllint treats line-length as a warning at 120 chars and disables document-start and comments-indentation.

When Updating Docs

  • Keep README.md and AGENTS.md aligned when workflows materially change.
  • If you add a new operational area, also add the narrowest validation command for it.
  • Call out checks you could not run and any follow-up verification needed.

Aegis Fedora IoT Notes

  • aegis is a remote Fedora IoT Raspberry Pi 4 node. Bootstrap it once with ansible/bootstrap/aegis.bu; the remaining configuration is applied by profile_aegis over SSH.
  • Fedora IoT is immutable. Do not add it to mutable Fedora package or shared dotfile roles.
  • profile_aegis owns rootful Podman Quadlets, persistent container state under /var/lib, the Podman auto-update timer, LAN-restricted firewalld rules, and SSH hardening. Keep aegis_lan_subnet and aegis_adguard_web_port host-specific; SSH permits only the declared key-authenticated users, never root or password authentication. Keep Apple IDs and other credentials in Vault and use no_log for their rendering.
  • aegis_adguard_web_port defaults to 80. The initial AdGuard Home wizard port 3000 is intentionally unmanaged: open and close it manually only while completing initial setup. Disable the local systemd-resolved stub through profile_aegis before AdGuard binds port 53; keep /etc/resolv.conf linked to /run/systemd/resolve/resolv.conf so Aegis retains router-provided DNS.
  • iCloudPD requires post-deployment interactive MFA initialization; its cookie/configuration state is persisted in /var/lib/icloudpd/config.