agent-distro

Your team's coding agents,
in one command.

Package your skills, MCP servers and model gateway once. Run them in Claude Code, Codex, OpenCode, Oh My Pi and Pi, each at its latest release, with nothing to install.

nix run github:juspay/agent-distro

Quick start Install & keep updated Build your own

A terminal: nix run github:juspay/agent-distro opens a picker with two profiles and six harnesses; the filter narrows to Claude Code, Enter launches it.
Pick a profile and a harness, or filter with /. Arguments after -- go straight to the harness.

Every harness, at its latest release

These are the versions this site was built from. The daily update moves them forward once the builds and VM tests pass.

How it fits together

A profile is data. agent-distro translates it once per harness, and each harness starts with your plugins and gateway already in place.

Also inside Kolu

Type claude. It's already there.

Kolu, the terminal workspace for running coding agents on a canvas of tiles, ships agent-distro built in. Choose a profile once and every new terminal, local or remote, has the agents on its PATH, kept current four times a day, each started with Kolu's plugin loaded so it can drive other tiles.

Agents in Kolu

Quick start

nix run github:juspay/agent-distro                     # pick from the list
AI_HARNESS=claude nix run github:juspay/agent-distro   # skip the list

To keep the agents installed and updated, see Install and keep updated.

  • Systems. x86_64-linux, aarch64-linux, and aarch64-darwin.
  • Binary cache. Pass --accept-flake-config to use the cache this flake names;1 without it, Oh My Pi is built from source.

The chooser

With several profiles, the chooser is master–detail: profiles are tabs in the header, harnesses are the list on the left, and the highlighted harness fills the panel on the right.2 ←/→ (or Tab) switch profile, and nothing in the box is ever cut.

Here vanilla/pi is remembered from last time.3

╭─ agent-distro ───────────────────────── juspay · • vanilla ── 6 harnesses ─╮
│ juspay · Juspay skills + Kolu, via Juspay's LiteLLM gateway                │
├──────────────────────────────┬─────────────────────────────────────────────┤
│ ❯   ✓ Oh My Pi       18.7.0  │ Oh My Pi 18.7.0                             │
│     ✓ Codex         0.160.1  │ gateway or own provider · extensions        │
│     ✓ Claude Code   2.1.292  │                                             │
│     ✓ OpenCode      1.18.35  │ Signed in                                   │
│       OpenCode v2    2.0.24  │   LITELLM_API_KEY                           │
│     ✓ Pi              1.0.4  │   anthropic                                 │
│                              │   openai                                    │
├──────────────────────────────┴─────────────────────────────────────────────┤
│ / filter…                                                                  │
╰────────────────────────────────────────────────────────────────────────────╯
↑↓ jk move   ←→ profile   Enter launch   / filter   q quit
Variable4 Values Effect
AI_HARNESS a directory under harnesses/ Launches that harness without the list5
AI_PROFILE a directory under profiles/ Chooses the profile;6 on its own, narrows the list to that profile
AI_GATEWAY 0 Keeps the plugins but skips gateway initialization
AGENT_DISTRO_PLUGINS :-separated plugin directories Loads more plugins for that launch; see Plugins at launch

By name

Command Does
agent-distro <harness> Launches directly
agent-distro <profile> <harness> Selects both
agent-distro <profile> Narrows the chooser

For example:

agent-distro codex --version
agent-distro juspay claude --version
agent-distro juspay
agent-distro --list                      # profile harness title version, one row per line
agent-distro --list --json               # the same, as JSON (below)
  • Precedence. AI_HARNESS and AI_PROFILE take precedence over positional selections.7
  • Passthrough. Recognized leading names are consumed; other arguments and everything after -- are passed to the harness unchanged.

One profile

Narrowed to one profile,8 the header shows that profile’s name instead of tabs, and the row under it is the same. Versions reflect the packages pinned at build time.9

╭─ agent-distro · juspay ────────────────────────────────────── 6 harnesses ─╮
│ juspay · Juspay skills + Kolu, via Juspay's LiteLLM gateway                │
├──────────────────────────────┬─────────────────────────────────────────────┤
│ ❯   ✓ Oh My Pi       18.7.0  │ Oh My Pi 18.7.0                             │
│     ✓ Codex         0.160.1  │ gateway or own provider · extensions        │
│     ✓ Claude Code   2.1.292  │                                             │
│     ✓ OpenCode      1.18.35  │ Signed in                                   │
│       OpenCode v2    2.0.24  │   LITELLM_API_KEY                           │
│     ✓ Pi              1.0.4  │   anthropic                                 │
│                              │   openai                                    │
├──────────────────────────────┴─────────────────────────────────────────────┤
│ / filter…                                                                  │
╰────────────────────────────────────────────────────────────────────────────╯
↑↓ jk move   Enter launch   / filter   q quit

