Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<name>` |
| `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` |

Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion ansible/molecule/existing-host/molecule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions ansible/playbooks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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']
Expand All @@ -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' }}"
Expand Down
6 changes: 6 additions & 0 deletions ansible/requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
71 changes: 71 additions & 0 deletions ansible/roles/developer-ai/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -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-<target>.tar.gz` is a ~120 MB tree that has to be
# staged into $CODEX_HOME/packages/standalone/<version>-<target>/ 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-<target>.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 <script>.mjs`, invoked directly, so node is
# required even when Codex itself was installed as a standalone binary with no
# npm involved. Upstream's README asks for 18.18 or later; the npm package's own
# `engines` says >=16, which is the looser and wrong number to size from.
#
# The gate compares the MAJOR only, so 18.0 through 18.17 would pass a check
# upstream would fail. Accepted rather than parsed further: the whole 18 line
# went end-of-life in April 2025, so a host running one has a bigger problem
# than this plugin, and a minor-level comparison here would be precision about
# a version nobody should be on.
developer_ai_node_min_major: 18
35 changes: 35 additions & 0 deletions ansible/roles/developer-ai/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
galaxy_info:
author: HyperI
description: >-
Opt-in AI coding-agent tooling: the OpenAI Codex CLI as a second opinion
alongside Claude Code, plus OpenAI's Codex plugin for Claude Code.
company: HyperI
license: Apache-2.0
min_ansible_version: "2.20"
platforms:
- name: Fedora
versions: ['43', '44']
- name: Ubuntu
versions: ['noble', 'resolute']

# NO meta dependencies, deliberately, and the reason is worth stating because
# the obvious wiring is wrong twice over.
#
# 1. Claude Code is a hard dependency of the Codex plugin, and the only role
# that installs it today is `soe`. Depending on soe would drag HyperI ORG
# POLICY -- VPN clients, telemetry-disable, branding, managed settings --
# onto a machine that asked for a code-review tool. roles/soe/tasks/main.yml
# is explicit that soe holds only "what we would have no business putting on
# anyone else's machine", so pulling it from here would break that contract.
#
# 2. Node is a hard dependency of the plugin's hooks, and it does NOT come from
# `developer-node` -- that role installs eslint and prettier. Node itself is
# in the `developer` base. So a dependency on developer-node would look
# right, satisfy the linter, and supply nothing.
#
# The dependencies are therefore PROBED on the target and reported by name --
# see tasks/init.yml. That is also the honest shape for a host of unknown
# state: the answer to "is Claude Code here" is per-USER on Linux (it installs
# to ~/.local/bin/claude), so no role graph could answer it anyway.
dependencies: []
Loading
Loading