Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
59 changes: 59 additions & 0 deletions .agents/skills/nixo-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
name: nixo-cli
description: Use when requests involve nixo or nixosandbox sandbox lifecycle operations, catalog queries, session-id workflows, or CLI errors such as 'Could not find flake.nix' and create flag conflicts.
---

# nixo CLI

Use this skill for runtime-agnostic workflows around the `nixo` command line.

## When not to use

- Do not use this skill for Nix flake authoring or package derivation work unless the task explicitly centers on the `nixo` CLI workflow.
- Do not use this skill when the user only needs raw `nix build`, `nix develop`, or `bubblewrap` commands outside a sandbox session flow.
- Do not use this skill when another repo-local workflow overrides the CLI, such as project-specific wrapper scripts or extension-only APIs.

## Naming

- Use `nixo` as the default command in all new instructions, prompts, and automation.
- Use `nixosandbox` only when the user explicitly asks for the legacy name or when `nixo` is unavailable.
- Keep flags and behavior identical across both names.

## Core workflow

- Discover available packages with `catalog`.
- Create a sandbox with `create`, using `--with` for package lists or `--profile` for named presets.
- Use `--json` when the output will be parsed by another tool or agent.
- Run commands inside an existing sandbox with `exec <session-id> -- <command...>`.
- Inspect session state with `list` and `status`.
- Remove sessions with `destroy` when they are no longer needed.

## Safe defaults

- Prefer `--network off` unless the task explicitly needs downloads or live network access.
- Prefer narrow package sets over broad sandboxes.
- Prefer machine-readable output when the result feeds another step.
- Use `list` before `destroy` if you need to confirm the active session set.
- For automation, capture `sessionId` from `create --json` output and reuse it for `exec`, `status`, and `destroy`.

## Input and output expectations

- Prefer plain-text `nixo` commands for human guidance and shell examples.
- Prefer `--json` for agent-to-agent handoff, scripts, or any step that will parse command output.
- Treat `create --json` as the canonical machine-readable entrypoint because it returns the `sessionId` needed for later steps.
- Keep JSON consumers on `nixo catalog --json` unless they explicitly need grouped categories, then use `nixo catalog --json --grouped`.
- Emit `nixosandbox` only as a compatibility fallback when `nixo` is unavailable or the user asks for the legacy name.

## Common failure patterns

- `Could not find flake.nix`:
- set `NIXOSANDBOX_FLAKE_ROOT` to a directory containing `flake.nix`, or run from the repo root.
- `create` option conflicts:
- never combine `--with`, `--profile`, and `--spec` in the same command.
- Invalid package names with `--with`:
- confirm package names through `catalog` first.

## When to read more

- [Quick reference](references/quick-reference.md)
- [Troubleshooting](references/troubleshooting.md)
31 changes: 31 additions & 0 deletions .agents/skills/nixo-cli/references/quick-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Quick Reference

## Common commands

```bash
nixo catalog
nixo catalog --json
nixo create --with bash,coreutils --network off --json
nixo create --profile strict --json
nixo exec <session-id> -- echo hello
nixo exec <session-id> --json -- python3 -c "print('hello')"
nixo list
nixo status <session-id>
nixo destroy <session-id>
```

## Typical patterns

- Package discovery:
- use `catalog` to find names before creating a sandbox
- New sandbox:
- `create --with <packages> --network off --json`
- Reusing a session:
- `exec <session-id> -- <command...>`
- Cleanup:
- `destroy <session-id>` after the work is finished

## Alias note

- `nixosandbox` is the compatibility alias.
- Prefer `nixo` in new instructions and examples.
41 changes: 41 additions & 0 deletions .agents/skills/nixo-cli/references/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Troubleshooting

## `nixo` is not found

- Check whether the primary binary is installed.
- Try `nixosandbox` if you are on an older setup that still exposes the compatibility alias.
- Verify the PATH points to the directory that contains the installed binary.

## `catalog` or `create` fails early

- Confirm the command name is correct and the binary is the expected one.
- Re-run with `--json` only if you need structured output; it does not fix command errors.
- If using `create --with`, make sure package names are valid and comma-separated.

## `Could not find flake.nix`

- Set flake root explicitly:
- `export NIXOSANDBOX_FLAKE_ROOT=/path/to/nixo`
- Verify the path:
- `test -f "$NIXOSANDBOX_FLAKE_ROOT/flake.nix" && echo ok`
- Retry:
- `nixo catalog --json`
- Alternative:
- run commands from the repository root that contains `flake.nix`.

## Sandbox creation problems

- Prefer `--network off` unless the task needs network access.
- If the sandbox setup references a profile, confirm the profile exists and matches the intended package set.
- If the CLI reports a missing package, re-check the package name returned by `catalog`.

## Session execution problems