Signed-in marks

✓ marks a harness that can start without a login step; the panel beside it says what was found.10 A harness with nothing to start from shows a dim Not signed in and no mark.11

Harness Looks at
Claude Code oauthAccount.emailAddress in $CLAUDE_CONFIG_DIR/.claude.json,12 or the name of ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN
Codex the email claim of the OpenID token in $CODEX_HOME/auth.json,13 or OPENAI_API_KEY
Gateway harnesses the profile’s gateway.keyEnv, then the provider ids it also has credentials for14
Own-provider harnesses the ids in the harness’s store,15 plus the provider API-key variables it reads from the environment

AI_GATEWAY=0 puts a gateway harness back on its own provider. This is a launch-time fact: --list and --list --json never carry it.

Keys

Key Does
↑/↓, j/k Move in the list
←/→, Tab/Shift-Tab, h/l Switch profile (several profiles only)
Enter Launch the highlighted harness
/ Filter the list by title or tagline
Escape (filtering) Clear the filter, keeping the cursor on its row
Escape, q, Ctrl-C Quit without choosing
  • Filtering. The filter line shows the query as typed and how many rows match.16
  • Footer. Lists only the keys that apply at that moment.
  • Colour. Follows NO_COLOR and the terminal type.
  • Small terminals. Small or unsupported terminals get a numbered list on stderr instead.17 One that shrinks below that once the box is up shows a notice until it grows again.18
  • No terminal. Without a terminal on stdin, agent-distro says which variables to set instead of drawing anything.

Remembered choice

An interactive selection is remembered in ${XDG_STATE_HOME:-$HOME/.local/state}/agent-distro/last-choice.19 Next time the default profile still opens, and the remembered profile and harness are marked with •.20

  • Updates it. Every interactive choice, including agent-distro <profile>.
  • Never updates it. Direct selections: AI_HARNESS, agent-distro <harness>, or agent-distro <profile> <harness>.

Listing as JSON

agent-distro --list --json prints what the chooser draws, on one line, for programs that offer the same choice (such as kolu).21 Here it is pretty-printed, with each profile cut to one harness:

{
  "profiles": [
    {
      "description": "Juspay skills + Kolu, via Juspay's LiteLLM gateway",
      "harnesses": [
        {
          "name": "claude",
          "tagline": "Anthropic login · plugin dirs per session",
          "title": "Claude Code",
          "version": "2.1.291"
        }
      ],
      "name": "juspay"
    },
    {
      "description": "Upstream harnesses with your own provider",
      "harnesses": [
        {
          "name": "claude",
          "tagline": "Anthropic login · plugin dirs per session",
          "title": "Claude Code",
          "version": "2.1.291"
        }
      ],
      "name": "vanilla"
    }
  ]
}
Field Rule
profiles The first profile is the default
harnesses In menu order
name Unique at its level, free of whitespace and /; what AI_PROFILE or AI_HARNESS (or a positional selector) takes
version The display version, as --list prints it22

Keys within an object are sorted. --list takes no other arguments.23

Plugins at launch

A profile fixes the plugins its harnesses start with. Put more Agent Plugins directories on AGENT_DISTRO_PLUGINS, and every harness loads them too, in every profile, for that launch:

AGENT_DISTRO_PLUGINS=~/src/my-plugin agent-distro claude
AGENT_DISTRO_PLUGINS=/nix/store/…-kolu/agent-plugin:~/src/my-plugin agent-distro juspay omp

Unset or empty, the launchers behave exactly as they do without this feature.

  • Entries. Directories separated by :,24 with empty components ignored.25 An entry that is not a directory fails the launch, naming it.
  • Read like a profile’s plugins. Each directory goes through the same reader: an invalid manifest fails the launch with a message naming the field. Lesser problems are reported while the launch goes on.26
  • Matched by name, never by version. A plugin whose plugin.json name matches one of the profile’s replaces it; on the variable, the last of a name wins.27
  • Translated once. Each harness’s translation of a plugin is cached under ${XDG_CACHE_HOME:-~/.cache}/agent-distro/plugins/<key>/<harness>/.28 An edited checkout is translated again on its next launch.29
  • Kept while in use. Each launch marks the translations it uses; a launch that translates something new removes what no launch has used for 14 days.30 With no harness running, removing ~/.cache/agent-distro/plugins is always safe.
  • For that launch only. Unset the variable and the harness is back to the profile’s plugins.31 Each harness’s README says how.

