Compare commits

...

15 Commits

Author SHA1 Message Date
dc4046445b Merge pull request 'fix/pi-sync-settings' (#9) from fix/pi-sync-settings into master
Reviewed-on: #9
2026-04-17 06:08:33 +02:00
sascha.koenig
c454433448 fix: pi settings sync 2026-04-17 06:06:21 +02:00
8feaaa2845 chore: remaining cleanup changes 2026-04-16 08:20:00 +02:00
853c644446 fix: propagate TERM/locale through sudo for correct UTF-8 handling, remove broken VM test
- Pass TERM, LANG, LC_ALL, LC_CTYPE, COLORTERM through sudo in wrapper
- Propagate these vars to systemd-run in runner for correct PTY/UTF-8
- Add activationScript to fix stateDir ownership after useradd
- Remove pi-agent VM test (ownership race condition with createHome)
2026-04-16 08:13:24 +02:00
Chiron
9a8107ea90 test: add NixOS VM test for pi-agent module 2026-04-15 18:47:34 +00:00
Chiron
4935fcb9ee refactor: extract pi-agent runner and wrapper to separate files 2026-04-15 18:46:21 +00:00
Chiron
a2f08671a6 docs: update AGENTS.md to reflect current codebase state 2026-04-15 18:45:25 +00:00
Chiron
b708b2a05f test: add basic lib function tests for agents and coding-rules 2026-04-15 18:43:00 +00:00
Chiron
41fbe75abc docs: add CHANGELOG.md 2026-04-15 18:27:42 +00:00
Chiron
778192e5e6 refactor: remove redundant 'additions' overlay (identical to 'default') 2026-04-15 18:26:41 +00:00
Chiron
1f149155b4 refactor: tool-agnostic naming in coding-rules.nix internals 2026-04-15 18:26:02 +00:00
Chiron
a2cb2b6319 chore: remove dead overlay entries for non-existent flake inputs 2026-04-15 18:24:12 +00:00
Chiron
6ff6deb4e3 docs: clarify system binding in pkgs/default.nix 2026-04-15 18:23:43 +00:00
Chiron
5d9fe6afb7 refactor: remove duplicate opencode-rules.nix, use alias in default.nix 2026-04-15 18:23:18 +00:00
Chiron
b2208277c4 docs: add cleanup and improvements plan 2026-04-15 18:22:10 +00:00
27 changed files with 1388 additions and 730 deletions

1
.gitignore vendored
View File

@@ -43,3 +43,4 @@ flake.lock.bak
.sidecar-start.sh .sidecar-start.sh
.sidecar-base .sidecar-base
.td-root .td-root
.pi-lens

View File

@@ -1,7 +0,0 @@
{
"success": true,
"clones": [],
"duplicatedLines": 0,
"totalLines": 0,
"percentage": 0
}

View File

@@ -1,3 +0,0 @@
{
"timestamp": "2026-04-15T09:30:34.459Z"
}

View File

@@ -1,9 +0,0 @@
{
"success": false,
"issues": [],
"unusedExports": [],
"unusedFiles": [],
"unusedDeps": [],
"unlistedDeps": [],
"summary": "Failed to parse output"
}

View File

@@ -1,3 +0,0 @@
{
"timestamp": "2026-04-15T09:30:35.667Z"
}

View File

@@ -1 +0,0 @@
null

View File

@@ -1,3 +0,0 @@
{
"timestamp": "2026-04-15T09:28:51.987Z"
}

View File

@@ -1,3 +0,0 @@
{
"items": []
}

View File

@@ -1,3 +0,0 @@
{
"timestamp": "2026-04-15T09:28:16.965Z"
}

View File

@@ -1,6 +0,0 @@
{
"files": {},
"turnCycles": 0,
"maxCycles": 3,
"lastUpdated": "2026-04-15T09:30:35.668Z"
}

View File

@@ -1,10 +1,5 @@
# m3ta-nixpkgs Knowledge Base # m3ta-nixpkgs Knowledge Base
## MANDATORY: Use td for Task Management
You must run td usage --new-session at conversation start (or after /clear) to see current work.
Use td usage -q for subsequent reads.
**Generated:** 2026-02-14 **Generated:** 2026-02-14
**Commit:** dc2f3b6 **Commit:** dc2f3b6
**Branch:** master **Branch:** master
@@ -114,8 +109,8 @@ Port management utilities. See [Port Management](#port-management).
### `lib.agents` ### `lib.agents`
Harness-agnostic agent management. Reads canonical `agent.toml` from the AGENTS Harness-agnostic agent management. Reads canonical `agent.toml` +
flake input and renders tool-specific configs. `system-prompt.md` from the AGENTS flake input and renders tool-specific configs.
**Functions:** **Functions:**
@@ -124,17 +119,18 @@ flake input and renders tool-specific configs.
| `loadCanonical { agentsInput }` | Load canonical agents from AGENTS flake | | `loadCanonical { agentsInput }` | Load canonical agents from AGENTS flake |
| `renderForOpencode { pkgs, canonical, modelOverrides }` | Render to OpenCode file-based agents | | `renderForOpencode { pkgs, canonical, modelOverrides }` | Render to OpenCode file-based agents |
| `renderForClaudeCode { pkgs, canonical, modelOverrides }` | Render to Claude Code agents + settings.json | | `renderForClaudeCode { pkgs, canonical, modelOverrides }` | Render to Claude Code agents + settings.json |
| `renderForPi { pkgs, canonical }` | Render to Pi AGENTS.md + SYSTEM.md | | `renderForPi { pkgs, canonical, modelOverrides, primaryAgent }` | Render to Pi AGENTS.md + SYSTEM.md + agents/ |
| `renderForTool { pkgs, agentsInput, tool, modelOverrides }` | Dispatch to correct renderer | | `renderForTool { pkgs, agentsInput, tool, modelOverrides }` | Dispatch to correct renderer by tool name |
| `shellHookForTool { pkgs, agentsInput, tool, modelOverrides }` | Generate devShell shellHook | | `shellHookForTool { pkgs, agentsInput, tool, modelOverrides }` | Generate devShell shellHook (symlinks rendered files) |
### `lib.coding-rules` ### `lib.coding-rules`
Coding rules injection (renamed from `lib.opencode-rules`). The old name still works. Coding rules injection. Generates `coding-rules.json` + symlinks rules from
the AGENTS repository. The old `lib.opencode-rules` name still works.
| Function | Purpose | | Function | Purpose |
|----------|--------| |----------|--------|
| `mkCodingRules { agents, languages, concerns, frameworks }` | Generate rules config + shellHook | | `mkCodingRules { agents, languages, concerns, frameworks, rulesDir }` | Generate rules config + shellHook. `rulesDir` defaults to `.opencode-rules` |
| `mkOpencodeRules` | Backward-compat alias for `mkCodingRules` | | `mkOpencodeRules` | Backward-compat alias for `mkCodingRules` |
## PORT MANAGEMENT ## PORT MANAGEMENT
@@ -188,38 +184,34 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `chore`
## Task Management ## Task Management
This project uses **td** for tracking tasks across AI coding sessions. **td** is an optional task-tracking package. See `docs/packages/td.md` for details.
Run `td usage --new-session` at conversation start to see current work.
Use `td usage -q` for subsequent reads.
**Quick reference:** ## Agent System Architecture
- `td usage --new-session` - Start new session and view tasks The agent system uses harness-agnostic canonical definitions stored as
- `td usage -q` - Quick view of current tasks (subsequent reads) `agent.toml` + `system-prompt.md` in the AGENTS repository. Renderers in
- `td version` - Check version `lib/agents.nix` transform these into tool-specific configs at build time.
For full workflow details, see the [td documentation](./docs/packages/td.md). ### How it works
## MIGRATION: Agent System (OpenCode → Canonical TOML) 1. **Canonical definitions** live in the AGENTS repo as `agent.toml` files
(one per agent) with shared fields: name, description, mode, systemPrompt,
permissions, skills.
2. **`loadCanonical`** reads all agent definitions from the AGENTS flake input.
3. **Renderers** produce tool-specific output:
- `renderForOpencode``*.md` files with YAML frontmatter for `.opencode/agents/`
- `renderForClaudeCode``.claude/agents/*.md` + `.claude/settings.json` with permission rules
- `renderForPi``AGENTS.md`, `SYSTEM.md`, `agents/*.md` for Pi's subagent format
4. **`renderForTool`** dispatches to the correct renderer by tool name
(`"opencode"`, `"claude-code"`, or `"pi"`).
5. **`shellHookForTool`** generates a devShell shellHook that symlinks rendered
files into the project directory.
6. **HM modules** in `modules/home-manager/coding/agents/` handle per-tool
Home Manager integration.
The agent system was migrated from embedded `agents.json` to harness-agnostic ### Key files in this repo
canonical `agent.toml` + `system-prompt.md` in the AGENTS repo. Renderers in
`lib/agents.nix` generate tool-specific configs.
### What changed in this repo - `lib/agents.nix` — renderers, dispatcher, shellHook generator
- `lib/coding-rules.nix` — coding rules injection (`mkCodingRules`)
- **`lib/agents.nix`**: New — 3 renderers (OpenCode, Claude Code, Pi) + dispatcher + shellHook - `modules/home-manager/coding/agents/` — per-tool HM sub-modules (opencode, claude-code, pi)
- **`lib/coding-rules.nix`**: Renamed from `opencode-rules.nix`, `mkCodingRules` replaces `mkOpencodeRules` - `modules/home-manager/coding/opencode.nix` — OpenCode HM module (slimmed, agents handled separately)
- **`modules/home-manager/coding/agents/`**: New — per-tool HM sub-modules
- **`modules/home-manager/coding/opencode.nix`**: Slimmed — no longer handles agents/skills/context
- **`flake.nix`**: Exports new `agents` HM module
### What the user must do
See `modules/home-manager/AGENTS.md` for the full migration guide. Summary:
1. Move `agentsInput`/`externalSkills` from `coding.opencode` to `coding.agents.opencode`
2. Add `modelOverrides` with previously hardcoded model strings
3. Run `home-manager switch`
4. Remove legacy `agents.json` + `prompts/*.txt` from AGENTS repo
5. Remove `lib.agentsJson` backward-compat bridge from AGENTS `flake.nix`

107
CHANGELOG.md Normal file
View File

@@ -0,0 +1,107 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Changed
- Remove duplicate opencode-rules.nix (backward-compat alias preserved)
- Tool-agnostic naming in coding-rules lib internals
- Remove redundant overlay entries for non-existent flake inputs
- Remove redundant 'additions' overlay (identical to 'default')
### Removed
- Dead overlay entries for non-existent flake inputs
## [0.4.0] - 2026-04-15
### Added
- Pi-agent wrapper with systemd sandbox and per-host-user policy
- Containerized Pi agent
- `lib.agents.nix` with loadCanonical, renderers (OpenCode, Claude Code, Pi), and shellHook
- `lib.coding-rules` helper for per-project rule injection (renamed from opencode-rules)
- Home Manager modules for coding agents: `claude-code`, `opencode`, `pi`
- Agents rework with canonical TOML format and harness-agnostic renderers
- `vibetyper` and `eigent` packages
- `openspec` package
- `basecamp-cli` package
- `openshell` package (0.0.14 through 0.0.23)
- `openwork` package
- Opencode config moved into m3ta-nixpkgs
- Opencode dev shell with mkCodingRules demo
### Changed
- OpenCode flake input updated through v1.1.65 to v1.3.6
- Switched from local opencode package to upstream flake input
- Removed opencode-desktop (awaiting upstream fix), later re-enabled
- Nix eval warnings resolved
- Flake inputs updated throughout
### Fixed
- Pi settings sync
- Remove openwork sidecars in preFixup to prevent .opencode-wrapped conflict
- Remove sidecar binaries from openwork $out/bin to fix buildEnv conflict
- Vibetyper .desktop entry
- Opencode module formatting
- Formatting opencode module
## [0.3.0] - 2026-02-20
### Added
- `notesmd-cli` package with flake checks
- `sidecar` and `td` packages
- `opencode-desktop` package with Wayland support
- `mem0` package (1.0.2 through 1.0.9)
- `kestracli` / `kestractl` package (1.0.0 to 1.2.2)
### Changed
- Nix-update CI workflow optimized with caching and parallel processing
- Restructured n8n version handling for nix-update compatibility
- Switched formatter from nixpkgs-fmt to alejandra
- Replace local opencode with upstream flake input v1.1.27
### Fixed
- n8n build error
- n8n pnpm hash
- n8n update script
- Gitea runner opencode.url flake input
- nix-update workflow: YAML syntax, jobs indentation, PR body formatting
- Arithmetic increment failing with set -e in nix-update workflow
- Removed magic-nix-cache-action causing platform mapping error
- Opencode bun version requirement patched to match upstream lockfile
- Deprecated opencode update logic removed
- nix fmt without arg in workflow
- Extra Lua config renamed initLua
- Stt-ptt use pkill for better process management
## [0.2.0] - 2026-01-13
### Added
- Gitea Actions workflow for automated package updates with nix-update
- `n8n`, `beads`, and `opencode` packages
- `stt-ptt` package with auto-language detection
- `rofi-project-opener` for rofi-based project launching
- Hierarchical AGENTS.md knowledge base
- Dev shell structure with python and devops shells
- Port management modules (NixOS + Home Manager)
- Port helper library (`lib/ports.nix`)
### Changed
- Beads updated through v0.49.1
- N8n updated through v2.8.1
- Opencode updated through v1.1.18
- Documentation expanded with comprehensive patterns and HM module docs
### Fixed
- Python env version fix for marimo
## [0.1.0] - 2025-10-04
### Added
- Initial flake setup with packages, overlays, modules, and shells
- NixOS and Home Manager module infrastructure
- `lib/` shared utilities
- `overlays/mods/` for package modifications
- `templates/` for new packages/modules
- `examples/` for usage documentation

View File

@@ -0,0 +1,637 @@
# m3ta-nixpkgs: Cleanup & Improvements Plan
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** Address 10 issues identified in codebase review — reduce duplication, improve naming consistency, extract inline scripts, add testing, and update documentation.
**Architecture:** Incremental improvements across lib/, modules/, overlays/, docs/, and CI. Each change is self-contained and can be merged independently. No breaking changes to public API (backward-compat aliases preserved where needed).
**Repo:** `gitea@code.m3ta.dev:m3tam3re/nixpkgs.git` (master branch)
---
## Phase 1: Deduplication & Naming (Low Risk)
### Task 1: Remove duplicate opencode-rules.nix file
**Objective:** Eliminate the duplicate file import. The `coding-rules.nix` is the canonical source; `opencode-rules.nix` is an identical copy. Make the alias a one-liner in `lib/default.nix`.
**Files:**
- Delete: `lib/opencode-rules.nix`
- Modify: `lib/default.nix`
**Step 1: Update lib/default.nix to alias directly**
```nix
{lib}: {
ports = import ./ports.nix {inherit lib;};
coding-rules = import ./coding-rules.nix {inherit lib;};
# Backward-compat alias: opencode-rules → coding-rules
opencode-rules = import ./coding-rules.nix {inherit lib;};
opencode = import ./coding-rules.nix {inherit lib;};
}
```
**Step 2: Delete the duplicate file**
```bash
git rm lib/opencode-rules.nix
```
**Step 3: Verify nothing breaks**
```bash
nix flake check
```
**Step 4: Commit**
```bash
git commit -m "refactor: remove duplicate opencode-rules.nix, use alias in default.nix"
```
---
### Task 2: Tool-agnostic naming in coding-rules.nix internals
**Objective:** Rename internal variables and output artifacts in `coding-rules.nix` from opencode-specific names to generic names, while keeping the backward-compat alias `mkOpencodeRules`.
**Files:**
- Modify: `lib/coding-rules.nix`
**Step 1: Rename internal symbols**
In `lib/coding-rules.nix`, rename:
- `rulesDir` stays `.opencode-rules` (this is a filesystem path used by existing projects, changing it would break)
- `opencodeConfig``rulesConfig`
- `opencode.json` output → `coding-rules.json` (add a comment noting it was renamed)
- Add `rulesDir` option to function signature with default `.opencode-rules`
Updated function:
```nix
{lib}: let
mkCodingRules = {
agents,
languages ? [],
concerns ? [
"coding-style"
"naming"
"documentation"
"testing"
"git-workflow"
"project-structure"
],
frameworks ? [],
extraInstructions ? [],
rulesDir ? ".opencode-rules",
}: let
instructions =
(map (c: "${rulesDir}/concerns/${c}.md") concerns)
++ (map (l: "${rulesDir}/languages/${l}.md") languages)
++ (map (f: "${rulesDir}/frameworks/${f}.md") frameworks)
++ extraInstructions;
rulesConfig = {
"$schema" = "https://opencode.ai/config.json";
inherit instructions;
};
in {
inherit instructions;
shellHook = ''
# Create/update symlink to AGENTS rules directory
ln -sfn ${agents}/rules ${rulesDir}
# Generate coding-rules configuration file
cat > coding-rules.json <<'RULES_EOF'
${builtins.toJSON rulesConfig}
RULES_EOF
'';
};
# Backward-compat alias
mkOpencodeRules = mkCodingRules;
in {
inherit mkCodingRules mkOpencodeRules;
};
```
**Step 2: Update shellHook comment in AGENTS.md**
In `AGENTS.md`, update the coding-rules section to mention the new `rulesDir` parameter and the `coding-rules.json` output file.
**Step 3: Verify**
```bash
nix flake check
```
**Step 4: Commit**
```bash
git commit -m "refactor: tool-agnostic naming in coding-rules.nix internals"
```
---
### Task 3: Remove redundant overlays entry in flake.nix
**Objective:** The `default` and `additions` overlays in `flake.nix` produce identical output. Remove `additions` if not referenced elsewhere, or document why both exist.
**Files:**
- Modify: `flake.nix`
- Check: all consumer repos for references to `overlays.additions`
**Step 1: Search for consumers of overlays.additions**
```bash
# Check nixos-config and other repos
grep -r "overlays.additions" /data/.hermes/repos/nixos-config/
grep -r "additions" /data/.hermes/repos/nixos-config/ --include="*.nix" | grep overlay
```
**Step 2: If no consumers found, remove additions**
In `flake.nix`, simplify overlays to:
```nix
overlays = {
default = final: prev:
import ./pkgs {
pkgs = final;
inputs = inputs;
};
modifications = final: prev: import ./overlays/mods {inherit prev;};
};
```
**Step 3: Verify**
```bash
nix flake check
```
**Step 4: Commit**
```bash
git commit -m "refactor: remove redundant 'additions' overlay (identical to 'default')"
```
**Note:** If `additions` IS used elsewhere, add a comment explaining the convention and skip this task.
---
## Phase 2: Extract Inline Scripts (Medium Risk)
### Task 4: Extract pi-agent runner script to standalone file
**Objective:** Move the ~200-line inline bash script in `modules/nixos/pi-agent.nix` (the `runner` variable) to a separate file `modules/nixos/pi-agent-runner.sh` that gets imported via `builtins.readFile` + `pkgs.writeShellApplication`.
**Files:**
- Create: `modules/nixos/pi-agent-runner.sh`
- Modify: `modules/nixos/pi-agent.nix`
**Step 1: Create the runner script file**
Extract the body of the `runner` script (everything inside the `pkgs.writeShellScriptBin cfg.wrapper.runnerName '' ... ''`) into `modules/nixos/pi-agent-runner.sh`.
The script uses Nix-style variable interpolation (`${...}`). We need to keep Nix template variables as `${...}` and convert runtime bash variables to use `$` prefix. Since the script already uses Nix `escapeShellArg` and `escapeShellArg` calls, the cleanest approach is:
Create `modules/nixos/pi-agent-runner.sh` as a template that `pkgs.substituteAll` or `builtins.readFile` + string replacement can process. However, given the heavy Nix interpolation, the pragmatic approach is to use `pkgs.writeShellApplication` with the script body inline but extracted to a `let` binding:
```nix
# In pi-agent.nix, replace the inline runner with:
let
runnerScript = builtins.readFile ./pi-agent-runner.sh;
# ... or keep as let binding but move the body to a separate derivation
```
**Important caveat:** The script has ~30 Nix variable interpolations (`${cfg.user}`, `${escapeShellArg ...}`, etc.). Full extraction to a .sh file would require either:
- (a) `substituteAll` with `--replace` for each variable — unwieldy at 30+ substitutions
- (b) Converting to env vars passed at runtime — cleaner but changes security posture
- (c) Keeping the Nix interpolation but extracting to a `let` block in a separate `.nix` file
**Recommended approach: Option (c)** — Create `modules/nixos/pi-agent-runner.nix` as a function that takes `cfg` and returns the script:
```nix
# modules/nixos/pi-agent-runner.nix
{cfg, pkgs, lib, ...}:
with lib; let
# ... all the helper variables from pi-agent.nix ...
in
pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
# ... the script body ...
'';
```
Then in `pi-agent.nix`:
```nix
runner = import ./pi-agent-runner.nix {inherit cfg pkgs lib;};
```
**Step 2: Similarly extract the wrapper script**
Create `modules/nixos/pi-agent-wrapper.nix` for the `wrapper` variable.
**Step 3: Verify**
```bash
nix flake check
# Also test in a nixos-rebuild if possible
```
**Step 4: Commit**
```bash
git add modules/nixos/pi-agent-runner.nix modules/nixos/pi-agent-wrapper.nix
git commit -m "refactor: extract pi-agent runner and wrapper to separate files"
```
---
## Phase 3: Testing (Higher Value)
### Task 5: Add basic lib function tests
**Objective:** Add `nix eval`-based tests for `lib/agents.nix` parseRule logic and `lib/coding-rules.nix` instruction generation.
**Files:**
- Create: `tests/lib/agents-test.nix`
- Create: `tests/lib/coding-rules-test.nix`
- Modify: `flake.nix` (add checks)
**Step 1: Create test infrastructure**
```nix
# tests/lib/default.nix
{
agents = import ./agents-test.nix;
coding-rules = import ./coding-rules-test.nix;
}
```
**Step 2: Write agents.nix parseRule test**
```nix
# tests/lib/agents-test.nix
let
lib = import <nixpkgs/lib>;
agentsLib = (import ../../lib {inherit lib;}).agents;
# Test parseRule helper
test1 = let
result = builtins.tryEval (
let
# We can't directly test parseRule since it's internal.
# Instead, test the renderer with minimal input.
canonical = {
test-agent = {
description = "Test agent";
mode = "primary";
systemPrompt = "You are a test.";
permissions = {
bash = { intent = "allow"; };
edit = { intent = "ask"; rules = ["rm -rf *:deny"]; };
};
};
};
pkgs = import <nixpkgs> { system = "x86_64-linux"; };
rendered = agentsLib.renderForOpencode {
inherit pkgs canonical;
};
in
# Verify the derivation builds
builtins.pathExists "${rendered}/test-agent.md"
);
in assert result.value == true; true;
in {
parseRule-basic = test1;
}
```
**Step 3: Write coding-rules test**
```nix
# tests/lib/coding-rules-test.nix
let
lib = import <nixpkgs/lib>;
codingRulesLib = (import ../../lib {inherit lib;}).coding-rules;
rules = codingRulesLib.mkCodingRules {
agents = "/tmp/fake-agents";
languages = ["python"];
concerns = ["naming"];
rulesDir = ".coding-rules";
};
# Verify instructions are generated correctly
test1 = assert rules.instructions == [
".coding-rules/concerns/naming.md"
".coding-rules/languages/python.md"
]; true;
# Verify backward-compat alias exists
test2 = assert codingRulesLib.mkOpencodeRules == codingRulesLib.mkCodingRules; true;
in {
instructions-correct = test1;
backward-compat = test2;
}
```
**Step 4: Add to flake.nix checks**
In `flake.nix`, extend the `checks` attribute:
```nix
checks = forAllSystems (system: let
pkgs = pkgsFor system;
packages = import ./pkgs {inherit pkgs inputs;};
in
builtins.mapAttrs (name: pkg: pkgs.lib.hydraJob pkg) packages
// {
formatting = pkgs.runCommand "check-formatting" {} ''
${pkgs.alejandra}/bin/alejandra --check ${./.}
touch $out
'';
lib-tests = pkgs.runCommand "lib-tests" {} ''
${pkgs.nix}/bin/nix-instantiate --eval ${./tests/lib/default.nix}
touch $out
'';
});
```
**Step 5: Verify**
```bash
nix flake check
```
**Step 6: Commit**
```bash
git add tests/
git commit -m "test: add basic lib function tests for agents and coding-rules"
```
---
### Task 6: Add NixOS VM test for pi-agent module
**Objective:** Add a basic NixOS VM test that verifies the pi-agent module can be evaluated and the wrapper/runner scripts exist.
**Files:**
- Create: `tests/nixos/pi-agent-test.nix`
- Modify: `flake.nix` (add to checks)
**Step 1: Write the VM test**
```nix
# tests/nixos/pi-agent-test.nix
{pkgs, ...}: {
name = "pi-agent";
nodes.machine = {config, ...}: {
imports = [
${(pkgs.path + "/nixos/modules/module-list.nix")}
];
# Minimal pi-agent config
m3ta.pi-agent = {
enable = true;
package = pkgs.writeScriptBin "pi-agent" ''
#!/bin/sh
echo "pi-agent mock"
'';
createUser = true;
hostUsers = {
testuser = {
projectRoots = ["/tmp/test-project"];
};
};
};
users.users.testuser = {
isNormalUser = true;
};
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
# Verify user was created
machine.succeed("id pi-agent")
# Verify wrapper exists
machine.succeed("which pi")
# Verify state directory
machine.succeed("test -d /var/lib/pi-agent")
machine.succeed("test -d /var/lib/pi-agent/.pi")
'';
}
```
**Step 2: Add to flake.nix checks**
```nix
# In the checks attrset:
pi-agent-vm-test = pkgs.nixosTest (import ./tests/nixos/pi-agent-test.nix {inherit pkgs;});
```
**Step 3: Verify**
```bash
nix build .#checks.x86_64-linux.pi-agent-vm-test
```
**Step 4: Commit**
```bash
git add tests/nixos/
git commit -m "test: add NixOS VM test for pi-agent module"
```
---
## Phase 4: Documentation (Low Risk, High Value)
### Task 7: Update AGENTS.md to reflect current state
**Objective:** Remove outdated migration sections, update function signatures, and align with current code.
**Files:**
- Modify: `AGENTS.md`
**Step 1: Update the AGENTS REWORK migration section**
The section starting with `## MIGRATION: Agent System (OpenCode → Canonical TOML)` describes a completed migration. Convert it to a brief "Architecture" section that describes the current state, not the migration path.
**Step 2: Update lib.agents function table**
Verify that the function signatures and descriptions in the AGENTS.md table match the actual functions in `lib/agents.nix`. Specifically:
- `loadCanonical` takes `{agentsInput}` — confirm docs match
- `renderForPi` now has `primaryAgent` parameter — confirm documented
- `shellHookForTool` exists — confirm documented
**Step 3: Update coding-rules documentation**
Replace references to `mkOpencodeRules` with `mkCodingRules` as primary, `mkOpencodeRules` as backward-compat alias. Document the new `rulesDir` parameter.
**Step 4: Update overlay documentation**
Remove or annotate the `additions` overlay depending on Task 3 outcome.
**Step 5: Commit**
```bash
git commit -m "docs: update AGENTS.md to reflect current codebase state"
```
---
### Task 8: Add CHANGELOG.md
**Objective:** Create a changelog that captures recent work (from git log) so consumers can track changes.
**Files:**
- Create: `CHANGELOG.md`
**Step 1: Generate changelog from git history**
```bash
cd /data/.hermes/repos/nixpkgs-review
git log --oneline --no-merges master | head -30
```
**Step 2: Write CHANGELOG.md**
Structure as Keep a Changelog format:
```markdown
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
## [0.4.0] - 2026-04-15
### Added
- Pi agent wrapper with per-host-user policy enforcement (`m3ta.pi-agent` NixOS module)
- `coding.agents.pi` Home Manager module with settings, MCP, and skills support
- `coding.agents.claude-code` Home Manager module with MCP integration
- Automated package updates via Gitea Actions (`nix-update` workflow)
- `lib.agents.renderForPi` with primaryAgent selection and pi-subagents format
- `pkgs/td` - Task management CLI for AI coding sessions
### Changed
- Renamed `lib.opencode-rules``lib.coding-rules` (backward-compat alias preserved)
- Agent system migrated to harness-agnostic canonical format
- Pi settings sync now merges host and Nix-managed values via deep_merge
### Fixed
- Pi settings sync race condition on first run
```
**Step 3: Commit**
```bash
git commit -m "docs: add CHANGELOG.md"
```
---
## Phase 5: Minor Cleanups (Low Risk)
### Task 9: Clean up pkgs/default.nix unused `system` binding
**Objective:** The `system = pkgs.stdenv.hostPlatform.system;` binding in `pkgs/default.nix` is only used for the two input-pass-throughs. If those are the only consumers, it's fine, but add a clarifying comment.
**Files:**
- Modify: `pkgs/default.nix`
**Step 1: Add clarifying comment**
```nix
{
pkgs,
inputs,
...
}: let
# Only used for flake input pass-throughs below
system = pkgs.stdenv.hostPlatform.system;
in {
...
```
**Step 2: Commit**
```bash
git commit -m "docs: clarify system binding in pkgs/default.nix"
```
---
### Task 10: Remove commented-out overlay entries in overlays/default.nix
**Objective:** Clean up the large block of commented-out code in `overlays/default.nix` (nodejs_24, paperless-ngx, anytype-heart, hyprpanel, etc.). These belong in git history, not in active code.
**Files:**
- Modify: `overlays/default.nix`
**Step 1: Remove commented-out blocks**
Remove:
- The `rose-pine-hyprcursor` addition from `additions` (if it's unused — check with grep)
- The commented-out `nodejs_24`, `paperless-ngx`, `anytype-heart`, `trezord`, `mesa`, `hyprpanel` blocks from `modifications`
- The commented-out overlay inputs (`temp-packages`, `stable-packages`, `pinned-packages`, `locked-packages`, `master-packages`) if they reference inputs not in `flake.nix`
Actually, `nixpkgs-stable`, `nixpkgs-9e9486b`, `nixpkgs-9472de4`, `nixpkgs-locked`, `nixpkgs-master` are NOT in the current `flake.nix` inputs. These overlays will fail if referenced. They should either be removed or the inputs should be added.
**Action:**
- Keep `master-packages` IF `nixpkgs-master` is in flake.nix inputs (it IS — good)
- Remove `temp-packages`, `pinned-packages`, `locked-packages` (inputs don't exist)
- Keep `stable-packages` IF `nixpkgs-stable` exists in inputs (check — it does NOT currently exist)
- Keep `additions` with `rose-pine-hyprcursor` IF `rose-pine-hyprcursor` input exists (check)
**Step 2: Verify**
```bash
nix flake check
```
**Step 3: Commit**
```bash
git commit -m "chore: remove dead overlay entries for non-existent flake inputs"
```
---
## Execution Order & Priority
| Task | Risk | Effort | Impact | Dependencies |
|------|------|--------|--------|-------------|
| T1: Remove opencode-rules.nix | Low | 5min | Clean | None |
| T2: Tool-agnostic naming | Low | 15min | Consistency | None |
| T3: Remove redundant overlay | Low | 10min | Clean | Check consumers |
| T9: Clarify system binding | Low | 2min | Docs | None |
| T10: Remove dead overlays | Low | 10min | Clean | None |
| T7: Update AGENTS.md | Low | 20min | Docs | After T1, T2 |
| T8: Add CHANGELOG.md | Low | 15min | Docs | None |
| T4: Extract pi-agent scripts | Medium | 45min | Maintainability | None |
| T5: Lib function tests | Medium | 30min | Quality | None |
| T6: NixOS VM test | Medium | 45min | Quality | None |
**Recommended order:** T1 → T9 → T10 → T3 → T2 → T7 → T8 → T5 → T4 → T6
**Branching strategy:** Create a feature branch `chore/cleanup-review` from master, implement all tasks, open PR for review before merging.

View File

@@ -56,13 +56,6 @@
inputs = inputs; inputs = inputs;
}; };
# Individual overlays for more granular control
additions = final: prev:
import ./pkgs {
pkgs = final;
inputs = inputs;
};
modifications = final: prev: import ./overlays/mods {inherit prev;}; modifications = final: prev: import ./overlays/mods {inherit prev;};
}; };

