fix(developer-ai): opt-in Codex agent tooling - #88
Merged
Conversation
The role hard-coded CARGO_HOME to ~/.cargo in cargo_env. On a host that relocates it, the role wrote a correct config.toml to a directory cargo never reads, and the stale file that stayed in effect named a rustc-wrapper which no longer existed. Every cargo build failed with "No such file or directory" while the converge reported success. Four occurrences were confirmed on the affected workstation, the most recent after a hand-patch had supposedly fixed it. CARGO_HOME and RUSTUP_HOME now come from the target user's own login shell, which is the environment their cargo runs in. ansible_facts.env cannot answer this - it is the connection user's environment, root under become. Output is sentinel-delimited because a login shell runs /etc/profile.d and ~/.profile first, and anything they print would otherwise land ahead of the answer. A post-condition at the end of the run reads BOTH candidate configs and fails the converge when either names a wrapper that does not resolve. Reading only the resolved one would let a wrong resolution certify itself. A config superseded by a relocated CARGO_HOME is renamed to config.toml.superseded rather than left as a trap. Also replace the global build mutex with a memory semaphore. One build at a time was correct on a 30 GB box and is the concurrency ceiling on a 246 GB one: twelve sessions queue while the machine idles, and a corpus gate parallelised from 102s to 18s spends the gain again the moment work is farmed out to agents. N = min(MemoryHigh / per-build allowance, pool/2), jobs = pool/N, both resolved at run time from the live host so a resized VM needs no re-converge. N=1 reproduces the old behaviour and is what a 32 GB host computes. Slots are one fd reopened across N lock files, so slot choice is which file is opened - no eval and no bash 4 descriptors. The build is run and waited on rather than exec'd, with fd 9 closed for the child: exec handed the lock to any daemon the build started, which then held it for its own lifetime. At saturation a build degrades to the floor job count under its own memory cap instead of proceeding unbounded, which is what the old timeout did at the exact moment contention was highest. CARGO_INCREMENTAL is left alone by default. Forcing it off makes workspace crates cacheable, but cargo never builds dependencies incrementally, so it buys less than a full-rebuild measurement suggests and costs an edit-rebuild loop the incremental it depends on. Opt in per host for build boxes and CI runners. Add zram_swap. MemoryHigh throttles by reclaim, so on a swapless host a build past its budget stalls rather than slows, and the governor now warns at converge time when it lands on one. Load local-config/vars.yml when present, tagged always. A host setting with nowhere to live is lost at the next converge - a box with a dedicated cache volume had its build pool silently relocated to the root filesystem because its cache root had only ever been passed as -e. The sccache unit now carries its own cache size and base directories. The server reads them once at startup, and a unit inherits pam_env but not /etc/profile.d, so a hand-set value in /etc/environment silently beat the role's and the running server had a ceiling nobody configured. The role restarts the server when the unit changes, only where the server is the unit's own. The prune timers carry CARGO_HOME on both systemd and launchd, since neither inherits a shell profile and the pruner reads it to find the config that names the pool. shellcheck in CI now covers the governor.
Three playbooks under tests/proxmox/, driven by hand, not CI. create.yml clones the base template into the test range, sizes it to the lab's other test machines, gives it a static address or DHCP, boots it once to bring every package current, then stops and snapshots the VM as `clean`. hyperi-developer never runs there: the snapshot is the state every test starts from. reset.yml rolls a VM back to that snapshot and starts it, or with start=false leaves it stopped, which is the resting state of a test VM. delete.yml removes one. All three refuse a vmid outside the range, which is the safety mechanism .env.sample had promised and nothing had implemented. A static VM is reached by its address and a DHCP VM by the name the lab DNS registers for it, so nothing waits on a DNS entry that may not exist yet. The free-vmid picker now sorts: difference() is a set operation and its first element was arbitrary. Authentication is the API token hyperi-infra's tools use, not a root password, and the modules are community.proxmox: the community.general proxmox_* modules are deprecated and removed at 15.0.0, which the existing floor reaches. Login to the VM is by key, because cloud images ship sshd with password login off. The shared loader in tests/common/setup_env.yml ran `source .env` under /bin/sh, which is dash on Debian and Ubuntu and has no `source`. The failure was silent - the pipeline's exit status is jq's - so `env` returned whatever PROXMOX_* the operator's shell already carried, and a token from the environment quietly stood in for the file's. It now runs under bash with set -euo pipefail. provision.yml, which shares the loader, only ever worked by the same accident. create.yml adopts a VM of the same name so a run that stopped partway is finished rather than doubled. Proven against Proxmox: a VM created, rolled back, started, converged and returned to its snapshot stopped.
hyperi_github_token, hyperi_github_headers, hyperi_github_env and hyperi_core_versions lived in inventories/localhost/group_vars, so playbooks/main.yml only worked with that one inventory. Against any other, a test VM or a fleet host, the first task to reference them failed undefined - the Rust role's build-environment step, on a clean VM, after thirty tasks had run. The variables belong to the playbook, not to one inventory: they are read from the control node's environment and name the versions the roles install. Moved to playbooks/group_vars, which Ansible loads beside the playbook for whatever inventory is passed. The localhost install path is unchanged.
New opt-in role installing the OpenAI Codex CLI as a second opinion alongside Claude Code, plus OpenAI's Codex plugin for Claude Code. Claude Code stays the driver. Selected by --tags developer-ai, the ai group tag, or the codex and codex-plugin tool tags; never pulled by a bare install. Dependencies are probed on the target, per user, rather than declared. Claude Code comes only from soe, so a meta dependency would drag org policy onto a machine that asked for a review tool, and node comes from the developer base rather than developer-node, so the dependency that looks right supplies nothing. A miss skips the component and names the tag that fixes it. Codex installs via the official installer, which verifies the release tarball against codex-package_SHA256SUMS; the thin codex-<target> assets carry no published digest at all. macOS takes the cask, there being no formula. Sign-in stays interactive and is not automated. Splits the Claude Code binary install from the HyperI managed settings, which move behind a claude-policy tag only soe selects. --tags claude used to write /etc/claude-code/ on any machine, which is org policy arriving through a tag that says nothing about policy. install.sh no longer reports success unconditionally. A failed optional component records a warning and the run continues by design, so the exit code is 0; the banner now reads the warning count from the applied-state stamp, and a --check run says nothing was changed. Headroom was built and removed before shipping. It works by pointing ANTHROPIC_BASE_URL at a local proxy, and Claude Code gates capabilities on api.anthropic.com: behind a custom base URL it stops sending the context-1m header and accounts against 200k, disables on-demand tool loading, and disables Remote Control. See docs/install-matrix.md. Verified on Ubuntu: codex-cli 0.153.2 and plugin codex@openai-codex 1.0.6 installed per user, applied-state stamp reports zero warnings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an opt-in role for AI coding-agent tooling: the OpenAI Codex CLI as a
second opinion alongside Claude Code, plus OpenAI's Codex plugin for Claude
Code. Claude Code stays the driver; nothing here makes Codex primary.
Selected by
--tags developer-ai, theaigroup tag, or thecodexandcodex-plugintool tags. Never pulled by a bare./install.sh.Dependencies are probed, not declared
The plugin hard-requires Claude Code, a
codexthat answers bothcodex --versionandcodex app-server --help, and node 18+. All three areper user, so no role graph can answer them:
soe, so a meta dependency would drag org policyonto a machine that asked for a review tool.
developerbase rather thandeveloper-node, so thedependency that looks right supplies nothing.
A miss skips the component and names the tag that fixes it, following the
flarectlprecedent inroles/infrastructure/tasks/cloudflare.yml.Also in here
--tags claudeused to write
/etc/claude-code/managed-settings.jsonon any machine, whichis org policy arriving through a tag that says nothing about policy. The
settings move to
claude_policy.ymlbehind aclaude-policytag onlysoeselects.
install.shno longer reports success unconditionally. A failed optionalcomponent records a warning and the run continues by design, so the exit code
is 0 and the banner said "complete!" over a half-install. It now reads the
warning count from the applied-state stamp, and
--checksays nothing waschanged rather than claiming an installation finished.
hyperi-updategains Codex and Codex-plugin sections on both platforms.docs/install-matrix.mdcorrected: the digest-verification count wasoverstated, and the
developer-nodetable claimed a runtime that lives in thebase.
Headroom was built and removed before shipping
It works by pointing
ANTHROPIC_BASE_URLat a local proxy, and Claude Codegates capabilities on
api.anthropic.com. Behind a custom base URL it stopssending the
context-1mbeta header and accounts against 200k(headroomlabs-ai/headroom#1158), disables on-demand tool loading (#746), and
disables Remote Control (#1779, open). Trading a 1M window for token
compression is a losing trade. The reasoning is recorded in the role and in
docs/install-matrix.mdso nobody re-adds it without re-checking the gating.Verification
Converged on Ubuntu against a real workstation:
codex-cli 0.153.2installed per user to~/.local/bin/codexcodex@openai-codex1.0.6 registered at user scope"warnings": 0ansible-linton the role: only the two findings the whole tree carries(
role-namehyphen,schema[meta]Fedora versions)tools/ci/run-tests.sh: syntax check, shellcheck and pytest passTwo adversarial reviews (correctness and security) ran against this branch and
their findings are remediated. Not covered by molecule: the matrix scenario runs
the default no-tags install, so an opt-in role is never exercised by it.
Note on scope
Carries three commits that predate this work and were sitting unpushed on
main:7d17f23,dd2d7fe,19e4632.