Install and keep updated

The Home Manager module puts every harness on your PATH and keeps them on the latest build: it checks four times a day,32 installs only what the binary cache already holds, and never compiles.

With Home Manager

{
  imports = [ inputs.agent-distro.homeManagerModules.default ];
  services.agent-distro = {
    enable = true;
    profile = "vanilla"; # default: juspay
  };
}

Home Manager creates the state directory and schedules the updates for you.33

Without Home Manager

Update by hand:

nix profile install github:juspay/agent-distro#juspay
nix profile upgrade juspay

Or keep the cache-only updates, but drive them yourself:

  • agent-distro.lib.mkUpdater (see Library) gives the command and a runnable program.34
  • agent-distro.lib.mkShims keeps installed commands pointing at the updated bundle.
  • Scheduling is yours. The updater schedules nothing itself, so run it on your own cadence.35
  • Setup is yours. Create the stateDirectory yourself, and run --cache-warnings once to surface an unusable cache.36

Progress output

Append --progress to command to drive the same update from a program that wants to show the download. Stdout is then one JSON object per line,37 and every human message moves to stderr. Exit codes and the history log are the same as without it.

The binary cache

The updater never compiles harnesses from source; it passes the project cache to every update.38 On a multi-user install where you are not a trusted Nix user, add the cache to your NixOS or nix-darwin configuration:39

nix.settings.extra-substituters = [ "https://cache.nixos.asia/oss" ];
nix.settings.extra-trusted-public-keys = [ "oss:KO872wNJkCDgmGN3xy9dT89WAhvv13EiKncTtHDItVU=" ];

Without it, or when the cache does not yet hold the whole bundle, the update is skipped and your current version keeps working.

history.log records the skip,40 and activation warns while the cache is unusable.

Troubleshooting

Symptom Where to look
Updates not arriving systemctl --user status agent-distro-update; run manually with systemctl --user start agent-distro-update
What was updated or failed ~/.local/state/agent-distro/history.log41
Full run output, Linux journalctl --user -u agent-distro-update
Full run output, macOS ~/.local/state/agent-distro/<source>/update.log

Build your own distribution

mkdir my-distribution && cd my-distribution
nix flake init -t github:juspay/agent-distro

Point my-skills at your Agent Plugins repository and edit profile.nix:

Field Meaning
name Distribution identifier; Codex marketplace is <name>-ai
description Shown with the profile in the picker
plugins List of Agent Plugins directories42
gateway null, or { url; keyEnv; models = { large; small; }; keyHint; } for a LiteLLM proxy
packages Optional pkgs: [ … ]: commands the plugins’ MCP servers name, put first on PATH for every harness43
  • Start from. The template includes a gateway example; agent-distro.profiles.vanilla is the reference profile shape.
  • Pin. Commit flake.lock to pin your build.
  • Run. Your team runs nix run github:<you>/my-distribution.
  • Update. nix flake update agent-distro.
agent-distro.lib.mkFlake { profile; systems ? [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ]; cache ? …; }
# → packages.<system>: every harness, default picker, and <profile.name> bundle
#   (a bundle's bin/ carries its harness commands and `agent-distro`, its own picker)
#   apps.<system>: every harness and default picker; homeManagerModules.default;
#   lib: the same eight-name library attrset this flake exposes

agent-distro.lib.mkLaunchers { pkgs; profile; }
# → every harness launcher, plus picker and bundle (the bundle's bin/ also holds
#   that picker as `agent-distro`, defaulting to the profile)
  • Outputs. mkFlake returns packages,44 apps, homeManagerModules.default, and the same lib attrset flake.nix exposes; add other outputs with //.
  • One profile. A single-profile distribution draws the narrowed list above: one profile’s rows under its description. mkFlake gives you the individual launchers and a bundle named after the profile, with the picker as its default.
  • Cache. mkFlake takes an optional cache = { url; publicKey; }, defaulting to agent-distro’s cache. Push your own builds to a cache and pass it, or your users’ updates are skipped.45