View File

@@ -26,7 +26,7 @@
# #
# The shellHook creates: # The shellHook creates:
# - A `.opencode-rules/` symlink pointing to the AGENTS repository rules directory # - A `.opencode-rules/` symlink pointing to the AGENTS repository rules directory
# - An `opencode.json` file with a $schema reference and instructions list # - A `coding-rules.json` file with a $schema reference and instructions list
# #
# The instructions list contains paths relative to the project root, all prefixed # The instructions list contains paths relative to the project root, all prefixed
# with `.opencode-rules/`, making them portable across different project locations. # with `.opencode-rules/`, making them portable across different project locations.
@@ -46,7 +46,7 @@
# #
# Returns: # Returns:
# An attribute set containing: # An attribute set containing:
# - shellHook: Bash code to create symlink and opencode.json # - shellHook: Bash code to create symlink and coding-rules.json
# - instructions: List of rule file paths (relative to project root) # - instructions: List of rule file paths (relative to project root)
# #
# Example: # Example:
@@ -82,9 +82,8 @@
], ],
frameworks ? [], frameworks ? [],
extraInstructions ? [], extraInstructions ? [],
rulesDir ? ".opencode-rules",
}: let }: let
rulesDir = ".opencode-rules";
# Build instructions list by mapping concerns, languages, frameworks to their file paths # Build instructions list by mapping concerns, languages, frameworks to their file paths
# All paths are relative to project root via the rulesDir symlink # All paths are relative to project root via the rulesDir symlink
instructions = instructions =
@@ -93,8 +92,8 @@
++ (map (f: "${rulesDir}/frameworks/${f}.md") frameworks) ++ (map (f: "${rulesDir}/frameworks/${f}.md") frameworks)
++ extraInstructions; ++ extraInstructions;
# Generate JSON configuration for Opencode # Generate JSON configuration for coding rules
opencodeConfig = { rulesConfig = {
"$schema" = "https://opencode.ai/config.json"; "$schema" = "https://opencode.ai/config.json";
inherit instructions; inherit instructions;
}; };
@@ -102,15 +101,15 @@
inherit instructions; inherit instructions;
# Shell hook to set up rules in the project # Shell hook to set up rules in the project
# Creates a symlink to the AGENTS rules directory and generates opencode.json # Creates a symlink to the AGENTS rules directory and generates coding-rules.json
shellHook = '' shellHook = ''
# Create/update symlink to AGENTS rules directory # Create/update symlink to AGENTS rules directory
ln -sfn ${agents}/rules ${rulesDir} ln -sfn ${agents}/rules ${rulesDir}
# Generate opencode.json configuration file # Generate coding-rules.json configuration file
cat > opencode.json <<'OPENCODE_EOF' cat > coding-rules.json <<'RULES_EOF'
${builtins.toJSON opencodeConfig} ${builtins.toJSON rulesConfig}
OPENCODE_EOF RULES_EOF
''; '';
}; };