- Use `status <session-id>` to confirm the session still exists.
- Use `list` to verify the ID before calling `exec` or `destroy`.
- If a command fails inside the sandbox, distinguish between sandbox setup failure and command exit status.

## Compatibility issues

- If instructions mention `nixosandbox`, treat them as valid alias usage, not a different tool.
- When documenting or prompting new workflows, prefer `nixo` so the canonical name stays consistent.
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,29 @@ jobs:
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox catalog --json | python3 -m json.tool > /dev/null
echo "Catalog JSON is valid"

- name: Test grouped catalog output
run: |
grouped_json=$(NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox catalog --json --grouped)
echo "$grouped_json" | python3 -m json.tool > /dev/null
echo "$grouped_json" | python3 -c '
import json, sys
data = json.load(sys.stdin)
missing = [key for key in ["agentCategories", "tools"] if key not in data]
if missing:
print(f"ERROR: grouped catalog JSON missing keys: {missing}", file=sys.stderr)
sys.exit(1)
'
echo "Grouped catalog JSON is valid"

- name: Verify CLI alias help parity
run: |
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixo --help > "$tmpdir/nixo-help"
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox --help > "$tmpdir/nixosandbox-help"
diff -u "$tmpdir/nixo-help" "$tmpdir/nixosandbox-help"
echo "nixo and nixosandbox help output match"

- name: Verify catalog agent count and new packages
run: |
catalog_json=$(NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox catalog --json)
Expand Down
81 changes: 81 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Release

on:
push:
tags:
- "v*.*.*"
- "v*.*.*-*"

permissions:
contents: write

jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
runner: macos-14
- target: x86_64-apple-darwin
runner: macos-13
- target: x86_64-unknown-linux-gnu
runner: ubuntu-24.04
steps:
- uses: actions/checkout@v6

- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- uses: swatinem/rust-cache@v2
with:
workspaces: crates/nixosandbox

- name: Build release binaries
working-directory: crates/nixosandbox
run: cargo build --locked --release --target "${{ matrix.target }}" --bin nixo --bin nixosandbox

- name: Package release artifact
shell: bash
run: |
set -euo pipefail
target="${{ matrix.target }}"
package_dir="dist/nixo-${target}"
mkdir -p "$package_dir/bin" "$package_dir/flake"
install -m 755 "crates/nixosandbox/target/${target}/release/nixo" "$package_dir/bin/nixo"
install -m 755 "crates/nixosandbox/target/${target}/release/nixosandbox" "$package_dir/bin/nixosandbox"
install -m 644 flake.nix "$package_dir/flake/flake.nix"
install -m 644 flake.lock "$package_dir/flake/flake.lock"
cp -R nix "$package_dir/flake/"
tar -C "$package_dir" -czf "dist/nixo-${target}.tar.gz" bin flake
if [[ "${RUNNER_OS}" == "macOS" ]]; then
shasum -a 256 "dist/nixo-${target}.tar.gz" > "dist/nixo-${target}.tar.gz.sha256"
else
sha256sum "dist/nixo-${target}.tar.gz" > "dist/nixo-${target}.tar.gz.sha256"
fi

- uses: actions/upload-artifact@v4
with:
name: nixo-${{ matrix.target }}
path: |
dist/nixo-${{ matrix.target }}.tar.gz
dist/nixo-${{ matrix.target }}.tar.gz.sha256
if-no-files-found: error

release:
name: Publish GitHub release
needs: build
runs-on: ubuntu-24.04
steps:
- uses: actions/download-artifact@v4
with:
pattern: nixo-*
path: release-assets
merge-multiple: true

