From 3e0d1cbb83a203ed67f3231be7e5dc608ed6d859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Sat, 26 Sep 2026 21:52:08 +0200 Subject: [PATCH] Add design spec --- .gitignore | 2 + .../2026-09-26-agent-environments-design.md | 191 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 .gitignore create mode 100644 docs/superpowers/specs/2026-09-26-agent-environments-design.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..750baebf4151eb52e380f15cf6a33a059527db79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +result +result-* diff --git a/docs/superpowers/specs/2026-09-26-agent-environments-design.md b/docs/superpowers/specs/2026-09-26-agent-environments-design.md new file mode 100644 index 0000000000000000000000000000000000000000..f1ee7b6edbeb9d73c21d3daf5b3ba6f1a029bac9 --- /dev/null +++ b/docs/superpowers/specs/2026-09-26-agent-environments-design.md @@ -0,0 +1,191 @@ +# agent-environments — design + +Status: self-authored during an autonomous run (user asked to work autonomously +and consult Codex). Reviewed by Codex (review-1: blocking points folded in), not +yet by the user. + +## Goal + +A Nix flake **library** for declaring reproducible configurations ("environments") +for coding agents — Claude Code and Codex for now — and launching them under +separate identities (logins). Other flakes consume it; `examples/` shows how. + +## Requirements (from the user) + +- Module-based: modules add text to the instruction file (AGENTS.md / CLAUDE.md) + and enable skills, e.g. `modules.skills.xyz.enable = true`. +- Several launchers such as `claude-work-acc`, `claude-pers-acc`. They may share + one environment but log in separately. Each launcher gets its own state + directory `~/.local/agent-environments/`, passed in with + `CLAUDE_CONFIG_DIR` / `CODEX_HOME`. +- Each environment is its own set of modules. +- Skills are packaged. A skill can need runtime tools and other skills. +- Flakes. `claude-code` and `codex` come in as inputs: + `github:sadjow/claude-code-nix`, `github:sadjow/codex-cli-nix`. +- Standalone only (no home-manager). The root flake exposes the library plus one + default environment with no skills. + +## Architecture + +``` +flake.nix inputs: nixpkgs, claude-code, codex +lib/default.nix mkEnvironment, mkSkill, mkSkillModule +modules/core.nix instructions, skills, launchers, assertions +modules/claude.nix claude.{enable,package,settings} +modules/codex.nix codex.{enable,package,settings} +modules/launcher.nix wrapper script generation +checks/ behaviour tests (stub agents, temp HOME) +examples//flake.nix consumer flakes +``` + +### Evaluation + +`lib.mkEnvironment { pkgs, modules ? [] }` runs `lib.evalModules` over the core +modules plus the user's modules, with `pkgs` passed as a module argument. It +returns the evaluated `config` along with `package` (a `symlinkJoin` of every +launcher) and `launchers` (an attrset of per-launcher packages). + +The flake's `lib` is system-independent. `mkEnvironment` receives `pkgs`, and the +flake sets default agent packages for `pkgs.stdenv.hostPlatform.system` from its +own inputs. Consumers can override those with `claude.package` / +`codex.package`. + +### Core options + +- `instructions. = { text : lines; order : int = 1000; }` — fragments + sorted by `(order, name)` and joined with a blank line into one document. That + document becomes `CLAUDE.md` for Claude and `AGENTS.md` for Codex. Modules add + a fragment by defining an entry. +- `skills.packages : listOf package` — skill derivations made by `mkSkill`. The + effective set is the transitive closure over `passthru.skillDeps`, deduplicated + by skill name. Two different derivations with the same name → evaluation error. +- `claude.enable`, `claude.package`, `claude.settings : attrs` (JSON). +- `codex.enable`, `codex.package`, `codex.settings : attrs` (TOML). +- `launchers. = { agent : enum ["claude" "codex"]; id : str = ; + stateDir : str = "~/.local/agent-environments/${id}"; }`. The name is the + command name. `stateDir` must be absolute or start with `~/` (assertion); it is + shell-quoted as a literal, and only the leading `~/` becomes `$HOME`. + A launcher's agent must be enabled (assertion). + `id` and launcher names must match `^[A-Za-z0-9][A-Za-z0-9._-]*$` (assertion). +- Launchers are always declared explicitly. There are no implicit default + launchers, because attrset options merge by key and a hidden default could not + be dropped cleanly. + +### Modules namespace + +`modules.*` is where optional features live, e.g. `modules.skills..enable`. +The helper `lib.mkSkillModule skill` produces a module declaring +`options.modules.skills..enable` and, when it is enabled, adding +the skill to `skills.packages`. Options cannot be generated from `config` (that +would recurse infinitely), so consumers import one module per skill they want to +make available. Instruction modules are plain modules that define +`instructions.`. + +### Skill packaging + +`lib.mkSkill { pkgs, name, src, runtimeInputs ? [], skills ? [] }`: + +- `name` must match `^[A-Za-z0-9][A-Za-z0-9._-]*$` (evaluation-time check). +- Output: `$out/` is the skill directory (it must contain `SKILL.md`; checked at + build time). The same directory works for both agents, since Claude Code and + Codex share the `SKILL.md` format. +- `passthru = { skillName = name; runtimeInputs; skillDeps = skills; }`. +- Runtime inputs: the launcher puts the union of all effective skills' + `runtimeInputs` on `PATH` (prepended). Skills run inside the agent's shell, so + this is the smallest mechanism that works for both agents. + +### Launcher (runtime) + +The wrapper is a `writeShellApplication` per launcher. Everything it manages is +listed in a manifest, `$state/.agent-environments-managed` (one relative path per +line). On every start it: + +1. `mkdir -p "$state/skills"`, then takes `flock` on + `$state/.agent-environments.lock` for the setup phase only; the lock fd is + closed before `exec`. +2. Computes the wanted set of managed links: the instruction file + (`CLAUDE.md` for claude, `AGENTS.md` for codex, when there are instructions), + `skills/` for each effective skill, and the codex profile TOML (when + there are settings). +3. Removes every path in the old manifest that is not wanted any more, but only + when it is still a symlink. Anything else is left in place. +4. For each wanted path that exists: unless it is listed in the old manifest and + is still a symlink, stop with an error naming the path (user data, including + user symlinks, is never replaced + and there are no automatic backups). Otherwise replace it atomically + (`ln -s` to a temp name, then `mv -T`). +5. Writes the new manifest atomically. Codex's own `skills/.system` and any + user-installed skills are never touched, because they are not in the manifest. +6. Settings, layered so the agents stay free to write their own files: + - Claude: `--settings ` when `claude.settings != {}`. + `settings.json`, `.claude.json` and `.credentials.json` in the state dir + stay owned by Claude. + - Codex: `$state/agent-environments.config.toml` (a managed link to the store + TOML) plus `--profile agent-environments`, which layers + `$CODEX_HOME/.config.toml` over `config.toml`. `config.toml` + (which holds trust entries) and `auth.json` stay owned by Codex. Skipped + when `codex.settings == {}`. +7. `export CLAUDE_CONFIG_DIR="$state"` / `export CODEX_HOME="$state"`, prepend + the runtime inputs to `PATH`, then `exec "$@"`. + +Logins are therefore per launcher: `claude-work-acc` and `claude-pers-acc` have +different ids, so different state dirs, so different credentials, even when they +come from the same environment. Two launchers that should share a login can set +the same `id`. They then also share managed content: the last launch wins, so they +should come from the same environment. This is documented, not enforced. + +### Root flake outputs + +- `lib` — `mkEnvironment`, `mkSkill`, `mkSkillModule`. +- `packages..default` — the default environment: claude + codex enabled, + launchers `claude` and `codex` with ids `default-claude` / `default-codex`, + no skills and no instructions. +- `checks..*` — behaviour tests. +- `formatter` — `nixfmt`. + +### Examples + +- `examples/basic` — one environment, one instruction fragment, one local skill + with a runtime dependency and a skill dependency. +- `examples/multi-account` — one module set, launchers `claude-work-acc`, + `claude-pers-acc`, `codex-work-acc`, plus a second environment with a different + module set. + +Each example is a flake with `inputs.agent-environments.url = "path:../..";`. + +## Error handling + +- Evaluation-time assertions: a launcher's agent is disabled; an unsafe id, + launcher or skill name; conflicting skill names. A skill without `SKILL.md` + fails at build time. +- Runtime: never delete non-symlink files and never replace unmanaged ones; fail + loudly (`set -euo pipefail`) and say which path conflicts. + +## Testing + +`nix flake check` runs derivation checks that execute the launchers with stub +`claude`/`codex` packages (scripts that dump argv, env and state-dir contents), +using a temporary `HOME`: + +- the state dir is created under `$HOME/.local/agent-environments/` and the + right env var points at it; +- two launchers from one environment get different state dirs (separate logins); +- the instruction file carries the fragments in order; +- dependency skills are linked transitively; a skill's runtime tool is on `PATH`; +- a skill that is no longer enabled is unlinked on the next launch, while user + skill dirs, user symlinks and `.system` are kept; +- a pre-existing user `CLAUDE.md` makes the launcher fail and is left untouched; +- the `--settings` / `--profile` flags appear only when settings are non-empty. + +One integration check runs the real Codex package offline to confirm that it +discovers `$CODEX_HOME/AGENTS.md`, the skills and the profile, if Codex has a +non-interactive way to show this. If it does not, that is recorded as a coverage +gap and checked by hand. + +Examples are built with `nix build ./examples/ --override-input +agent-environments path:$PWD`. + +## Out of scope (YAGNI) + +home-manager integration, MCP server modules, plugins, per-project config, +agents other than Claude Code and Codex.