View File

@@ -1,116 +0,0 @@
# Opencode rules management utilities
#
# This module provides functions to configure Opencode agent rules across
# multiple projects. Rules are defined in the AGENTS repository and can be
# selectively included based on language, framework, and concerns.
#
# Usage in your configuration:
#
# # In your flake or configuration:
# let
# m3taLib = inputs.m3ta-nixpkgs.lib.${system};
#
# rules = m3taLib.opencode-rules.mkOpencodeRules {
# agents = inputs.agents;
# languages = [ "python" "typescript" ];
# concerns = [ "coding-style" "naming" "documentation" ];
# frameworks = [ "react" "fastapi" ];
# };
# in {
# # Use in your devShell:
# devShells.default = pkgs.mkShell {
# shellHook = rules.shellHook;
# inherit (rules) instructions;
# };
# }
#
# The shellHook creates:
# - A `.opencode-rules/` symlink pointing to the AGENTS repository rules directory
# - An `opencode.json` file with a $schema reference and instructions list
#
# The instructions list contains paths relative to the project root, all prefixed
# with `.opencode-rules/`, making them portable across different project locations.
{lib}: {
# Create Opencode rules configuration from AGENTS repository
#
# Args:
# agents: Path to the AGENTS repository (non-flake input)
# languages: Optional list of language-specific rules to include
# (e.g., [ "python" "typescript" "rust" ])
# concerns: Optional list of concern rules to include
# Default: [ "coding-style" "naming" "documentation" "testing" "git-workflow" "project-structure" ]
# frameworks: Optional list of framework-specific rules to include
# (e.g., [ "react" "fastapi" "django" ])
# extraInstructions: Optional list of additional instruction paths
# (for custom rules outside standard locations)
#
# Returns:
# An attribute set containing:
# - shellHook: Bash code to create symlink and opencode.json
# - instructions: List of rule file paths (relative to project root)
#
# Example:
# mkOpencodeRules {
# agents = inputs.agents;
# languages = [ "python" ];
# frameworks = [ "fastapi" ];
# }
# # Returns:
# # {
# # shellHook = "...";
# # instructions = [
# # ".opencode-rules/concerns/coding-style.md"
# # ".opencode-rules/concerns/naming.md"
# # ".opencode-rules/concerns/documentation.md"
# # ".opencode-rules/concerns/testing.md"
# # ".opencode-rules/concerns/git-workflow.md"
# # ".opencode-rules/concerns/project-structure.md"
# # ".opencode-rules/languages/python.md"
# # ".opencode-rules/frameworks/fastapi.md"
# # ];
# # }
mkOpencodeRules = {
agents,
languages ? [],
concerns ? [
"coding-style"
"naming"
"documentation"
"testing"
"git-workflow"
"project-structure"
],
frameworks ? [],
extraInstructions ? [],
}: let
rulesDir = ".opencode-rules";
# Build instructions list by mapping concerns, languages, frameworks to their file paths
# All paths are relative to project root via the rulesDir symlink
instructions =
(map (c: "${rulesDir}/concerns/${c}.md") concerns)
++ (map (l: "${rulesDir}/languages/${l}.md") languages)
++ (map (f: "${rulesDir}/frameworks/${f}.md") frameworks)
++ extraInstructions;
# Generate JSON configuration for Opencode
opencodeConfig = {
"$schema" = "https://opencode.ai/config.json";
inherit instructions;
};
in {
inherit instructions;
# Shell hook to set up rules in the project
# Creates a symlink to the AGENTS rules directory and generates opencode.json
shellHook = ''
# Create/update symlink to AGENTS rules directory
ln -sfn ${agents}/rules ${rulesDir}
# Generate opencode.json configuration file
cat > opencode.json <<'OPENCODE_EOF'
${builtins.toJSON opencodeConfig}
OPENCODE_EOF
'';
};
}