- uses: softprops/action-gh-release@v2
with:
files: release-assets/*
98 changes: 98 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# AGENTS.md

This file provides guidance to coding agents when working in this repository.

## Build and test commands

### Rust CLI
```bash
cd crates/nixosandbox
cargo build # build
cargo test # run all tests
cargo test session::tests # run tests in one module
cargo test metadata_roundtrip # run a single test by name
```

### TypeScript extension
```bash
cd packages/pi-sandbox-extension
npm install
npx tsc --noEmit # typecheck only
npm run build # compile to dist/
```

### Nix
```bash
nix flake check --accept-flake-config
nix build --accept-flake-config .#nixosandbox # build CLI as Nix package
nix eval --accept-flake-config .#catalog.agents --apply 'x: builtins.attrNames x'
nix eval --accept-flake-config .#catalog.tools --apply 'x: builtins.attrNames x'
```

### CLI smoke test (Linux only, requires bwrap)
```bash
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixo catalog --json --grouped
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixo create --with bash,coreutils --network off --json
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixo exec <session-id> -- echo hello
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox --help # compatibility alias
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixo destroy <session-id>
```

## Architecture

### Core data flow

1. **User runs** `nixo create --with claude-code,bash --network off`
2. **nix.rs** resolves package names via `build_with_catalog()` which generates a Nix expression calling `mkAgentSandbox`
3. **mkAgentSandbox.nix** resolves names from the catalog (agents first, then tools) and delegates to `mkSandboxRootfs`
4. **mkSandboxRootfs.nix** uses `pkgs.buildEnv` to merge packages, then creates a rootfs directory with symlinks into `/nix/store`
5. **session.rs** creates a session directory under `~/.local/share/nixosandbox/sessions/<id>/` with metadata, workspace, home, and cache dirs
6. **User runs** `nixo exec <id> -- command`
7. **plan_builder.rs** constructs bwrap argv: `--ro-bind <rootfs> /`, `--ro-bind /nix/store /nix/store`, writable bind mounts for workspace/home/cache, namespace flags, env vars
8. **lib.rs** spawns bwrap (detected path from `bubblewrap::detect()`) with the constructed argv

### Key design decisions

- **Primary CLI name is nixo**: `src/bin/nixo.rs` is the canonical entrypoint, and `src/bin/nixosandbox.rs` invokes the same app for compatibility.
- **Profile field is overloaded**: `session.profile` stores either a built-in profile name (e.g., `"strict"`) which maps to `nix/profiles/<name>.json`, or `"custom:<pkg1>,<pkg2>"` for `--with` sessions. Code must check `meta.profile.starts_with("custom:")` before calling `load_profile()`.
- **Rootfs symlinks require Nix store**: The rootfs contains absolute symlinks into `/nix/store`. bwrap must bind-mount `/nix/store` read-only, and the rootfs must have `/nix/store` as an empty mount point directory.
- **macOS support via Docker sidecar**: On non-Linux, `bubblewrap::detect()` tries a Docker sidecar container (`nixosandbox-sidecar`) with bwrap inside. Session paths are rewritten from host to container paths via `docker::rewrite_path()`.
- **Package name validation**: `nix::validate_package_name()` rejects names not matching `[a-zA-Z0-9_.-]+` to prevent Nix expression injection via `--with`.
- **Network mode storage**: For `--with` sessions, the network mode is stored in `session.network` (not derivable from a profile file). For built-in profiles, it's in the profile JSON.

### Rust module responsibilities

| Module | Owns |
|--------|------|
| `cli.rs` | clap argument parsing |
| `lib.rs` | command dispatch, `cmd_create`, `cmd_exec`, `cmd_catalog`, etc. |
| `session.rs` | session CRUD, metadata serialization, directory layout |
| `nix.rs` | `find_flake_root()`, `build_profile()`, `build_with_catalog()`, `query_catalog()` (filters non-derivation attrs via `filterDrvs` before reading `.meta.description`) |
| `plan_builder.rs` | bwrap argv construction (`--ro-bind`, `--bind`, `--unshare-*`, `--setenv`) |
| `bubblewrap.rs` | bwrap detection: `NIXOSANDBOX_BWRAP_PATH` env var, then `which bwrap`, Docker fallback on macOS |
| `docker.rs` | Docker sidecar lifecycle (find, start, create, image build) |
| `spec.rs` | profile/spec loading from JSON, validation |

### Nix module responsibilities

| File | Owns |
|------|------|
| `nix/catalog.nix` | Unified `{ agents, tools }` attrset — agents is a full dynamic passthrough of `llm-agents-pkgs` (no whitelist), tools from nixpkgs |
| `nix/mkSandboxRootfs.nix` | Builds rootfs directory tree from package list (symlinks, /etc, certs) |
| `nix/mkAgentSandbox.nix` | Resolves catalog names to packages, delegates to mkSandboxRootfs |
| `nix/profiles/*.json` | Built-in profile specs (strict, build-install, offline-review, debug-network) |

### TypeScript extension (packages/pi-sandbox-extension)

Provides Pi coding agent integration. Key files:
- `cli-client.ts` — spawns the `nixo` CLI as a subprocess (`nixosandbox` remains a compatibility alias), provides `createSession()`, `execCommand()`, `catalogPackages()`
- `extension.ts` — registers Pi tools: `sandboxRun`, `sandboxReadFile`, `sandboxWriteFile`, `sandboxListFiles`, `sandboxSessionInfo`, `sandboxBrowser`, `sandboxCatalog`
- `contract.ts` — TypeScript type definitions for the NDJSON protocol

## Session tests use env var serialization

Tests in `session.rs` mutate `NIXOSANDBOX_DATA_DIR` to use temp directories. They serialize via `static ENV_LOCK: Mutex<()>` acquired in `with_temp_data_dir()`. If adding session tests, always use this helper.

## CI runs on nixos-25.11

The flake pins `nixpkgs` to `nixos-25.11`. CI uses `cachix/install-nix-action@v30` with the numtide binary cache. Agent smoke tests use system apt bwrap (setuid) because Nix-built bwrap lacks setuid and fails on Ubuntu 24.04's AppArmor user namespace restrictions.
20 changes: 11 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,29 @@ nix eval --accept-flake-config .#catalog.tools --apply 'x: builtins.attrNames x'

### CLI smoke test (Linux only, requires bwrap)
```bash
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox catalog --json
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox create --with bash,coreutils --network off --json
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox exec <session-id> -- echo hello
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox destroy <session-id>
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixo catalog --json --grouped
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixo create --with bash,coreutils --network off --json
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixo exec <session-id> -- echo hello
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox --help # compatibility alias
NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixo destroy <session-id>
```

## Architecture

### Core data flow

1. **User runs** `nixosandbox create --with claude-code,bash --network off`
1. **User runs** `nixo create --with claude-code,bash --network off`
2. **nix.rs** resolves package names via `build_with_catalog()` which generates a Nix expression calling `mkAgentSandbox`
3. **mkAgentSandbox.nix** resolves names from the catalog (agents first, then tools) and delegates to `mkSandboxRootfs`
4. **mkSandboxRootfs.nix** uses `pkgs.buildEnv` to merge packages, then creates a rootfs directory with symlinks into `/nix/store`
5. **session.rs** creates a session directory under `~/.local/share/nixosandbox/sessions/<id>/` with metadata, workspace, home, and cache dirs
6. **User runs** `nixosandbox exec <id> -- command`
6. **User runs** `nixo exec <id> -- command`
7. **plan_builder.rs** constructs bwrap argv: `--ro-bind <rootfs> /`, `--ro-bind /nix/store /nix/store`, writable bind mounts for workspace/home/cache, namespace flags, env vars
8. **main.rs** spawns bwrap (detected path from `bubblewrap::detect()`) with the constructed argv
8. **lib.rs** spawns bwrap (detected path from `bubblewrap::detect()`) with the constructed argv

### Key design decisions

- **Primary CLI name is nixo**: `src/bin/nixo.rs` is the canonical entrypoint, and `src/bin/nixosandbox.rs` invokes the same app for compatibility.
- **Profile field is overloaded**: `session.profile` stores either a built-in profile name (e.g., `"strict"`) which maps to `nix/profiles/<name>.json`, or `"custom:<pkg1>,<pkg2>"` for `--with` sessions. Code must check `meta.profile.starts_with("custom:")` before calling `load_profile()`.
- **Rootfs symlinks require Nix store**: The rootfs contains absolute symlinks into `/nix/store`. bwrap must bind-mount `/nix/store` read-only, and the rootfs must have `/nix/store` as an empty mount point directory.
- **macOS support via Docker sidecar**: On non-Linux, `bubblewrap::detect()` tries a Docker sidecar container (`nixosandbox-sidecar`) with bwrap inside. Session paths are rewritten from host to container paths via `docker::rewrite_path()`.
Expand All @@ -63,7 +65,7 @@ NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox destroy <session-id>
| Module | Owns |
|--------|------|
| `cli.rs` | clap argument parsing |
| `main.rs` | command dispatch, `cmd_create`, `cmd_exec`, `cmd_catalog`, etc. |
| `lib.rs` | command dispatch, `cmd_create`, `cmd_exec`, `cmd_catalog`, etc. |
| `session.rs` | session CRUD, metadata serialization, directory layout |
| `nix.rs` | `find_flake_root()`, `build_profile()`, `build_with_catalog()`, `query_catalog()` (filters non-derivation attrs via `filterDrvs` before reading `.meta.description`) |
| `plan_builder.rs` | bwrap argv construction (`--ro-bind`, `--bind`, `--unshare-*`, `--setenv`) |
Expand All @@ -83,7 +85,7 @@ NIXOSANDBOX_FLAKE_ROOT=$PWD ./result/bin/nixosandbox destroy <session-id>
### TypeScript extension (packages/pi-sandbox-extension)

Provides Pi coding agent integration. Key files:
- `cli-client.ts` — spawns the nixosandbox CLI as a subprocess, provides `createSession()`, `execCommand()`, `catalogPackages()`
- `cli-client.ts` — spawns the `nixo` CLI as a subprocess (`nixosandbox` remains a compatibility alias), provides `createSession()`, `execCommand()`, `catalogPackages()`
- `extension.ts` — registers Pi tools: `sandboxRun`, `sandboxReadFile`, `sandboxWriteFile`, `sandboxListFiles`, `sandboxSessionInfo`, `sandboxBrowser`, `sandboxCatalog`
- `contract.ts` — TypeScript type definitions for the NDJSON protocol

Expand Down
Loading