diff --git a/README.md b/README.md index a233273..f41eb10 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ flowchart TD | `vpn-clients` | OpenVPN 3, WireGuard, Tunnelblick (macOS) | | `vm` | VM guest optimisations (QEMU/SPICE agents) | | `power-profile` | Sleep/idle/lid policy. `always-on` (default) or `vm`, via `-e power_profile=` | +| `zram` | Compressed-RAM swap so cgroup memory limits throttle instead of stalling. Pairs with the Rust build governor | | `arcane` | Arcane container UI, localhost-only. Off unless `-e soe_arcane_enabled=true`. Add `-e soe_arcane_long_session=true` for a year-long login | | `local-services` | Persistent local ClickHouse + Redpanda for spikes. Off unless `-e soe_local_services_enabled=true` | @@ -158,8 +159,21 @@ That matters most on a fleet machine reached over SSH as a service account, because that account's home is not the desktop. Get it wrong and the run still reports success -- the settings simply land where nobody sees them. +Settings that are true of one machine rather than of the repo -- a dedicated +cache volume, say -- go in `local-config/vars.yml` at the repo root, which is +gitignored and loaded on every run, tagged or not. A setting passed once as +`-e` and never written down is lost at the next converge: a box with a 512G +cache volume had its Rust build pool silently moved onto the root filesystem +that way. Anything in that file runs with the playbook's own authority -- root, +under `become` -- so treat it exactly as you would the playbook. + ## What Gets Installed +The summary below is the shape of it. [docs/install-matrix.md](docs/install-matrix.md) +is the single source of truth for the detail: every role, group, persona and +tool, which tag selects it, how each one is fetched, and which of them verify a +digest. Read that before changing a role or adding a tool. + **Default** (`./install.sh`) - a lightweight generic CLI dev base, nothing HyperI-specific: - Docker (Engine on Linux, CLI-only via Homebrew on macOS, no Docker Desktop, bring your own daemon) @@ -174,7 +188,9 @@ reports success -- the settings simply land where nobody sees them. - `infrastructure`: OpenTofu + OpenBao (the OSS forks, no HashiCorp BUSL tools), AWS CLI v2, checkov, and terraform-docs for generating IaC module reference docs (engine-agnostic -- it reads `.tf` whichever binary runs it, and OpenTofu has no native `tofu docs`). Under `k8s`: kubectl + kubectx + kubens + k9s + kind + argocd + dive + kustomize + kubeconform + kube-linter. helm is NOT in that group -- it sits in `cloud`, so plain `--tags infrastructure` gets it whether or not you select `k8s`. The `data` group: clickhouse-client, rpk, valkey-cli, vector. The `cloudflare` group: flarectl + wrangler (flarectl builds from source on both platforms -- Cloudflare ships no binary -- so Linux needs `developer-go`) - `contributor`: hyperi-ci and the tools its checks drive (semgrep, alint), gitleaks, trivy, hadolint, pip-audit, ansible-lint, pre-commit, act, and git-scrub for rewriting AI residue or a leaked secret out of git history -- gitleaks scans FULL history, so a secret removed from HEAD still fails the gate. macbash is here too: it flags the GNU-only bash constructs that break on macOS, and CONTRIBUTING.md asks for it on every shell change - `soe` / `soe-gui`: HyperI org policy: VPN clients, Claude Code, Slack, LibreOffice, RDP client, telemetry-disable, auto-updates, GNOME taskbar +- `developer-ai` (off by default): the OpenAI Codex CLI as a second opinion alongside Claude Code rather than a replacement for it, plus OpenAI's Codex plugin FOR Claude Code, so `/codex:review` and `/codex:adversarial-review` are things Claude asks Codex for. The plugin is skipped -- with a warning naming the tag that fixes it -- unless claude, codex and a new enough node are all present for that user, because it installs happily without them and then throws on every invocation. Sign-in stays the person's: `codex login --device-auth` on a box with no browser - `power-profile` (off by default, and deliberately not in `soe`): sleep, idle and lid policy, selected per machine. `always-on` (the default profile) never idle-suspends on mains power and does not sleep when the lid shuts -- for a repurposed laptop doing build work, or a desktop that has to answer ssh. `vm` never sleeps or suspends at all, for an unattended RDP guest that nobody can walk over and wake. Battery behaviour stays stock under `always-on`, because a machine that will not sleep in a bag cooks itself. Profiles are data files, so adding one is adding a file -- see [roles/power-profile/README.md](ansible/roles/power-profile/README.md) +- `zram` (off by default, and deliberately not in `soe`): a small compressed-RAM swap device, sized from the host's RAM and capped at 8 GiB. It exists because cgroup `MemoryHigh` throttles by reclaim, and on a swapless host the only reclaimable memory is page cache -- so a build past its budget stalls rather than slows. Pairs with the Rust build governor, which warns at converge time when it lands on a swapless host. Never restarts a running device, so a size change waits for a reboot -- see [roles/zram_swap/README.md](ansible/roles/zram_swap/README.md) - `arcane` (off by default): [Arcane](https://getarcane.app), a web UI for the containers on the box. Enable it with `-e soe_arcane_enabled=true` and you get a daemon on `http://localhost:3552` that comes back after a reboot and keeps itself updated. Works against docker-ce on Linux and colima on macOS. Bound to loopback because it holds the Docker socket, so whatever reaches that port owns the machine. Login is whatever Arcane seeds -- `arcane` / `arcane-admin` as upstream documents it. The role sets neither, and only clears the forced first-login password prompt, which it does by re-submitting that seeded password so the credentials stay unchanged. That needs the password policy relaxed to `basic` (`soe_arcane_password_policy`), because upstream's default `strong` policy rejects its own seeded password. There is still a login -- auto-login sits behind a `buildables` Go build tag that no published image is compiled with, so zero-auth is not available without building your own image. The login lasts about a day by default, which is a prompt every morning on a dev box -- `-e soe_arcane_long_session=true` stretches it to a year from each login. It is off by default because Arcane holds the Docker socket, and it takes one log-out and log-in to take effect, since the session expiry is stamped at login - `local-services` (off by default): a persistent local ClickHouse and Redpanda for ad-hoc work -- somewhere to poke at a query or hand-feed a topic without waiting for a suite to build. Enable with `-e soe_local_services_enabled=true`. Deployed **stopped**: `restart: no`, so a reboot leaves them down and they cost nothing until `local-services up`, which pulls latest and takes seconds. Both capped at 1GB and bound to loopback. They are spike instances -- integration and e2e suites create and tear down their own containers, because a shared daemon makes a suite non-hermetic and order-dependent diff --git a/ansible/molecule/existing-host/molecule.yml b/ansible/molecule/existing-host/molecule.yml index d4aedb0..f87b771 100644 --- a/ansible/molecule/existing-host/molecule.yml +++ b/ansible/molecule/existing-host/molecule.yml @@ -58,7 +58,7 @@ provisioner: hosts: inventory.yml # The version SSoT. Without it any task reading hyperi_core_versions # (the rustup pin, node_major) is undefined under this scenario. - group_vars: ${MOLECULE_PROJECT_DIRECTORY}/inventories/localhost/group_vars + group_vars: ${MOLECULE_PROJECT_DIRECTORY}/playbooks/group_vars scenario: name: existing-host diff --git a/ansible/inventories/localhost/group_vars/all.yml b/ansible/playbooks/group_vars/all.yml similarity index 100% rename from ansible/inventories/localhost/group_vars/all.yml rename to ansible/playbooks/group_vars/all.yml diff --git a/ansible/playbooks/main.yml b/ansible/playbooks/main.yml index b6baac0..ad365c7 100644 --- a/ansible/playbooks/main.yml +++ b/ansible/playbooks/main.yml @@ -13,6 +13,22 @@ gather_facts: true pre_tasks: + # Per-machine settings that cannot live in the repo, because the repo is + # cloned onto every machine. `local-config/` is already gitignored. + # + # This exists because a host setting with no home is silently lost: a box + # with a dedicated cache volume that passed `-e rust_cache_root=/cache` once + # gets the DEFAULT on the next converge, and its build pool moves off the + # volume it was put on without a word. + # `always`, because a tagged run needs these too -- and a tagged run is + # exactly when a missing host setting does its damage, since a targeted + # re-converge is the common case. + - name: Load per-machine overrides if this host has any + ansible.builtin.include_vars: + file: "{{ item }}" + loop: "{{ q('first_found', [playbook_dir ~ '/../../local-config/vars.yml'], errors='ignore') }}" + tags: ['always'] + - name: Display system information ansible.builtin.debug: msg: | @@ -286,6 +302,18 @@ become: "{{ ansible_facts['distribution'] != 'MacOSX' }}" tags: ['soe-gui', 'never'] + # AI coding-agent tooling (opt-in): the Codex CLI as a second opinion + # alongside Claude Code, plus OpenAI's Codex plugin FOR Claude Code. + # + # AFTER soe ON PURPOSE. Claude Code is a hard dependency of the Codex + # plugin and is installed by soe/tasks/claude.yml, so a single + # `--tags claude,codex-plugin` run has to reach that task first. Listed + # earlier, the plugin's dependency probe would run before Claude Code + # existed and skip the plugin the same run that installed its dependency. + - role: developer-ai + become: "{{ ansible_facts['distribution'] != 'MacOSX' }}" + tags: ['developer-ai', 'never'] + # macOS modern bash (opt-in, macOS-only). - role: bash-modern tags: ['bash-modern', 'never'] @@ -306,6 +334,15 @@ become: false tags: ['power-profile', 'never'] + # zram swap, so cgroup memory limits throttle instead of stalling. Opt-in + # and per-machine like power-profile: it is a host-wide memory policy, and + # a box that already has disk swap wants different numbers. Pairs with + # developer-rust's build governor, whose MemoryHigh has nothing to reclaim + # on a swapless host. + - role: zram_swap + become: "{{ ansible_facts['distribution'] != 'MacOSX' }}" + tags: ['zram', 'never'] + # System cleanup — always runs at the end. - role: system_cleanup become: "{{ ansible_facts['distribution'] != 'MacOSX' }}" diff --git a/ansible/requirements.yml b/ansible/requirements.yml index 4920387..2408bc7 100644 --- a/ansible/requirements.yml +++ b/ansible/requirements.yml @@ -19,5 +19,11 @@ collections: # Required by the molecule docker driver, not by any role. - name: community.docker version: ">=5.0.0" + + # tests/proxmox/create.yml and reset.yml. The proxmox_* modules left + # community.general for this collection and are removed from it at 15.0.0, + # which the floor above will reach. + - name: community.proxmox + version: ">=1.0.0" - name: ansible.posix version: ">=2.0.0" diff --git a/ansible/roles/developer-ai/defaults/main.yml b/ansible/roles/developer-ai/defaults/main.yml new file mode 100644 index 0000000..e6c1d52 --- /dev/null +++ b/ansible/roles/developer-ai/defaults/main.yml @@ -0,0 +1,71 @@ +--- +# ============================================================================ +# SHARED ENVIRONMENT +# ============================================================================ +# One PATH for every probe and every install in this role, covering both +# platforms so the tasks do not each carry their own copy. +# +# `{{ user_home }}/.local/bin` is the load-bearing entry and the reason this is +# not just the inherited PATH: claude, codex and uv-installed tools all land +# there, PER USER. `ansible_facts['env'].PATH` is the CONNECTING user's, which +# on a fleet machine is a service account that has none of them -- it is +# appended for the system binaries, never relied on for the agent CLIs. +developer_ai_env: + PATH: >- + /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:{{ + user_home }}/.local/bin:{{ ansible_facts['env'].PATH }} + +# ============================================================================ +# CODEX CLI +# ============================================================================ +# The official installer, which is the same route `learn.chatgpt.com/docs/codex/cli` +# documents. chatgpt.com/codex/install.sh is a 302 to releases.openai.com; the +# redirect is followed rather than hardcoded so a move upstream does not need a +# change here. +# +# Why the script rather than a get_url of the release asset: the asset is not a +# bare binary. `codex-package-.tar.gz` is a ~120 MB tree that has to be +# staged into $CODEX_HOME/packages/standalone/-/ and symlinked +# twice, and the script verifies its SHA256 against the published +# codex-package_SHA256SUMS before it does so. Reimplementing that here would buy +# nothing on integrity and would leave us owning a layout upstream versions. +# +# The thin `codex-.tar.gz` assets are NOT in codex-package_SHA256SUMS -- +# they have no published digest at all. That is the trap to avoid: they are the +# obvious thing to reach for and the only unverifiable option. +developer_ai_codex_installer_url: https://chatgpt.com/codex/install.sh + +# No version pin, per the repo's one install mode: every tool resolves its +# version when the installer runs (docs/install-matrix.md). Codex ships several +# stable releases a WEEK, so this moves faster than most -- `CODEX_RELEASE` is +# the upstream knob if a host ever needs to be held back, and it belongs in +# local-config/vars.yml on that host rather than here. +developer_ai_codex_release: "" + +# ============================================================================ +# CODEX PLUGIN FOR CLAUDE CODE +# ============================================================================ +# Names come from the repo's .claude-plugin/marketplace.json: the marketplace is +# `openai-codex` and the plugin inside it is `codex`. Both are needed -- the +# install id is plugin@marketplace. +developer_ai_codex_plugin_marketplace: openai-codex +developer_ai_codex_plugin_repo: openai/codex-plugin-cc +developer_ai_codex_plugin_name: codex + +# User scope, so the plugin follows the developer rather than a checkout. This +# is also `claude plugin install`'s own default; stated explicitly because the +# whole point of this role is that user scope means the TARGET user, not +# whoever Ansible connected as. +developer_ai_codex_plugin_scope: user + +# The plugin's hooks are `node