mirror of
https://github.com/fscotto/infra.git
synced 2026-09-27 19:03:47 +00:00
18 KiB
18 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.j2somodel_instructions_filepoints 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:
deadalusis Windows + Fedora WSL. - Rocky server:
prometheusbelongs torocky_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.ymlover hostname assumptions. - Inventory axes are independent:
platform_*,role_*, anddesktop_*. Legacyvoidanddesktopremain compatibility parents.
Working Rules
- Preserve layering
all -> platform -> role -> desktop -> host. - Keep
ansible/site.ymlsmall; 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, andsupport/for maintained release lines. Do not use abbreviated prefixes such asfeat/. - 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.ymlansible-lint ansible/rolesyamllint 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
- Fedora desktop work:
- 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.shandshellcheck scripts/bootstrap_mail.sh - Server compose render:
podman-compose -f /opt/docker/server/docker-compose.yml configandsystemctl status podman-compose-server - Atlas media stack:
ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff - Prometheus media mount:
ansible-playbook ansible/site.yml --limit prometheus --tags rclone,navidrome --check --diff - DuckDNS config only:
ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff
- Emacs is disabled by default; temporary Emacs check:
Conventions
- Use FQCN Ansible modules.
- Prefer declarative modules over
command/shell; whenshellis 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 sharedgroup_vars. - Use
no_log: truefor secret-bearing task inputs or outputs.
Desktop Notes
desktop_profilenames independently selectable desktop groups such asdesktop_gnome,desktop_sway, anddesktop_niri. Keep platform-specific session bootstrap in platform-specific roles.desktop_environmentis fixed tominimalfor Void desktops.profile_desktop_commonowns shared Void bootstrap;profile_desktop_swayandprofile_desktop_nirimanage the enabled sessions, whileprofile_desktop_gnomecopies shared desktop dotfiles for Fedora/GNOME without managing GNOME settings.desktop_sessions_enabledanddesktop_default_sessionapply to the minimal mode.- Emacs has one authoring-oriented
.emacs.d, deployed bydotfiles_commonwhenemacs_enabledis 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-3ginansible/inventory/group_vars/void.yml. - Void user services are managed by
turnstileand live underdotfiles/desktop/.config/service/. ssh-agentkeeps the stable socket~/.local/state/ssh-agent/socket.- Critical session entrypoints:
dotfiles/desktop/.config/sway/configplushost.confandsession-envdeployed viahost_sway_dotfiles(sway / Wayland)dotfiles/desktop/.config/niri/config.kdlandsession-envdeployed viadesktop_niri_dotfiles(Niri / Wayland)
- Void Niri lives in
profile_desktop_niri, gated on'niri' in desktop_sessions_enabled; it installs theempttyniri.desktopsession, the/usr/local/bin/start-nirilauncher, and the xdg-desktop-portal config, mirroringprofile_desktop_sway. - Fedora GNOME (
desktop_gnome) assumes GNOME comes from the Fedora Workstation base install; Ansible deploys shared desktop dotfiles and git/GPG config forikarosandnymph, not GNOME settings. - Do not switch or restart the display manager during a playbook run from an active graphical session.
nymphis 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 theempttydisplay manager.desktop_sway_packages— binaries specific to the Sway session.profile_packagesremains the shared package bucket for Void and Fedora profiles. Rocky usesrocky_profile_packagesso 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_dotfilescarries mode-independent content anddesktop_minimal_dotfilescarries Thunar, Udiskie, and MIME defaults.desktop_void_dotfilesremains reserved for files that need the Void runtime.
Workstation Notes
deadalusis 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/miseCOPR 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
- DuckDNS is rendered by
profile_serverfrom host-localserver_duckdns_domainandvault_duckdns_token. Keep the rotated token in encrypted Vault or untracked local vars, never in dotfiles. The private~/duckdns/duck.shkeeps the existing entrypoint; rendering usesno_logand disables diffs. Provisioning does not execute the updater or change its external schedule. rocky_serveris a child of bothplatform_rockyandserver;prometheusis its active target.- The target must already provide
server_usernamewith local sudo access before the profile runs. - The Rocky profile installs Podman and podman-compose, uses firewalld, preserves SELinux enforcement, and renders the
Nginx Proxy Manager/Gitea/Navidrome-PostgreSQL Compose stack with a
podman-compose-serversystemd unit. It does not start or enable that Compose stack, transfer data, update DNS, or cut over traffic; activating it remains manual. - Prometheus has a gated system
rclone-music.serviceand rootless Navidrome Quadlet. They remain disabled until the Atlas WireGuard address, pinned SSH host key and Vault-backed SFTP private key are configured. The rclone mount is read-only at/mnt/music_atlas; Navidrome must not start against the underlying empty mountpoint or while the legacy rootful Navidrome container is still running. The role never removes that legacy container or its data. - Firewalld enables SSH, Cockpit (
9090/tcp), HTTP and HTTPS. Nginx Proxy Manager publishes80/tcpand443/tcp; bind its administration interface only to127.0.0.1:81and usenpm-tunnelfrom Ikaros or Nymph. Nextcloud remains disabled; do not provision/srv/nextclouddirectories. scripts/migrate_prometheus_data.shis 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, and Syncthing stay selected through Atlas host variables and must not
leak into
rocky_server. Cockpit plus its Navigator and Podman extensions are selected explicitly for Prometheus through its host variables.
Atlas NAS Notes
atlasis a remote Rocky Linux 9 NAS. Keep its connection, LAN, pool and mountpoint values inhost_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,atlas_manage_firewall, andatlas_manage_media_stackremain false until their placeholders and Vault inputs are replaced; only then may the profile manage datasets, shares, LAN-restricted firewall rules, and rootful media Quadlets.- Atlas requires
vault_atlas_authorized_ssh_keys,vault_atlas_admin_password_hashfor Cockpit and, when the relevant gates are enabled,vault_atlas_samba_passwordandvault_atlas_immich_db_password. Never print these values. - Atlas creates
archive,media/music,media/icloud_photos, andbackups/servicesonly under the verified pre-existing pool;backups/serviceshas a500Grefreservation. Existing Work, Syncthing, and Prometheus-backup datasets remain managed and separate. - The
immichsystem account is fixed to UID/GID1100, has no login shell orwheelmembership, and receives only thevideoandrendersupplementary groups. Immich's rootful Quadlets run as1100:1100; Server and ML receive/dev/dri, while the iCloud Photos external library is read-only. - Atlas exports iCloud Photos only to the configured Aegis IP with all access squashed to UID/GID
1100. SMB3 exposesArchiveto Vault-backed authorized accounts and admits the configured LAN without host-specific exclusions. - Atlas NPM and Immich share a rootful Podman network. NPM publishes HTTP/HTTPS, but its administration port remains
bound to
127.0.0.1:81; do not expose it directly to the LAN or Internet.
Atlas NAS TODO
- Replace every Atlas
CHANGEMEvalue, provide the required Vault variables and validate the first remote bootstrap on the real Rocky Linux 9 host. Enableatlas_manage_storagefirst andatlas_manage_firewallonly after confirming the pool, mountpoints, LAN subnet and firewalld zone. Enableatlas_manage_media_stacklast, after validating/dev/dri, the container paths and the Immich database secret. - 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.
- Validate the existing WireGuard path and add its 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_agentsinansible/inventory/group_vars/all.yml. - Shared agent dotfiles live in
ai_agents_dotfiles; rendered configs live inai_agents_templates. - Every
ai_agents.<agent>entry has independentinstall_enabled,deploy_enabled, anduninstall_enabledflags. 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_commondeploysai_agents_dotfilesand rendersai_agents_templatesonly 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-pyansible-galaxy collection install -r ansible/collections/requirements.yml
- Required collections currently include
ansible.posixandcommunity.general. .yamllinttreatsline-lengthas a warning at 120 chars and disablesdocument-startandcomments-indentation.
When Updating Docs
- Keep
README.mdandAGENTS.mdaligned 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
aegisis a remote Fedora IoT Raspberry Pi 4 node. Bootstrap it once withansible/bootstrap/aegis.bu; the remaining configuration is applied byprofile_aegisover SSH.- Fedora IoT is immutable. Do not add it to mutable Fedora package or shared dotfile roles.
profile_aegisowns rootful Podman Quadlets, persistent container state under/var/lib, the Podman auto-update timer, LAN-restricted firewalld rules, and SSH hardening. Keepaegis_lan_subnetandaegis_adguard_web_porthost-specific; SSH permits only the declared key-authenticated users, never root or password authentication. Keep Apple IDs and other credentials in Vault and useno_logfor their rendering.aegis_adguard_web_portdefaults to80. The initial AdGuard Home wizard port3000is intentionally unmanaged: open and close it manually only while completing initial setup. Disable the local systemd-resolved stub throughprofile_aegisbefore AdGuard binds port 53; keep/etc/resolv.conflinked to/run/systemd/resolve/resolv.confso Aegis retains router-provided DNS.- iCloudPD requires post-deployment interactive MFA initialization; its cookie/configuration state is
persisted in
/var/lib/icloudpd/config.