For NixOS, with your distribution bound as distro:

environment.systemPackages = [ distro.packages.${system}.my-profile ];

AI_GATEWAY=0 nix run .#omp skips gateway initialization while keeping plugins.46

Bundle layout

A profile’s bundle (nix build .#<profile.name>) is self-describing: a consumer reads two files rather than running a command for information.

bin/<harness>…                      # one launcher per harness
bin/agent-distro                    # the picker, defaulting to this profile
share/agent-distro/profile.json     # {"description": "…", "name": "<profile.name>"}
share/agent-distro/versions         # <harness>\t<title>\t<version>, one line per harness, in menu order

profile.json comes from the same profile attributes as the picker’s --list --json, so the two cannot drift.47

Library

Beyond the launchers, agent-distro.lib exposes the pieces Home Manager uses under the hood. Any Nix consumer, not only Home Manager, can keep a profile installed, updated, and chosen.48

agent-distro.lib.stateDirectory { xdgStateHome; flake; profile; }
# → the string "<xdgStateHome>/agent-distro/<sha256 of {flake, profile}>", the
#   same hash Home Manager computes today. One function, so a consumer and the
#   updater can never disagree on where `current` lives.

agent-distro.lib.mkShims { pkgs; bundle; stateDirectory; }
# → a derivation whose bin/ holds one shim per bundle.commands, each
#   `exec "<stateDirectory>/current/bin/<name>"` when executable, else
#   `exec <bundle>/bin/<name>`; identical text to the Home Manager module's shims.
#   `bundle` must be a bundle from `mkLaunchers` of this agent-distro (it carries
#   `commands`); anything else is an eval error naming the problem.

agent-distro.lib.mkUpdater { pkgs; bundle; flake; profile; stateDirectory; history; nix; substituters; }
# → { config = <the generated JSON file>; command = [ node update.ts config ];
#   program = <a writeShellApplication running `command "$@"`>; }. The Home
#   Manager module's systemd ExecStart, launchd ProgramArguments (`++ ["--scheduled"]`)
#   and activation `--cache-warnings` all come from this; `command ++ [ "--progress" ]`
#   is the same update with JSON progress on stdout. `bundle` must be a
#   `mkLaunchers` bundle with a `runtime` (post-#59 ones do); the period/offset
#   come from `lib.schedule` and are not overridable here; the updater never
#   compiles, handing nix only `substituters`.

agent-distro.lib.mkPicker { pkgs; profiles; default; }
# → lib/picker.nix's derivation: the interactive profile/harness chooser, where
#   `profiles = { <name> = { profile; launchers; }; … }` — one entry per profile,
#   `launchers` the `mkLaunchers` result for it — and `default` names the preselected one.

The updater’s schedule is one lib/schedule.nix, exposed as agent-distro.lib.schedule: updateHoursUTC, defaultFrequency, updatePeriodSeconds and updateOffsetSeconds. The module, mkUpdater and any consumer read the same numbers.

Profiles

Profile What it is
vanilla Upstream harnesses with your own provider; no plugins, no gateway
juspay (default) Juspay skills + Kolu, via Juspay’s LiteLLM gateway
Using the Juspay profile
AI_PROFILE=juspay nix run github:juspay/agent-distro
AI_PROFILE=juspay AI_HARNESS=omp nix run github:juspay/agent-distro -- --version
  • Adds Juspay’s skills and Kolu.
  • Gateway. Runs OMP, Pi, and OpenCode against Juspay’s LiteLLM gateway, prompting for LITELLM_API_KEY unless it is exported.49 AI_GATEWAY=0 keeps the plugins but skips gateway initialization.
  • Own login. Codex and Claude Code always use their own login.
  • MCP servers. Kolu’s MCP server needs kolu on PATH. The profile supplies mcp-nixos itself, at its latest release.

A profile is a directory under profiles/:

profiles/
  registry.nix          # { default = "<name>"; } — the profile the list opens on
  <name>/profile.nix    # { name; description; plugins; gateway; packages; }
  <name>/npins/         # optional: the profile's pinned plugin and package sources
  • Discovered. Profiles are discovered from the directory listing, so adding one is adding a directory; nothing in flake.nix names them.
  • Plain data. profile.nix is a plain attrset whose name must match its directory.50
  • Pinned with npins. Each profile pins its own sources, not with flake inputs:51
let sources = import ./npins;
in { name = "example"; plugins = [ sources.skills ]; /* … */ }
  • Adding a source. npins --directory profiles/<name>/npins add github <owner> <repo> follows the repository’s releases; add --branch main to follow a branch.
  • Updates. The daily update advances every profile’s pins alongside the harnesses: a release pin to the latest release, a branch pin to its head.

Harnesses

Each harness is a directory under harnesses/; its README holds the design notes for that adapter.

Harness Command Notes
Oh My Pi omp design notes
Codex codex design notes
Claude Code claude design notes
OpenCode opencode design notes
OpenCode v2 opencode2 design notes
Pi pi design notes

How it works

  • Profile. Harness-independent data.
  • Harness. The agent application.
  • Plugin. A portable Agent Plugins directory.
  • Gateway. An optional LiteLLM proxy used by OMP, Pi, and OpenCode.

Harness design notes live in each directory under harnesses/.

Runtime

  • TypeScript, no build step. What runs on your machine beyond the harnesses themselves52 is TypeScript under src/, run by Node 24’s native type stripping: no package.json, bundler or compile step.53
  • Dependencies. Node comes from the distribution’s nixpkgs. Its one npm dependency is yaml, for OMP’s config.54
  • Entry points. The commands you run, and the shims Home Manager installs, stay small generated shell scripts that call into it.

Reading plugins

Every plugin is read once, harness-independently, against Agent Plugins 1.0.0, by the same reader and the same harness writers.55

  • Invalid manifest. Fails the build (or the launch) with a message naming the field.
  • Lesser problems. Skipped skills, disabled mcp.json files and invalid server entries are reported in the build log (or on stderr).56
  • Commands. A bare MCP command is found on PATH: the profile’s packages first, then the user’s own. A ./ command runs from the plugin.

One known gap: Claude Code expands ${VAR} in a remote server’s url and headers, which the spec forbids.57

Shared homes

Profiles share each harness’s own home directory, so what OMP and Codex persist outlives the profile that wrote it.58 Claude Code’s plugins last only for the session.

Development

nix build .#default    # the picker, and through it every profile's launchers
nix build .#vanilla    # one profile's bundle: its harnesses plus its own bin/agent-distro
nix flake check
just test              # offline NixOS VM tests; Linux with KVM
just test-template
just demo              # re-record doc/demo.gif and the website's stills
python3 .github/scripts/test-update-flake.py

The TypeScript’s own checks need no VM or KVM, and nix flake check at the root does not run them. Build them from the test flake:

cd test && nix build --no-link .#checks.x86_64-linux.{reader,launch-plugins,omp-adapter,pi-adapter,opencode-adapter,update-schedule,list-json,picker-layout,picker-auth}
Check Covers
reader the plugin reader
launch-plugins AGENT_DISTRO_PLUGINS: cache keys, re-translation, precedence and the launches it fails
<harness>-adapter a harness’s tests/check-adapter.ts
update-schedule the updater’s schedule and cache policy
list-json --list --json against its type, the chooser’s menu and --list
picker-layout the chooser’s cell widths, truncation and box at common terminal sizes
picker-auth the auth probes over fixture homes and stores

CI

  • Daily sources. CI runs .github/scripts/update-sources.sh,59 then updates the root and test locks.
  • Report. The update report reads resolved versions and each harness’s release-note metadata.
  • Cache. CI pushes realised paths to the OSS cache.60
  • Merge gate. The daily update PR merges only after the required Linux/macOS builds and VM/template checks pass.61
  • Cancellation. Superseded PR runs are cancelled.
  • Consumers. They run nix flake update agent-distro.

For manual updates, run bash .github/scripts/update-sources.sh, nix flake update, and then bash test/update-lock.sh.

Test library

Consumers can import test/lib.nix { pkgs; launchers; profile; features; koluPlugin ? null; }. Checks are selected by required features from each harness’s metadata;62 picker is shared.

  • Also mkLaunchers. Rebuild and gateway checks also accept mkLaunchers.
  • Plugin rebuilds. Select plugin rebuild checks only for nonempty skill plugins.
  • Coverage. The test flake covers vanilla (with kolu’s plugin loaded at launch), Juspay, and spec fixtures, plus registry (the picker over the whole profile registry).

doc/logo.svg is the source vector; downstream consumers (kolu) vendor a copy.

Keep it transparent, textless, readable at 16 px.

Adding a harness

Create harnesses/<name>/ with four files:

File Holds
meta.nix title, order, auth,63 releaseNotes = version: URL, and checks
default.nix the adapter, accepting { pkgs, plugins, gateway, package, profileName }64
source.nix { pkgs } to package
README.md design notes
  • Alongside. Pins in npins/, scripts in tests/, and an optional custom updater in update.py.
  • No registration. Nothing outside this directory needs registration.65
  • Logic beyond shell. Keep it in src/harness/<name>.ts and call it with (import ../../lib/runtime.nix pkgs).script "harness/<name>.ts".66
  • Unit checks. Those that need no VM go in tests/check-adapter.ts, which receives the runtime’s src and the harness directory.
  • Checks. Checks declare { name; script; requires ? []; packages ? []; env ? {}; diskSize ? null; }.67 script is VM-driver Python.68
  • Gateway checks. They receive the shared fake service and launchers configured against it.
  • Fixtures. mkLaunchers also accepts a sources = name: pkgs: … hook for package fixtures.

  1. The cache is named in the flake’s nixConfig. The Home Manager updater in Install handles the cache itself.↩︎

  2. The active profile’s name and description take a row of their own under the header. The panel shows the highlighted harness’s tagline and its auth status.↩︎

  3. So the default profile opens, the remembered one is dotted, and switching to it lands on that harness.↩︎

  4. Names unchanged for script compatibility.↩︎

  5. Also usable in scripts and non-interactive shells.↩︎

  6. Defaulting to the one registry.nix names.↩︎

  7. The profile otherwise defaults to the registry default.↩︎

  8. By agent-distro juspay, by AI_PROFILE, or by being a single-profile distribution.↩︎

  9. Shown without revision suffixes.↩︎

  10. Presence only: the picker contacts no provider and checks no token’s validity or expiry.↩︎

  11. One whose store cannot be read shows nothing at all, and no mark.↩︎

  12. Default ~/.claude.json.↩︎

  13. Read from tokens.id_token without checking its signature; ChatGPT when the claim is absent.↩︎

  14. The gateway and the harness’s own providers are usable at once.↩︎

  15. OMP’s ~/.omp/agent/agent.db, Pi’s $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent/auth.json), OpenCode’s ${XDG_DATA_HOME:-~/.local/share}/opencode/auth.json.↩︎

  16. ↑/↓ still move, Backspace edits, and Enter takes the highlighted match.↩︎

  17. TERM unset or dumb, no controlling terminal, or fewer rows or columns than the box needs.↩︎

  18. The cursor and filter are kept.↩︎

  19. An unavailable state directory is silently ignored.↩︎

  20. Switching to the remembered profile puts the cursor on that harness.↩︎

  21. Its shape is stable, typed as Listing in src/listing.ts.↩︎

  22. Without the package’s + revision suffix.↩︎

  23. The plain --list and the chooser read the same value, so the three cannot disagree.↩︎

  24. So a path containing : cannot be given.↩︎

  25. A relative entry is resolved against the directory the launcher starts in, and symlinks are followed. ~ is expanded by the shell, and only where an assignment expands it: unquoted, in bash or zsh, not in fish.↩︎

  26. Skipped skills, a disabled mcp.json or an invalid server entry are reported on stderr, on every launch.↩︎

  27. No version is compared.↩︎

  28. The cache directory must be an absolute path. A launch never calls Nix, and a cache it cannot write fails it.↩︎

  29. A plugin under /nix/store is keyed by its store path, as is, without reading it. Any other directory is keyed by a hash of its contents (its NAR serialization, computed without Nix, leaving out a top-level .git) and of its absolute path, so a key is particular to one machine and user. That directory is hashed in full on every launch: a large tree (a node_modules, say) costs launch time, and a FIFO or an unreadable file in it fails the launch.↩︎

  30. So old versions of an edited checkout and translations for an older agent-distro do not pile up.↩︎

  31. Claude Code and OMP take them as arguments, OpenCode in its session config, and Codex through -c overrides; Codex keeps an inert copy in its plugin cache, removed once unused for 14 days. Pi only reads its own files, so it records what a launch added and its next launch, whenever that is, takes it back.↩︎

  32. At 02:00, 08:00, 14:00 and 20:00 UTC, two hours after upstream’s update.↩︎

  33. At activation its prune stage deletes sibling state directories left from an earlier flake/profile choice.↩︎

  34. Never compiling; it passes nix only substituters.↩︎

  35. It checks the UTC boundary only when you pass --scheduled, as the module’s launchd does.↩︎

  36. The updater replaces <state>/current directly, so there is no out-link pruning for the consumer to do.↩︎

  37. {"progress":{"done":<bytes>,"total":<bytes>}} while nix fetches, then {"result":"updated","bundle":"/nix/store/…"} (or unchanged, or skipped/failed with a reason).↩︎

  38. cache.nixos.asia/oss, configurable with services.agent-distro.substituters.↩︎

  39. Nothing more is needed when you are a trusted Nix user or run a single-user install. Otherwise the daemon ignores the updater’s request unless your system config already lists the cache.↩︎

  40. A line such as skipped: cache https://cache.nixos.asia/oss not usable; add it to nix.settings substituters/trusted-public-keys. The same line is not repeated, and a skip is not retried until the next scheduled run.↩︎

  41. Update and failure events, in your local timezone, e.g. 2026-10-01T23:03:17+05:30 juspay updated: Pi 0.99.2 → 1.0.0; unchanged runs add nothing.↩︎

  42. Each is a plugin.json manifest, with optional skills/<name>/SKILL.md and mcp.json.↩︎

  43. An MCP server that a plugin declares by bare command, such as "command": "mcp-nixos", is found on PATH. List its package here and it is built, or fetched from a binary cache, together with the launcher, so the server starts at once instead of being downloaded when the agent first asks for it, and every user runs the version the build pinned.↩︎

  44. Every harness, the default picker, and the <profile.name> bundle, which carries its own picker as bin/agent-distro.↩︎

  45. The Home Manager updater only installs what that cache holds and never compiles.↩︎

  46. It preserves existing settings: choose personal models in OMP or Pi if you previously used gateway defaults. Codex and Claude Code always use their own login.↩︎

  47. src/listing.ts types both files (ProfileFile, parseProfileFile).↩︎

  48. Every function evaluates from a plain import of its lib/*.nix file with an arbitrary nixpkgs pkgs; a required argument left out is an eval error naming it.↩︎

  49. Create a key at grid.ai.juspay.net/dashboard (Juspay VPN required).↩︎

  50. packages is its one function, since only the builder has a package set.↩︎

  51. The top-level flake.lock is inherited by everyone who builds on lib.mkFlake, and no distribution’s plugins belong there.↩︎

  52. The plugin reader, each harness’s config writer, the picker and the Home Manager updater.↩︎

  53. The one exception is the entry for AGENT_DISTRO_PLUGINS, plain JavaScript so that it can turn on Node’s compile cache before any TypeScript loads.↩︎

  54. A tarball pinned in lib/npins, which lib/runtime.nix links in as node_modules/yaml.↩︎

  55. A profile’s at build time, and one from AGENT_DISTRO_PLUGINS at launch, into the cache described in Plugins at launch.↩︎

  56. As the spec’s failure boundaries require.↩︎

  57. Those values cannot go through a launcher, so a remote server whose URL or headers contain ${ reaches Claude Code as is.↩︎

  58. A Codex marketplace registered by one profile is still registered when you launch vanilla, and so are the model roles a gateway filled into OMP’s config.↩︎

  59. It discovers npins directories under profiles/, harnesses/, and lib/, re-pins yaml to the newest npm release of its major version, and runs each harness’s update.py.↩︎

  60. ATTIC_TOKEN is needed except on fork PRs.↩︎

  61. The update workflow approves runs GitHub holds back for automation-created pull requests and squash-merges after the checks required by Require CI on main pass.↩︎

  62. The features are plugins, gateway, kolu, spec, and koluLaunch, which loads koluPlugin through AGENT_DISTRO_PLUGINS.↩︎

  63. auth = "anthropic" | "openai" | "gateway".↩︎

  64. The explicit profileName argument supports profile-specific registration, such as Codex’s marketplace name.↩︎

  65. The picker, bundles, outputs, reserved names, checks, and daily report all discover it.↩︎

  66. For an adapter that needs more than shell: translating plugin descriptions into the harness’s config, merging user state at launch.↩︎

  67. Packages can name updated, upstream, koluFixture, or recordFixture.↩︎

  68. Use import ../../test/guest-script.nix ./tests/check.py to run a guest script as the unprivileged VM user.↩︎