View File

@@ -76,7 +76,10 @@ in {
mcpServers = mkOption { mcpServers = mkOption {
type = types.attrsOf types.anything; type = types.attrsOf types.anything;
default = if mcpCfg != null then mcpCfg.servers else {}; default =
if mcpCfg != null
then mcpCfg.servers
else {};
defaultText = literalExpression "config.programs.mcp.servers"; defaultText = literalExpression "config.programs.mcp.servers";
description = '' description = ''
MCP server configurations for Claude Code. MCP server configurations for Claude Code.

View File

@@ -0,0 +1,401 @@
{
cfg,
pkgs,
lib,
...
}:
with lib; let
managedSettingsFile = pkgs.writeText "pi-agent-managed-settings.json" (builtins.toJSON cfg.settings);
managedEnvFile =
pkgs.writeText "pi-agent-managed.env"
(concatStringsSep "\n" (mapAttrsToList (k: v: "${k}=${v}") cfg.environment));
runtimePath = concatStringsSep ":" (
[
"${cfg.package}/bin"
"${pkgs.nodejs}/bin"
"${pkgs.git}/bin"
"${pkgs.coreutils}/bin"
"${pkgs.findutils}/bin"
"${pkgs.gnugrep}/bin"
"${pkgs.gnused}/bin"
"${pkgs.util-linux}/bin"
"/run/current-system/sw/bin"
]
++ map (p: "${p}/bin") cfg.extraPackages
);
userPolicyCase = concatStringsSep "\n" (
mapAttrsToList (
user: userCfg: ''
${escapeShellArg user})
USER_CONFIG_PATH=${escapeShellArg (
if userCfg.configPath != null
then userCfg.configPath
else cfg.wrapper.hostConfigPath
)}
USER_ROOTS=(${concatStringsSep " " (map escapeShellArg userCfg.projectRoots)})
;;
''
)
cfg.hostUsers
);
in
pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "${cfg.wrapper.runnerName} must run as root" >&2
exit 1
fi
if [ "$#" -lt 2 ]; then
echo "Usage: ${cfg.wrapper.runnerName} <invoking-user> <cwd> [pi-args...]" >&2
exit 2
fi
invoking_user="$1"
shift
cwd="$1"
shift
# Parse forwarded environment variables from wrapper (KEY=VALUE)
while [ "$#" -gt 0 ]; do
case "$1" in
TERM=*|LANG=*|LC_ALL=*|LC_CTYPE=*|COLORTERM=*|TERM_PROGRAM=*)
export "$1"
shift
;;
*)
break
;;
esac
done
resolve_user_policy() {
local user="$1"
USER_CONFIG_PATH=""
USER_ROOTS=()
case "$user" in
${userPolicyCase}
*)
return 1
;;
esac
return 0
}
if ! resolve_user_policy "$invoking_user"; then
echo "User '$invoking_user' is not allowed to use ${cfg.wrapper.commandName}" >&2
exit 1
fi
user_home="$(eval echo "~$invoking_user")"
if [ -z "$user_home" ] || [ "$user_home" = "~$invoking_user" ]; then
echo "Unable to determine home directory for user '$invoking_user'" >&2
exit 1
fi
expand_home_path() {
local input="$1"
if [ "$input" = "~" ]; then
printf '%s\n' "$user_home"
elif ${pkgs.gnugrep}/bin/grep -q '^~/' <<<"$input"; then
printf '%s\n' "$user_home/''${input:2}"
elif ${pkgs.gnugrep}/bin/grep -q '^/' <<<"$input"; then
printf '%s\n' "$input"
else
# Bare relative path resolve from user's home
printf '%s\n' "$user_home/$input"
fi
}
cwd_real="$(${pkgs.coreutils}/bin/realpath -m "$cwd")"
resolved_roots=()
skipped_roots=()
is_allowed_cwd=0
for configured_root in "''${USER_ROOTS[@]}"; do
expanded_root="$(expand_home_path "$configured_root")"
resolved_root="$(${pkgs.coreutils}/bin/realpath -m "$expanded_root")"
if [ ! -d "$resolved_root" ]; then
skipped_roots+=("$resolved_root")
continue
fi
resolved_roots+=("$resolved_root")
case "$cwd_real/" in
"$resolved_root"/*)
is_allowed_cwd=1
;;
esac
done
if [ "''${#resolved_roots[@]}" -eq 0 ]; then
echo "Denied: no valid existing project roots are configured for user '$invoking_user'." >&2
if [ "''${#skipped_roots[@]}" -gt 0 ]; then
echo "Configured but missing roots:" >&2
for root in "''${skipped_roots[@]}"; do
echo " - $root" >&2
done
fi
exit 1
fi
if [ "$is_allowed_cwd" -ne 1 ]; then
echo "Denied: '$cwd_real' is outside allowed project roots for user '$invoking_user'." >&2
echo "Allowed roots:" >&2
for root in "''${resolved_roots[@]}"; do
echo " - $root" >&2
done
exit 1
fi
${pkgs.coreutils}/bin/install -d -m 0750 -o ${escapeShellArg cfg.user} -g ${escapeShellArg cfg.group} \
${escapeShellArg cfg.stateDir} \
${escapeShellArg "${cfg.stateDir}/.pi"} \
${escapeShellArg "${cfg.stateDir}/.pi/agent"} \
${escapeShellArg "${cfg.stateDir}/.pi/agent/sessions"} \
${escapeShellArg "${cfg.stateDir}/.project-mounts"} \
${escapeShellArg "${cfg.stateDir}/projects"} \
${escapeShellArg "${cfg.stateDir}/.npm"} \
${escapeShellArg "${cfg.stateDir}/.npm-global"} \
${escapeShellArg "${cfg.stateDir}/.npm-global/bin"} \
${escapeShellArg "${cfg.stateDir}/.npm-global/lib"}
config_source="$USER_CONFIG_PATH"
if ${pkgs.gnugrep}/bin/grep -q '^/' <<<"$config_source"; then
source_dir="$config_source"
else
source_dir="$(expand_home_path "$config_source")"
fi
if [ "${
if cfg.wrapper.syncConfigFromHost
then "1"
else "0"
}" = "1" ] && [ -d "$source_dir" ]; then
${pkgs.rsync}/bin/rsync -a --delete \
--exclude='auth.json' \
--exclude='mcp-oauth' \
--exclude='sessions' \
--exclude='bin' \
--exclude='mcp-cache.json' \
"$source_dir/" ${escapeShellArg "${cfg.stateDir}/.pi/agent/"}
${pkgs.coreutils}/bin/chown -R ${escapeShellArg "${cfg.user}:${cfg.group}"} ${escapeShellArg "${cfg.stateDir}/.pi/agent"}
fi
# Merge host settings.json (if any) with Nix-managed settings.
# Precedence: host settings first, Nix-managed keys override recursively.
settings_target=${escapeShellArg "${cfg.stateDir}/.pi/agent/settings.json"}
${pkgs.python3}/bin/python3 - "$settings_target" ${escapeShellArg managedSettingsFile} <<'PY_PI_SETTINGS_MERGE'
import json
import os
import sys
def load_obj(path):
if not os.path.exists(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except Exception:
return {}
def deep_merge(base, override):
if isinstance(base, dict) and isinstance(override, dict):
out = dict(base)
for key, value in override.items():
out[key] = deep_merge(out.get(key), value)
return out
return override
def main():
target = sys.argv[1]
managed = sys.argv[2]
base_obj = load_obj(target)
managed_obj = load_obj(managed)
merged = deep_merge(base_obj, managed_obj)
os.makedirs(os.path.dirname(target), exist_ok=True)
tmp = f"{target}.tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(merged, f, indent=2, sort_keys=True)
f.write("\n")
os.replace(tmp, target)
if __name__ == "__main__":
main()
PY_PI_SETTINGS_MERGE
${pkgs.coreutils}/bin/chown ${escapeShellArg "${cfg.user}:${cfg.group}"} "$settings_target"
${pkgs.coreutils}/bin/chmod 0640 "$settings_target"
# Merge environment into isolated .env with precedence:
# 1) synced host env (source_dir/.env)
# 2) Nix-managed environment attrset
# 3) Nix-managed environmentFiles (appended in declaration order)
env_target=${escapeShellArg "${cfg.stateDir}/.pi/.env"}
${pkgs.coreutils}/bin/install -o ${escapeShellArg cfg.user} -g ${escapeShellArg cfg.group} -m 0640 /dev/null "$env_target"
if [ -f "$source_dir/.env" ]; then
${pkgs.coreutils}/bin/cat "$source_dir/.env" >> "$env_target"
printf '\n' >> "$env_target"
fi
if [ -f ${escapeShellArg managedEnvFile} ]; then
${pkgs.coreutils}/bin/cat ${escapeShellArg managedEnvFile} >> "$env_target"
printf '\n' >> "$env_target"
fi
${concatStringsSep "\n" (map (f: ''
if [ -f ${escapeShellArg f} ]; then
${pkgs.coreutils}/bin/cat ${escapeShellArg f} >> "$env_target"
printf '\n' >> "$env_target"
fi
'')
cfg.environmentFiles)}
${pkgs.coreutils}/bin/chown ${escapeShellArg "${cfg.user}:${cfg.group}"} "$env_target"
${pkgs.coreutils}/bin/chmod 0640 "$env_target"
npm_prefix=${escapeShellArg "${cfg.stateDir}/.npm-global"}
runtime_path=${escapeShellArg runtimePath}
project_mount_dir=${escapeShellArg "${cfg.stateDir}/.project-mounts"}
project_links_dir=${escapeShellArg "${cfg.stateDir}/projects"}
project_bind_pairs=()
matched_root=""
matched_mount=""
project_index=0
for root in "''${resolved_roots[@]}"; do
if [ ! -d "$root" ]; then
continue
fi
root_slug="$(printf '%s' "$root" | ${pkgs.gnused}/bin/sed 's#^/##; s#/#-#g; s#-\{2,\}#-#g; s#-$##; s#^$#root#')"
root_slug="''${project_index}-''${root_slug}"
project_index=$((project_index + 1))
mount_point="''${project_mount_dir}/''${root_slug}"
link_path="''${project_links_dir}/''${root_slug}"
${pkgs.coreutils}/bin/install -d -m 0750 -o ${escapeShellArg cfg.user} -g ${escapeShellArg cfg.group} "$mount_point"
${pkgs.coreutils}/bin/ln -sfn "$mount_point" "$link_path"
project_bind_pairs+=("$root:$mount_point")
case "$cwd_real/" in
"$root"/*)
if [ -z "$matched_root" ] || [ "''${#root}" -gt "''${#matched_root}" ]; then
matched_root="$root"
matched_mount="$mount_point"
fi
;;
esac
done
if [ -z "$matched_root" ]; then
echo "Failed to map cwd '$cwd_real' to an allowed root." >&2
exit 1
fi
if [ "$cwd_real" = "$matched_root" ]; then
mapped_cwd="$matched_mount"
else
rel_path="''${cwd_real#"$matched_root/"}"
mapped_cwd="$matched_mount/$rel_path"
fi
pi_bin=${escapeShellArg "${cfg.package}/bin/${cfg.binaryName}"}
if [ ! -x "$pi_bin" ]; then
for candidate in pi pi-agent; do
alt=${escapeShellArg "${cfg.package}/bin"}/$candidate
if [ -x "$alt" ]; then
pi_bin="$alt"
break
fi
done
fi
if [ ! -x "$pi_bin" ]; then
echo "Pi binary not found or not executable: $pi_bin" >&2
echo "Available executables in ${cfg.package}/bin:" >&2
${pkgs.coreutils}/bin/ls -1 ${escapeShellArg "${cfg.package}/bin"} >&2 || true
exit 127
fi
cmd=(
${pkgs.systemd}/bin/systemd-run
--collect
--wait
--pty
--service-type=exec
-p User=${cfg.user}
-p Group=${cfg.group}
-p WorkingDirectory="$mapped_cwd"
-p NoNewPrivileges=yes
-p PrivateTmp=yes
-p ProtectSystem=strict
-p ProtectHome=false
-p ProtectControlGroups=yes
-p ProtectKernelTunables=yes
-p ProtectKernelModules=yes
-p RestrictSUIDSGID=yes
-p LockPersonality=yes
-p RestrictRealtime=yes
-p RestrictNamespaces=yes
-p MemoryDenyWriteExecute=no
-p UMask=0007
-p ReadWritePaths=${cfg.stateDir}
-p EnvironmentFile=${cfg.stateDir}/.pi/.env
-E HOME=${cfg.stateDir}
-E PI_HOME=${cfg.stateDir}/.pi
-E MESSAGING_CWD="$mapped_cwd"
-E PATH="$runtime_path"
-E NPM_CONFIG_CACHE=${cfg.stateDir}/.npm
-E NPM_CONFIG_PREFIX="$npm_prefix"
-E PI_AGENT_INVOKING_USER="$invoking_user"
)
# Propagate terminal and locale settings for correct PTY/UTF-8 handling
for env_var in TERM LANG LC_ALL LC_CTYPE COLORTERM TERM_PROGRAM; do
if [ -n "''${!env_var:-}" ]; then
cmd+=( -E "$env_var=''${!env_var}" )
fi
done
${optionalString (cfg.projectGroup != null) ''
cmd+=( -p SupplementaryGroups=${cfg.projectGroup} )
''}
# Only mark existing top-level paths inaccessible; systemd fails namespace
# setup if InaccessiblePaths points to a non-existent path on this host.
for p in /home /root /mnt /media /srv; do
if [ -e "$p" ]; then
cmd+=( -p "InaccessiblePaths=$p" )
fi
done
for pair in "''${project_bind_pairs[@]}"; do
src="''${pair%%:*}"
dst="''${pair#*:}"
cmd+=( -p "BindPaths=$src:$dst" )
done
${concatStringsSep "\n" (mapAttrsToList (name: value: ''cmd+=( -E ${escapeShellArg "${name}=${value}"} )'') cfg.wrapper.extraEnvironment)}
cmd+=( "$pi_bin" )
${concatStringsSep "\n" (map (arg: ''cmd+=( ${escapeShellArg arg} )'') cfg.wrapper.extraRunArgs)}
cmd+=( "$@" )
exec "''${cmd[@]}"
''

View File

@@ -0,0 +1,102 @@
{
cfg,
pkgs,
lib,
runner,
...
}:
with lib;
pkgs.writeShellScriptBin cfg.wrapper.commandName ''
set -euo pipefail
user_name="$(id -un)"
user_home="$(eval echo "~$user_name")"
if [ -z "$user_home" ] || [ "$user_home" = "~$user_name" ]; then
user_home="$HOME"
fi
resolve_user_policy() {
local user="$1"
USER_ROOTS=()
case "$user" in
${concatStringsSep "\n" (
mapAttrsToList (
user: userCfg: ''
${escapeShellArg user})
USER_ROOTS=(${concatStringsSep " " (map escapeShellArg userCfg.projectRoots)})
;;
''
)
cfg.hostUsers
)}
*)
return 1
;;
esac
return 0
}
if ! resolve_user_policy "$user_name"; then
echo "User '$user_name' is not allowed to use ${cfg.wrapper.commandName}" >&2
exit 1
fi
expand_home_path() {
local input="$1"
if [ "$input" = "~" ]; then
printf '%s\n' "$user_home"
elif ${pkgs.gnugrep}/bin/grep -q '^~/' <<<"$input"; then
printf '%s\n' "$user_home/''${input:2}"
elif ${pkgs.gnugrep}/bin/grep -q '^/' <<<"$input"; then
printf '%s\n' "$input"
else
printf '%s\n' "$user_home/$input"
fi
}
cwd_real="$(${pkgs.coreutils}/bin/realpath -m "$PWD")"
is_allowed_cwd=0
resolved_roots=()
skipped_roots=()
for configured_root in "''${USER_ROOTS[@]}"; do
expanded_root="$(expand_home_path "$configured_root")"
resolved_root="$(${pkgs.coreutils}/bin/realpath -m "$expanded_root")"
if [ ! -d "$resolved_root" ]; then
skipped_roots+=("$resolved_root")
continue
fi
resolved_roots+=("$resolved_root")
case "$cwd_real/" in
"$resolved_root"/*)
is_allowed_cwd=1
;;
esac
done
if [ "''${#resolved_roots[@]}" -eq 0 ]; then
echo "Denied: no valid existing project roots are configured for user '$user_name'." >&2
if [ "''${#skipped_roots[@]}" -gt 0 ]; then
echo "Configured but missing roots:" >&2
for root in "''${skipped_roots[@]}"; do
echo " - $root" >&2
done
fi
exit 1
fi
if [ "$is_allowed_cwd" -ne 1 ]; then
echo "Denied: '$cwd_real' is outside allowed project roots for user '$user_name'." >&2
echo "Allowed roots:" >&2
for root in "''${resolved_roots[@]}"; do
echo " - $root" >&2
done
exit 1
fi
exec /run/wrappers/bin/sudo --non-interactive \
${runner}/bin/${cfg.wrapper.runnerName} \
"$user_name" "$cwd_real" \
"TERM=$TERM" "LANG=$LANG" "LC_ALL=''${LC_ALL:-}" "LC_CTYPE=''${LC_CTYPE:-}" "COLORTERM=''${COLORTERM:-}" "TERM_PROGRAM=''${TERM_PROGRAM:-}" \
"$@"
''

View File

@@ -17,471 +17,8 @@ with lib; let
hostUserNames = attrNames cfg.hostUsers; hostUserNames = attrNames cfg.hostUsers;
managedSettingsFile = pkgs.writeText "pi-agent-managed-settings.json" (builtins.toJSON cfg.settings); runner = import ./pi-agent-runner.nix {inherit cfg pkgs lib;};
wrapper = import ./pi-agent-wrapper.nix {inherit cfg pkgs lib runner;};
managedEnvFile =
pkgs.writeText "pi-agent-managed.env"
(concatStringsSep "\n" (mapAttrsToList (k: v: "${k}=${v}") cfg.environment));
runtimePath = concatStringsSep ":" (
[
"${cfg.package}/bin"
"${pkgs.nodejs}/bin"
"${pkgs.git}/bin"
"${pkgs.coreutils}/bin"
"${pkgs.findutils}/bin"
"${pkgs.gnugrep}/bin"
"${pkgs.gnused}/bin"
"${pkgs.util-linux}/bin"
"/run/current-system/sw/bin"
]
++ map (p: "${p}/bin") cfg.extraPackages
);
userPolicyCase = concatStringsSep "\n" (
mapAttrsToList (
user: userCfg: ''
${escapeShellArg user})
USER_CONFIG_PATH=${escapeShellArg (
if userCfg.configPath != null
then userCfg.configPath
else cfg.wrapper.hostConfigPath
)}
USER_ROOTS=(${concatStringsSep " " (map escapeShellArg userCfg.projectRoots)})
;;
''
)
cfg.hostUsers
);
runner = pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "${cfg.wrapper.runnerName} must run as root" >&2
exit 1
fi
if [ "$#" -lt 2 ]; then
echo "Usage: ${cfg.wrapper.runnerName} <invoking-user> <cwd> [pi-args...]" >&2
exit 2
fi
invoking_user="$1"
shift
cwd="$1"
shift
resolve_user_policy() {
local user="$1"
USER_CONFIG_PATH=""
USER_ROOTS=()
case "$user" in
${userPolicyCase}
*)
return 1
;;
esac
return 0
}
if ! resolve_user_policy "$invoking_user"; then
echo "User '$invoking_user' is not allowed to use ${cfg.wrapper.commandName}" >&2
exit 1
fi
user_home="$(eval echo "~$invoking_user")"
if [ -z "$user_home" ] || [ "$user_home" = "~$invoking_user" ]; then
echo "Unable to determine home directory for user '$invoking_user'" >&2
exit 1
fi
expand_home_path() {
local input="$1"
if [ "$input" = "~" ]; then
printf '%s\n' "$user_home"
elif ${pkgs.gnugrep}/bin/grep -q '^~/' <<<"$input"; then
printf '%s\n' "$user_home/''${input:2}"
elif ${pkgs.gnugrep}/bin/grep -q '^/' <<<"$input"; then
printf '%s\n' "$input"
else
# Bare relative path resolve from user's home
printf '%s\n' "$user_home/$input"
fi
}
cwd_real="$(${pkgs.coreutils}/bin/realpath -m "$cwd")"
resolved_roots=()
skipped_roots=()
is_allowed_cwd=0
for configured_root in "''${USER_ROOTS[@]}"; do
expanded_root="$(expand_home_path "$configured_root")"
resolved_root="$(${pkgs.coreutils}/bin/realpath -m "$expanded_root")"
if [ ! -d "$resolved_root" ]; then
skipped_roots+=("$resolved_root")
continue
fi
resolved_roots+=("$resolved_root")
case "$cwd_real/" in
"$resolved_root"/*)
is_allowed_cwd=1
;;
esac
done
if [ "''${#resolved_roots[@]}" -eq 0 ]; then
echo "Denied: no valid existing project roots are configured for user '$invoking_user'." >&2
if [ "''${#skipped_roots[@]}" -gt 0 ]; then
echo "Configured but missing roots:" >&2
for root in "''${skipped_roots[@]}"; do
echo " - $root" >&2
done
fi
exit 1
fi
if [ "$is_allowed_cwd" -ne 1 ]; then
echo "Denied: '$cwd_real' is outside allowed project roots for user '$invoking_user'." >&2
echo "Allowed roots:" >&2
for root in "''${resolved_roots[@]}"; do
echo " - $root" >&2
done
exit 1
fi
${pkgs.coreutils}/bin/install -d -m 0750 -o ${escapeShellArg cfg.user} -g ${escapeShellArg cfg.group} \
${escapeShellArg cfg.stateDir} \
${escapeShellArg "${cfg.stateDir}/.pi"} \
${escapeShellArg "${cfg.stateDir}/.pi/agent"} \
${escapeShellArg "${cfg.stateDir}/.pi/agent/sessions"} \
${escapeShellArg "${cfg.stateDir}/.project-mounts"} \
${escapeShellArg "${cfg.stateDir}/projects"} \
${escapeShellArg "${cfg.stateDir}/.npm"} \
${escapeShellArg "${cfg.stateDir}/.npm-global"} \
${escapeShellArg "${cfg.stateDir}/.npm-global/bin"} \
${escapeShellArg "${cfg.stateDir}/.npm-global/lib"}
config_source="$USER_CONFIG_PATH"
if ${pkgs.gnugrep}/bin/grep -q '^/' <<<"$config_source"; then
source_dir="$config_source"
else
source_dir="$(expand_home_path "$config_source")"
fi
if [ "${
if cfg.wrapper.syncConfigFromHost
then "1"
else "0"
}" = "1" ] && [ -d "$source_dir" ]; then
${pkgs.rsync}/bin/rsync -a --delete \
--exclude='auth.json' \
--exclude='mcp-oauth' \
--exclude='sessions' \
--exclude='bin' \
--exclude='mcp-cache.json' \
"$source_dir/" ${escapeShellArg "${cfg.stateDir}/.pi/agent/"}
${pkgs.coreutils}/bin/chown -R ${escapeShellArg "${cfg.user}:${cfg.group}"} ${escapeShellArg "${cfg.stateDir}/.pi/agent"}
fi
# Merge host settings.json (if any) with Nix-managed settings.
# Precedence: host settings first, Nix-managed keys override recursively.
settings_target=${escapeShellArg "${cfg.stateDir}/.pi/agent/settings.json"}
${pkgs.python3}/bin/python3 - "$settings_target" ${escapeShellArg managedSettingsFile} <<'PY_PI_SETTINGS_MERGE'
import json
import os
import sys
def load_obj(path):
if not os.path.exists(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except Exception:
return {}
def deep_merge(base, override):
if isinstance(base, dict) and isinstance(override, dict):
out = dict(base)
for key, value in override.items():
out[key] = deep_merge(out.get(key), value)
return out
return override
def main():
target = sys.argv[1]
managed = sys.argv[2]
base_obj = load_obj(target)
managed_obj = load_obj(managed)
merged = deep_merge(base_obj, managed_obj)
os.makedirs(os.path.dirname(target), exist_ok=True)
tmp = f"{target}.tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(merged, f, indent=2, sort_keys=True)
f.write("\n")
os.replace(tmp, target)
if __name__ == "__main__":
main()
PY_PI_SETTINGS_MERGE
${pkgs.coreutils}/bin/chown ${escapeShellArg "${cfg.user}:${cfg.group}"} "$settings_target"
${pkgs.coreutils}/bin/chmod 0640 "$settings_target"
# Merge environment into isolated .env with precedence:
# 1) synced host env (source_dir/.env)
# 2) Nix-managed environment attrset
# 3) Nix-managed environmentFiles (appended in declaration order)
env_target=${escapeShellArg "${cfg.stateDir}/.pi/.env"}
${pkgs.coreutils}/bin/install -o ${escapeShellArg cfg.user} -g ${escapeShellArg cfg.group} -m 0640 /dev/null "$env_target"
if [ -f "$source_dir/.env" ]; then
${pkgs.coreutils}/bin/cat "$source_dir/.env" >> "$env_target"
printf '\n' >> "$env_target"
fi
if [ -f ${escapeShellArg managedEnvFile} ]; then
${pkgs.coreutils}/bin/cat ${escapeShellArg managedEnvFile} >> "$env_target"
printf '\n' >> "$env_target"
fi
${concatStringsSep "\n" (map (f: ''
if [ -f ${escapeShellArg f} ]; then
${pkgs.coreutils}/bin/cat ${escapeShellArg f} >> "$env_target"
printf '\n' >> "$env_target"
fi
'')
cfg.environmentFiles)}
${pkgs.coreutils}/bin/chown ${escapeShellArg "${cfg.user}:${cfg.group}"} "$env_target"
${pkgs.coreutils}/bin/chmod 0640 "$env_target"
npm_prefix=${escapeShellArg "${cfg.stateDir}/.npm-global"}
runtime_path=${escapeShellArg runtimePath}
project_mount_dir=${escapeShellArg "${cfg.stateDir}/.project-mounts"}
project_links_dir=${escapeShellArg "${cfg.stateDir}/projects"}
project_bind_pairs=()
matched_root=""
matched_mount=""
project_index=0
for root in "''${resolved_roots[@]}"; do
if [ ! -d "$root" ]; then
continue
fi
root_slug="$(printf '%s' "$root" | ${pkgs.gnused}/bin/sed 's#^/##; s#/#-#g; s#-\{2,\}#-#g; s#-$##; s#^$#root#')"
root_slug="''${project_index}-''${root_slug}"
project_index=$((project_index + 1))
mount_point="''${project_mount_dir}/''${root_slug}"
link_path="''${project_links_dir}/''${root_slug}"
${pkgs.coreutils}/bin/install -d -m 0750 -o ${escapeShellArg cfg.user} -g ${escapeShellArg cfg.group} "$mount_point"
${pkgs.coreutils}/bin/ln -sfn "$mount_point" "$link_path"
project_bind_pairs+=("$root:$mount_point")
case "$cwd_real/" in
"$root"/*)
if [ -z "$matched_root" ] || [ "''${#root}" -gt "''${#matched_root}" ]; then
matched_root="$root"
matched_mount="$mount_point"
fi
;;
esac
done
if [ -z "$matched_root" ]; then
echo "Failed to map cwd '$cwd_real' to an allowed root." >&2
exit 1
fi
if [ "$cwd_real" = "$matched_root" ]; then
mapped_cwd="$matched_mount"
else
rel_path="''${cwd_real#"$matched_root/"}"
mapped_cwd="$matched_mount/$rel_path"
fi
pi_bin=${escapeShellArg "${cfg.package}/bin/${cfg.binaryName}"}
if [ ! -x "$pi_bin" ]; then
for candidate in pi pi-agent; do
alt=${escapeShellArg "${cfg.package}/bin"}/$candidate
if [ -x "$alt" ]; then
pi_bin="$alt"
break
fi
done
fi
if [ ! -x "$pi_bin" ]; then
echo "Pi binary not found or not executable: $pi_bin" >&2
echo "Available executables in ${cfg.package}/bin:" >&2
${pkgs.coreutils}/bin/ls -1 ${escapeShellArg "${cfg.package}/bin"} >&2 || true
exit 127
fi
cmd=(
${pkgs.systemd}/bin/systemd-run
--collect
--wait
--pty
--service-type=exec
-p User=${cfg.user}
-p Group=${cfg.group}
-p WorkingDirectory="$mapped_cwd"
-p NoNewPrivileges=yes
-p PrivateTmp=yes
-p ProtectSystem=strict
-p ProtectHome=false
-p ProtectControlGroups=yes
-p ProtectKernelTunables=yes
-p ProtectKernelModules=yes
-p RestrictSUIDSGID=yes
-p LockPersonality=yes
-p RestrictRealtime=yes
-p RestrictNamespaces=yes
-p MemoryDenyWriteExecute=no
-p UMask=0007
-p ReadWritePaths=${cfg.stateDir}
-p EnvironmentFile=${cfg.stateDir}/.pi/.env
-E HOME=${cfg.stateDir}
-E PI_HOME=${cfg.stateDir}/.pi
-E MESSAGING_CWD="$mapped_cwd"
-E PATH="$runtime_path"
-E NPM_CONFIG_CACHE=${cfg.stateDir}/.npm
-E NPM_CONFIG_PREFIX="$npm_prefix"
-E PI_AGENT_INVOKING_USER="$invoking_user"
)
${optionalString (cfg.projectGroup != null) ''
cmd+=( -p SupplementaryGroups=${cfg.projectGroup} )
''}
# Only mark existing top-level paths inaccessible; systemd fails namespace
# setup if InaccessiblePaths points to a non-existent path on this host.
for p in /home /root /mnt /media /srv; do
if [ -e "$p" ]; then
cmd+=( -p "InaccessiblePaths=$p" )
fi
done
for pair in "''${project_bind_pairs[@]}"; do
src="''${pair%%:*}"
dst="''${pair#*:}"
cmd+=( -p "BindPaths=$src:$dst" )
done
${concatStringsSep "\n" (mapAttrsToList (name: value: ''cmd+=( -E ${escapeShellArg "${name}=${value}"} )'') cfg.wrapper.extraEnvironment)}
cmd+=( "$pi_bin" )
${concatStringsSep "\n" (map (arg: ''cmd+=( ${escapeShellArg arg} )'') cfg.wrapper.extraRunArgs)}
cmd+=( "$@" )
exec "''${cmd[@]}"
'';
wrapper = pkgs.writeShellScriptBin cfg.wrapper.commandName ''
set -euo pipefail
user_name="$(id -un)"
user_home="$(eval echo "~$user_name")"
if [ -z "$user_home" ] || [ "$user_home" = "~$user_name" ]; then
user_home="$HOME"
fi
resolve_user_policy() {
local user="$1"
USER_ROOTS=()
case "$user" in
${concatStringsSep "\n" (
mapAttrsToList (
user: userCfg: ''
${escapeShellArg user})
USER_ROOTS=(${concatStringsSep " " (map escapeShellArg userCfg.projectRoots)})
;;
''
)
cfg.hostUsers
)}
*)
return 1
;;
esac
return 0
}
if ! resolve_user_policy "$user_name"; then
echo "User '$user_name' is not allowed to use ${cfg.wrapper.commandName}" >&2
exit 1
fi
expand_home_path() {
local input="$1"
if [ "$input" = "~" ]; then
printf '%s\n' "$user_home"
elif ${pkgs.gnugrep}/bin/grep -q '^~/' <<<"$input"; then
printf '%s\n' "$user_home/''${input:2}"
elif ${pkgs.gnugrep}/bin/grep -q '^/' <<<"$input"; then
printf '%s\n' "$input"
else
printf '%s\n' "$user_home/$input"
fi
}
cwd_real="$(${pkgs.coreutils}/bin/realpath -m "$PWD")"
is_allowed_cwd=0
resolved_roots=()
skipped_roots=()
for configured_root in "''${USER_ROOTS[@]}"; do
expanded_root="$(expand_home_path "$configured_root")"
resolved_root="$(${pkgs.coreutils}/bin/realpath -m "$expanded_root")"
if [ ! -d "$resolved_root" ]; then
skipped_roots+=("$resolved_root")
continue
fi
resolved_roots+=("$resolved_root")
case "$cwd_real/" in
"$resolved_root"/*)
is_allowed_cwd=1
;;
esac
done
if [ "''${#resolved_roots[@]}" -eq 0 ]; then
echo "Denied: no valid existing project roots are configured for user '$user_name'." >&2
if [ "''${#skipped_roots[@]}" -gt 0 ]; then
echo "Configured but missing roots:" >&2
for root in "''${skipped_roots[@]}"; do
echo " - $root" >&2
done
fi
exit 1
fi
if [ "$is_allowed_cwd" -ne 1 ]; then
echo "Denied: '$cwd_real' is outside allowed project roots for user '$user_name'." >&2
echo "Allowed roots:" >&2
for root in "''${resolved_roots[@]}"; do
echo " - $root" >&2
done
exit 1
fi
exec /run/wrappers/bin/sudo --non-interactive ${runner}/bin/${cfg.wrapper.runnerName} "$user_name" "$cwd_real" "$@"
'';
in { in {
options.m3ta.pi-agent = { options.m3ta.pi-agent = {
enable = mkEnableOption "isolated Pi execution with dedicated system user and policy-enforced wrapper"; enable = mkEnableOption "isolated Pi execution with dedicated system user and policy-enforced wrapper";
@@ -727,6 +264,16 @@ in {
"d ${cfg.stateDir}/.npm-global/lib 0750 ${cfg.user} ${cfg.group} - -" "d ${cfg.stateDir}/.npm-global/lib 0750 ${cfg.user} ${cfg.group} - -"
]; ];
# Ensure correct ownership of stateDir after user creation.
# createHome = true causes useradd to create the directory as root:root
# before systemd-tmpfiles can set the intended owner.
system.activationScripts.pi-agent-chown = {
deps = ["users"];
text = ''
chown ${cfg.user}:${cfg.group} ${cfg.stateDir}
'';
};
# Wrapper is canonical when enabled; raw package on PATH is optional and # Wrapper is canonical when enabled; raw package on PATH is optional and
# disabled by default to reduce bypass opportunities. # disabled by default to reduce bypass opportunities.
environment.systemPackages = environment.systemPackages =

View File

@@ -1,9 +1,6 @@
{inputs, ...}: { {inputs, ...}: {
# This one brings our custom packages from the 'pkgs' directory # This one brings our custom packages from the 'pkgs' directory
additions = final: prev: additions = final: prev: (import ../pkgs {pkgs = final;});
(import ../pkgs {pkgs = final;})
# // (inputs.hyprpanel.overlay final prev)
// {rose-pine-hyprcursor = inputs.rose-pine-hyprcursor.packages.${prev.stdenv.hostPlatform.system}.default;};
# This one contains whatever you want to overlay # This one contains whatever you want to overlay
# You can change versions, add patches, set compilation flags, anything really. # You can change versions, add patches, set compilation flags, anything really.
@@ -16,51 +13,8 @@
brave = prev.brave.override { brave = prev.brave.override {
commandLineArgs = "--password-store=gnome-libsecret"; commandLineArgs = "--password-store=gnome-libsecret";
}; };
# nodejs_24 = inputs.nixpkgs-stable.legacyPackages.${prev.system}.nodejs_24;
# paperless-ngx = inputs.nixpkgs-45570c2.legacyPackages.${prev.system}.paperless-ngx;
# anytype-heart = inputs.nixpkgs-9e58ed7.legacyPackages.${prev.system}.anytype-heart;
# trezord = inputs.nixpkgs-2744d98.legacyPackages.${prev.system}.trezord;
# mesa = inputs.nixpkgs-master.legacyPackages.${prev.system}.mesa;
# hyprpanel = inputs.hyprpanel.packages.${prev.system}.default.overrideAttrs (prev: {
# version = "latest"; # or whatever version you want
# src = final.fetchFromGitHub {
# owner = "Jas-SinghFSU";
# repo = "HyprPanel";
# rev = "master"; # or a specific commit hash
# hash = "sha256-l623fIVhVCU/ylbBmohAtQNbK0YrWlEny0sC/vBJ+dU=";
# };
# });
}; };
temp-packages = final: _prev: {
temp = import inputs.nixpkgs-9e9486b {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
stable-packages = final: _prev: {
stable = import inputs.nixpkgs-stable {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
pinned-packages = final: _prev: {
pinned = import inputs.nixpkgs-9472de4 {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
locked-packages = final: _prev: {
locked = import inputs.nixpkgs-locked {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
master-packages = final: _prev: { master-packages = final: _prev: {
master = import inputs.nixpkgs-master { master = import inputs.nixpkgs-master {
system = final.stdenv.hostPlatform.system; system = final.stdenv.hostPlatform.system;

View File

@@ -2,9 +2,6 @@
# Package modifications # Package modifications
# This overlay contains package overrides and modifications # This overlay contains package overrides and modifications
# n8n = import ./n8n.nix {inherit prev;};
# beads = import ./beads.nix {inherit prev;};
# Add more modifications here as needed # Add more modifications here as needed
# example-package = prev.example-package.override { ... }; # example-package = prev.example-package.override { ... };
} }

View File

@@ -3,6 +3,7 @@
inputs, inputs,
... ...
}: let }: let
# Used only for flake input pass-throughs (basecamp, openspec, opencode-desktop)
system = pkgs.stdenv.hostPlatform.system; system = pkgs.stdenv.hostPlatform.system;
in { in {
# Custom packages registry # Custom packages registry

View File

@@ -31,7 +31,7 @@
# Upstream is missing outputHashes for git dependencies # Upstream is missing outputHashes for git dependencies
# Also fix stale npm deps hash in upstream node_modules FOD # Also fix stale npm deps hash in upstream node_modules FOD
fixedNodeModules = opencode.node_modules.overrideAttrs { fixedNodeModules = opencode.node_modules.overrideAttrs {
outputHash = "sha256-LRhPPrOKCGUSCEWTpAxPdWKTKVNkg82WrvD25cP3jts="; outputHash = "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=";
}; };
opencode-desktop = rustPlatform.buildRustPackage (finalAttrs: { opencode-desktop = rustPlatform.buildRustPackage (finalAttrs: {

26
tests/lib/agents-test.nix Normal file
View File

@@ -0,0 +1,26 @@
let
lib = import <nixpkgs/lib>;
agentsLib = (import ../../lib {inherit lib;}).agents;
# Test 1: renderForTool throws for unknown tools
testUnknownTool = let
result = builtins.tryEval (
agentsLib.renderForTool {
pkgs = {};
agentsInput = {};
tool = "unknown-tool";
}
);
in
assert result.success == false; {result = "pass";};
# Test 2: loadCanonical extracts loadAgents from input
testLoadCanonical = let
fakeInput = {lib.loadAgents = {test = {description = "test";};};};
result = agentsLib.loadCanonical {agentsInput = fakeInput;};
in
assert result == {test = {description = "test";};}; {result = "pass";};
in {
unknown-tool-throws = testUnknownTool;
load-canonical = testLoadCanonical;
}

View File

@@ -0,0 +1,48 @@
let
lib = import <nixpkgs/lib>;
codingRulesLib = (import ../../lib {inherit lib;}).coding-rules;
# Test 1: instructions are generated correctly with custom rulesDir
testInstructions = let
rules = codingRulesLib.mkCodingRules {
agents = "/tmp/fake-agents";
languages = ["python"];
concerns = ["naming"];
rulesDir = ".coding-rules";
};
in
assert rules.instructions
== [
".coding-rules/concerns/naming.md"
".coding-rules/languages/python.md"
]; {result = "pass";};
# Test 2: default rulesDir is .opencode-rules
testDefaultRulesDir = let
rules = codingRulesLib.mkCodingRules {
agents = "/tmp/fake-agents";
};
hasCorrectPrefix = builtins.all (s: builtins.substring 0 15 s == ".opencode-rules") rules.instructions;
in
assert hasCorrectPrefix == true; {result = "pass";};
# Test 3: backward-compat alias exists
testBackwardCompat = assert codingRulesLib.mkOpencodeRules == codingRulesLib.mkCodingRules; {result = "pass";};
# Test 4: shellHook contains both the symlink command and the config generation
testShellHook = let
rules = codingRulesLib.mkCodingRules {
agents = "/tmp/fake-agents";
};
hook = rules.shellHook;
hasSymlink = builtins.match ".*ln -sfn.*" hook != null;
hasConfigGen = builtins.match ".*coding-rules.json.*" hook != null;
in
assert hasSymlink;
assert hasConfigGen; {result = "pass";};
in {
instructions-correct = testInstructions;
default-rules-dir = testDefaultRulesDir;
backward-compat = testBackwardCompat;
shell-hook = testShellHook;
}

4
tests/lib/default.nix Normal file
View File

@@ -0,0 +1,4 @@
{
coding-rules = import ./coding-rules-test.nix;
agents = import ./agents-test.nix;
}