Compare commits
22 Commits
master
...
c454433448
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c454433448 | ||
| 8feaaa2845 | |||
| 853c644446 | |||
|
|
9a8107ea90 | ||
|
|
4935fcb9ee | ||
|
|
a2f08671a6 | ||
|
|
b708b2a05f | ||
|
|
41fbe75abc | ||
|
|
778192e5e6 | ||
|
|
1f149155b4 | ||
|
|
a2cb2b6319 | ||
|
|
6ff6deb4e3 | ||
|
|
5d9fe6afb7 | ||
|
|
b2208277c4 | ||
| 9adfa185bb | |||
|
|
a1b6950e93 | ||
| 25a44e79fa | |||
|
|
c615eb5c1e | ||
| aa084be01a | |||
|
|
3794500230 | ||
| 0867492170 | |||
|
|
cab1f73c89 |
Binary file not shown.
BIN
.cache/nix/fetcher-cache-v4.sqlite
Normal file
BIN
.cache/nix/fetcher-cache-v4.sqlite
Normal file
Binary file not shown.
1
.gitignore
vendored
1
.gitignore
vendored
@@ -43,3 +43,4 @@ flake.lock.bak
|
|||||||
.sidecar-start.sh
|
.sidecar-start.sh
|
||||||
.sidecar-base
|
.sidecar-base
|
||||||
.td-root
|
.td-root
|
||||||
|
.pi-lens
|
||||||
|
|||||||
7
.pi-lens/cache/jscpd.json
vendored
7
.pi-lens/cache/jscpd.json
vendored
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"success": true,
|
|
||||||
"clones": [],
|
|
||||||
"duplicatedLines": 0,
|
|
||||||
"totalLines": 0,
|
|
||||||
"percentage": 0
|
|
||||||
}
|
|
||||||
3
.pi-lens/cache/jscpd.meta.json
vendored
3
.pi-lens/cache/jscpd.meta.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"timestamp": "2026-04-11T04:17:20.531Z"
|
|
||||||
}
|
|
||||||
9
.pi-lens/cache/knip.json
vendored
9
.pi-lens/cache/knip.json
vendored
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"success": false,
|
|
||||||
"issues": [],
|
|
||||||
"unusedExports": [],
|
|
||||||
"unusedFiles": [],
|
|
||||||
"unusedDeps": [],
|
|
||||||
"unlistedDeps": [],
|
|
||||||
"summary": "Failed to parse output"
|
|
||||||
}
|
|
||||||
3
.pi-lens/cache/knip.meta.json
vendored
3
.pi-lens/cache/knip.meta.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"timestamp": "2026-04-11T04:17:21.374Z"
|
|
||||||
}
|
|
||||||
3
.pi-lens/cache/session-start-guidance.json
vendored
3
.pi-lens/cache/session-start-guidance.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"content": "📌 pi-lens active — as you work on this project, fix any errors you encounter (including pre-existing). Prefer: lsp_navigation for definitions/references, ast_grep_search for code patterns, grep for text/TODO search."
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"timestamp": "2026-04-11T04:21:36.939Z"
|
|
||||||
}
|
|
||||||
3
.pi-lens/cache/todo-baseline.json
vendored
3
.pi-lens/cache/todo-baseline.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"items": []
|
|
||||||
}
|
|
||||||
3
.pi-lens/cache/todo-baseline.meta.json
vendored
3
.pi-lens/cache/todo-baseline.meta.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"timestamp": "2026-04-11T04:21:36.940Z"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"files": {},
|
|
||||||
"turnCycles": 0,
|
|
||||||
"maxCycles": 3,
|
|
||||||
"lastUpdated": "2026-04-11T04:17:22.397Z"
|
|
||||||
}
|
|
||||||
74
AGENTS.md
74
AGENTS.md
@@ -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
107
CHANGELOG.md
Normal 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
|
||||||
99
docs/guides/pi-agent-isolation.md
Normal file
99
docs/guides/pi-agent-isolation.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Pi Agent Isolation (two-repo setup)
|
||||||
|
|
||||||
|
This guide documents the split setup where:
|
||||||
|
|
||||||
|
- `m3ta-nixpkgs` provides reusable module logic.
|
||||||
|
- `nixos-config` consumes it on specific hosts.
|
||||||
|
|
||||||
|
## 1) In `m3ta-nixpkgs`
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
- Home Manager module: `coding.agents.pi`
|
||||||
|
- renders Pi config in user space (default path: `.pi/agent` => `~/.pi/agent`)
|
||||||
|
- NixOS module: `m3ta.pi-agent`
|
||||||
|
- dedicated user/group (default `pi-agent`)
|
||||||
|
- state directory (default `/var/lib/pi-agent`)
|
||||||
|
- hardened execution via transient `systemd-run`
|
||||||
|
- host-side wrapper command (default `pi`)
|
||||||
|
- per-user allowlists via `hostUsers.<name>.projectRoots`
|
||||||
|
- host config sync into isolated runtime (default source `.pi/agent`)
|
||||||
|
- managed settings/env merge into isolated runtime
|
||||||
|
|
||||||
|
## 2) In consumer repo (`nixos-config`)
|
||||||
|
|
||||||
|
### Home Manager side
|
||||||
|
|
||||||
|
Keep Pi config rendering enabled for your normal user:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
coding.agents.pi = {
|
||||||
|
enable = true;
|
||||||
|
agentsInput = inputs.agents;
|
||||||
|
path = ".pi/agent";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### NixOS host side (example: `m3-kratos`)
|
||||||
|
|
||||||
|
Enable isolated wrapper execution:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
m3ta.pi-agent = {
|
||||||
|
enable = true;
|
||||||
|
stateDir = "/var/lib/pi-agent";
|
||||||
|
|
||||||
|
hostUsers = {
|
||||||
|
m3tam3re = {
|
||||||
|
projectRoots = ["~/p" "~/work/private"];
|
||||||
|
# optional; defaults to wrapper.hostConfigPath
|
||||||
|
configPath = ".pi/agent";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
defaultProvider = "anthropic";
|
||||||
|
defaultModel = "anthropic/claude-sonnet-4";
|
||||||
|
quietStartup = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
PI_TELEMETRY = "0";
|
||||||
|
};
|
||||||
|
|
||||||
|
environmentFiles = [
|
||||||
|
"/run/secrets/pi-agent.env"
|
||||||
|
];
|
||||||
|
|
||||||
|
wrapper = {
|
||||||
|
enable = true;
|
||||||
|
commandName = "pi";
|
||||||
|
hideDirectBinary = true;
|
||||||
|
hostConfigPath = ".pi/agent";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Authorization model
|
||||||
|
|
||||||
|
The wrapper uses a tightly scoped sudo rule:
|
||||||
|
|
||||||
|
- authorized users may run only the privileged runner command
|
||||||
|
- with `NOPASSWD`
|
||||||
|
- no broad `NOPASSWD: ALL`
|
||||||
|
|
||||||
|
## 4) Merge behavior
|
||||||
|
|
||||||
|
At invocation time, isolated runtime files are built from:
|
||||||
|
|
||||||
|
1. Host user Pi config (synced from source path, e.g. `~/.pi/agent`)
|
||||||
|
2. Nix-managed settings/env (override host values)
|
||||||
|
3. Environment files (appended after managed env attrs)
|
||||||
|
|
||||||
|
This keeps user-authored Pi config available while allowing reproducible Nix overrides.
|
||||||
|
|
||||||
|
## 5) Migration notes
|
||||||
|
|
||||||
|
- If wrapper mode is canonical, remove direct `pi-coding-agent` from user package lists to reduce command-path ambiguity.
|
||||||
|
- Rebuild host config and test from an allowlisted project path.
|
||||||
|
- Validate `pi` process identity runs as `pi-agent`.
|
||||||
@@ -157,6 +157,32 @@ m3ta.mem0 = {
|
|||||||
|
|
||||||
**Documentation**: [mem0 Module](../modules/nixos/mem0.md)
|
**Documentation**: [mem0 Module](../modules/nixos/mem0.md)
|
||||||
|
|
||||||
|
#### `m3ta.pi-agent`
|
||||||
|
|
||||||
|
Isolated Pi execution with a dedicated system user (`pi-agent` by default),
|
||||||
|
a hardened runtime, and a host-side `pi` wrapper command.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
m3ta.pi-agent = {
|
||||||
|
enable = true;
|
||||||
|
stateDir = "/var/lib/pi-agent";
|
||||||
|
|
||||||
|
hostUsers = {
|
||||||
|
m3tam3re = {
|
||||||
|
projectRoots = ["~/p" "~/work/private"];
|
||||||
|
configPath = ".pi/agent"; # optional
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
settings.defaultModel = "anthropic/claude-sonnet-4";
|
||||||
|
environment.PI_TELEMETRY = "0";
|
||||||
|
wrapper.commandName = "pi";
|
||||||
|
wrapper.hideDirectBinary = true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Documentation**: [Pi Agent Isolation Guide](./pi-agent-isolation.md)
|
||||||
|
|
||||||
### Home Manager Modules
|
### Home Manager Modules
|
||||||
|
|
||||||
#### `m3ta.ports`
|
#### `m3ta.ports`
|
||||||
@@ -255,6 +281,7 @@ Pi agent deployment from canonical TOML definitions.
|
|||||||
coding.agents.pi = {
|
coding.agents.pi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
agentsInput = inputs.agents;
|
agentsInput = inputs.agents;
|
||||||
|
path = ".pi/agent"; # default; can be changed
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
637
docs/plans/2026-04-15-nixpkgs-cleanup-and-improvements.md
Normal file
637
docs/plans/2026-04-15-nixpkgs-cleanup-and-improvements.md
Normal 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.
|
||||||
@@ -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;};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,6 +65,7 @@
|
|||||||
# Individual modules for selective imports
|
# Individual modules for selective imports
|
||||||
ports = ./modules/nixos/ports.nix;
|
ports = ./modules/nixos/ports.nix;
|
||||||
mem0 = ./modules/nixos/mem0.nix;
|
mem0 = ./modules/nixos/mem0.nix;
|
||||||
|
pi-agent = ./modules/nixos/pi-agent.nix;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Home Manager modules - for user-level configuration
|
# Home Manager modules - for user-level configuration
|
||||||
|
|||||||
@@ -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
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -119,23 +119,26 @@ coding.agents.claude-code = {
|
|||||||
enable = true;
|
enable = true;
|
||||||
agentsInput = inputs.agents;
|
agentsInput = inputs.agents;
|
||||||
modelOverrides = {};
|
modelOverrides = {};
|
||||||
|
externalSkills = [{ src = inputs.skills-anthropic; }];
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Options:** `enable`, `agentsInput`, `modelOverrides`
|
**Options:** `enable`, `agentsInput`, `modelOverrides`, `externalSkills`
|
||||||
|
|
||||||
### Pi (`coding.agents.pi`)
|
### Pi (`coding.agents.pi`)
|
||||||
|
|
||||||
Renders `AGENTS.md` + `SYSTEM.md` to `~/.pi/agent/`:
|
Renders `AGENTS.md` + `SYSTEM.md` to `~/.pi/agent/` by default:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
coding.agents.pi = {
|
coding.agents.pi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
agentsInput = inputs.agents;
|
agentsInput = inputs.agents;
|
||||||
|
path = ".pi/agent"; # default, relative to $HOME
|
||||||
|
externalSkills = [{ src = inputs.skills-anthropic; }];
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Options:** `enable`, `agentsInput`
|
**Options:** `enable`, `path`, `agentsInput`, `modelOverrides`, `externalSkills`, `primaryAgent`, `mcpServers`, `settings`
|
||||||
|
|
||||||
### Project-level usage
|
### Project-level usage
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,50 @@ in {
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
externalSkills = mkOption {
|
||||||
|
type = types.listOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
src = mkOption {
|
||||||
|
type = types.anything;
|
||||||
|
description = "Flake input pointing to a skills repository root.";
|
||||||
|
};
|
||||||
|
skillsDir = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "skills";
|
||||||
|
description = ''
|
||||||
|
Subdirectory inside src that contains skill folders.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
selectSkills = mkOption {
|
||||||
|
type = types.nullOr (types.listOf types.str);
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
List of skill names to cherry-pick from this source.
|
||||||
|
null means include every skill found in skillsDir.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
External skill sources passed to mkOpencodeSkills.
|
||||||
|
Each entry maps directly to an element of the externalSkills
|
||||||
|
list accepted by the AGENTS flake's lib.mkOpencodeSkills.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''
|
||||||
|
[
|
||||||
|
{ src = inputs.skills-anthropic; selectSkills = [ "claude-api" ]; }
|
||||||
|
{ src = inputs.skills-vercel; }
|
||||||
|
]
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
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.
|
||||||
@@ -82,6 +123,21 @@ in {
|
|||||||
source = "${rendered}/.claude/agents";
|
source = "${rendered}/.claude/agents";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Skills (merged from personal AGENTS repo + optional external skills)
|
||||||
|
home.file.".claude/skills" = mkIf (cfg.agentsInput != null) {
|
||||||
|
source = cfg.agentsInput.lib.mkOpencodeSkills {
|
||||||
|
inherit pkgs;
|
||||||
|
customSkills = "${cfg.agentsInput}/skills";
|
||||||
|
externalSkills =
|
||||||
|
map (
|
||||||
|
entry:
|
||||||
|
{inherit (entry) src skillsDir;}
|
||||||
|
// optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;}
|
||||||
|
)
|
||||||
|
cfg.externalSkills;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Rendered settings.json with permissions + MCP servers
|
# Rendered settings.json with permissions + MCP servers
|
||||||
home.file.".claude/settings.json" = mkIf (settingsJson != null) {
|
home.file.".claude/settings.json" = mkIf (settingsJson != null) {
|
||||||
source = "${settingsJson}";
|
source = "${settingsJson}";
|
||||||
|
|||||||
@@ -11,13 +11,28 @@ in {
|
|||||||
options.coding.agents.pi = {
|
options.coding.agents.pi = {
|
||||||
enable = mkEnableOption "Pi agent management via canonical agent.toml definitions";
|
enable = mkEnableOption "Pi agent management via canonical agent.toml definitions";
|
||||||
|
|
||||||
|
path = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = ".pi/agent";
|
||||||
|
description = ''
|
||||||
|
Relative path (inside the Home Manager user's home) where Pi agent
|
||||||
|
config should be materialized.
|
||||||
|
|
||||||
|
Defaults to `.pi/agent`, i.e. `~/.pi/agent`.
|
||||||
|
'';
|
||||||
|
example = ".config/pi/agent";
|
||||||
|
};
|
||||||
|
|
||||||
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 Pi (pi-mcp-adapter).
|
MCP server configurations for Pi (pi-mcp-adapter).
|
||||||
Written to ~/.pi/agent/mcp.json.
|
Written to `${cfg.path}/mcp.json`.
|
||||||
Automatically inherits from config.programs.mcp.servers.
|
Automatically inherits from config.programs.mcp.servers.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
@@ -42,6 +57,44 @@ in {
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
externalSkills = mkOption {
|
||||||
|
type = types.listOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
src = mkOption {
|
||||||
|
type = types.anything;
|
||||||
|
description = "Flake input pointing to a skills repository root.";
|
||||||
|
};
|
||||||
|
skillsDir = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "skills";
|
||||||
|
description = ''
|
||||||
|
Subdirectory inside src that contains skill folders.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
selectSkills = mkOption {
|
||||||
|
type = types.nullOr (types.listOf types.str);
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
List of skill names to cherry-pick from this source.
|
||||||
|
null means include every skill found in skillsDir.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
External skill sources passed to mkOpencodeSkills.
|
||||||
|
Each entry maps directly to an element of the externalSkills
|
||||||
|
list accepted by the AGENTS flake's lib.mkOpencodeSkills.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''
|
||||||
|
[
|
||||||
|
{ src = inputs.skills-anthropic; selectSkills = [ "claude-api" ]; }
|
||||||
|
{ src = inputs.skills-vercel; }
|
||||||
|
]
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
primaryAgent = mkOption {
|
primaryAgent = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
@@ -60,7 +113,7 @@ in {
|
|||||||
default = [];
|
default = [];
|
||||||
description = ''
|
description = ''
|
||||||
Pi packages to install (npm:, git:, or local paths).
|
Pi packages to install (npm:, git:, or local paths).
|
||||||
These are written to ~/.pi/agent/settings.json.
|
These are written to `${cfg.path}/settings.json`.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,7 +201,7 @@ in {
|
|||||||
};
|
};
|
||||||
default = {};
|
default = {};
|
||||||
description = ''
|
description = ''
|
||||||
Pi settings written to ~/.pi/agent/settings.json.
|
Pi settings written to `${cfg.path}/settings.json`.
|
||||||
Only non-null values are included in the generated JSON.
|
Only non-null values are included in the generated JSON.
|
||||||
See pi docs/settings.md for all options.
|
See pi docs/settings.md for all options.
|
||||||
'';
|
'';
|
||||||
@@ -156,6 +209,8 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable (let
|
config = mkIf cfg.enable (let
|
||||||
|
basePath = lib.removeSuffix "/" cfg.path;
|
||||||
|
|
||||||
# Build settings.json by filtering out null values recursively
|
# Build settings.json by filtering out null values recursively
|
||||||
filterNulls = attrs:
|
filterNulls = attrs:
|
||||||
lib.filterAttrs (_: v: v != null) (
|
lib.filterAttrs (_: v: v != null) (
|
||||||
@@ -164,8 +219,11 @@ in {
|
|||||||
then let
|
then let
|
||||||
filtered = filterNulls v;
|
filtered = filterNulls v;
|
||||||
in
|
in
|
||||||
if filtered == {} then null else filtered
|
if filtered == {}
|
||||||
else v) attrs
|
then null
|
||||||
|
else filtered
|
||||||
|
else v)
|
||||||
|
attrs
|
||||||
);
|
);
|
||||||
|
|
||||||
piSettings = filterNulls cfg.settings;
|
piSettings = filterNulls cfg.settings;
|
||||||
@@ -185,38 +243,37 @@ in {
|
|||||||
# Dynamic home.file entries for agent .md files
|
# Dynamic home.file entries for agent .md files
|
||||||
agentFiles =
|
agentFiles =
|
||||||
if cfg.agentsInput != null
|
if cfg.agentsInput != null
|
||||||
then
|
then let
|
||||||
let
|
agentNames = builtins.attrNames cfg.agentsInput.lib.loadAgents;
|
||||||
agentNames = builtins.attrNames cfg.agentsInput.lib.loadAgents;
|
in
|
||||||
in
|
builtins.listToAttrs (
|
||||||
builtins.listToAttrs (
|
map (name: {
|
||||||
map (name: {
|
name = "${basePath}/agents/${name}.md";
|
||||||
name = ".pi/agent/agents/${name}.md";
|
value = {source = "${rendered}/agents/${name}.md";};
|
||||||
value = {source = "${rendered}/agents/${name}.md";};
|
})
|
||||||
})
|
agentNames
|
||||||
agentNames
|
)
|
||||||
)
|
|
||||||
else {};
|
else {};
|
||||||
in {
|
in {
|
||||||
home.file = mkMerge [
|
home.file = mkMerge [
|
||||||
# ── MCP servers from programs.mcp → ~/.pi/agent/mcp.json ───────
|
# ── MCP servers from programs.mcp → ${cfg.path}/mcp.json ───────
|
||||||
(mkIf (cfg.mcpServers != {}) {
|
(mkIf (cfg.mcpServers != {}) {
|
||||||
".pi/agent/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;};
|
"${basePath}/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;};
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── ~/.pi/agent/settings.json ──────────────────────────────────
|
# ── ${cfg.path}/settings.json ──────────────────────────────────
|
||||||
{
|
{
|
||||||
".pi/agent/settings.json".text = builtins.toJSON piSettings;
|
"${basePath}/settings.json".text = builtins.toJSON piSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── AGENTS.md — agent descriptions and specialist listing ──────
|
# ── AGENTS.md — agent descriptions and specialist listing ──────
|
||||||
(mkIf (cfg.agentsInput != null) {
|
(mkIf (cfg.agentsInput != null) {
|
||||||
".pi/agent/AGENTS.md".source = "${rendered}/AGENTS.md";
|
"${basePath}/AGENTS.md".source = "${rendered}/AGENTS.md";
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── SYSTEM.md — primary agent's system prompt ──────────────────
|
# ── SYSTEM.md — primary agent's system prompt ──────────────────
|
||||||
(mkIf (cfg.agentsInput != null) {
|
(mkIf (cfg.agentsInput != null) {
|
||||||
".pi/agent/SYSTEM.md".source = "${rendered}/SYSTEM.md";
|
"${basePath}/SYSTEM.md".source = "${rendered}/SYSTEM.md";
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── Agents — pi-subagents .md files ────────────────────────────
|
# ── Agents — pi-subagents .md files ────────────────────────────
|
||||||
@@ -224,9 +281,16 @@ in {
|
|||||||
|
|
||||||
# ── Skills symlinked from AGENTS repo ──────────────────────────
|
# ── Skills symlinked from AGENTS repo ──────────────────────────
|
||||||
(mkIf (cfg.agentsInput != null) {
|
(mkIf (cfg.agentsInput != null) {
|
||||||
".pi/agent/skills".source = cfg.agentsInput.lib.mkOpencodeSkills {
|
"${basePath}/skills".source = cfg.agentsInput.lib.mkOpencodeSkills {
|
||||||
inherit pkgs;
|
inherit pkgs;
|
||||||
customSkills = "${cfg.agentsInput}/skills";
|
customSkills = "${cfg.agentsInput}/skills";
|
||||||
|
externalSkills =
|
||||||
|
map (
|
||||||
|
entry:
|
||||||
|
{inherit (entry) src skillsDir;}
|
||||||
|
// optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;}
|
||||||
|
)
|
||||||
|
cfg.externalSkills;
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
imports = [
|
imports = [
|
||||||
./mem0.nix
|
./mem0.nix
|
||||||
./ports.nix
|
./ports.nix
|
||||||
|
./pi-agent.nix
|
||||||
# Example: ./my-service.nix
|
# Example: ./my-service.nix
|
||||||
# Add more module files here as you create them
|
# Add more module files here as you create them
|
||||||
];
|
];
|
||||||
|
|||||||
401
modules/nixos/pi-agent-runner.nix
Normal file
401
modules/nixos/pi-agent-runner.nix
Normal 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[@]}"
|
||||||
|
''
|
||||||
102
modules/nixos/pi-agent-wrapper.nix
Normal file
102
modules/nixos/pi-agent-wrapper.nix
Normal 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:-}" \
|
||||||
|
"$@"
|
||||||
|
''
|
||||||
295
modules/nixos/pi-agent.nix
Normal file
295
modules/nixos/pi-agent.nix
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
# NixOS Module for isolated Pi execution (fresh design)
|
||||||
|
#
|
||||||
|
# Goals:
|
||||||
|
# - Dedicated isolated runtime identity (pi-agent user/group)
|
||||||
|
# - Host UX via `pi` wrapper command
|
||||||
|
# - Per-host-user project allowlists (different roots per user)
|
||||||
|
# - No container mode
|
||||||
|
# - Merge user Pi config + Nix-managed settings/env into isolated runtime
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
with lib; let
|
||||||
|
cfg = config.m3ta.pi-agent;
|
||||||
|
|
||||||
|
hostUserNames = attrNames cfg.hostUsers;
|
||||||
|
|
||||||
|
runner = import ./pi-agent-runner.nix {inherit cfg pkgs lib;};
|
||||||
|
wrapper = import ./pi-agent-wrapper.nix {inherit cfg pkgs lib runner;};
|
||||||
|
in {
|
||||||
|
options.m3ta.pi-agent = {
|
||||||
|
enable = mkEnableOption "isolated Pi execution with dedicated system user and policy-enforced wrapper";
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.pi-coding-agent;
|
||||||
|
defaultText = literalExpression "pkgs.pi-coding-agent";
|
||||||
|
description = "Pi package providing the executable used in isolated runtime.";
|
||||||
|
};
|
||||||
|
|
||||||
|
binaryName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "pi-agent";
|
||||||
|
description = "Preferred executable name inside `${cfg.package}/bin` (falls back to pi/pi-agent auto-detection).";
|
||||||
|
example = "pi";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "pi-agent";
|
||||||
|
description = "System user that executes Pi in isolated mode.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "pi-agent";
|
||||||
|
description = "System group for the isolated Pi user.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "/var/lib/pi-agent";
|
||||||
|
description = "Writable state/home directory for isolated Pi runtime.";
|
||||||
|
};
|
||||||
|
|
||||||
|
createUser = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether to create the dedicated Pi user/group automatically.";
|
||||||
|
};
|
||||||
|
|
||||||
|
hostUsers = mkOption {
|
||||||
|
type = types.attrsOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
projectRoots = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
Allowed project roots for this host user.
|
||||||
|
`~` and `~/...` are expanded relative to that host user's home.
|
||||||
|
'';
|
||||||
|
example = ["~/p" "~/work/client-a"];
|
||||||
|
};
|
||||||
|
|
||||||
|
configPath = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Optional host path for this user's Pi config source. If null,
|
||||||
|
wrapper.hostConfigPath is used. Relative paths resolve from the
|
||||||
|
host user's home.
|
||||||
|
'';
|
||||||
|
example = ".pi/agent";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = {};
|
||||||
|
description = ''
|
||||||
|
Per-host-user policy map. Keys are host usernames.
|
||||||
|
Each user defines their own allowed project roots and optional config source.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
m3tam3re = {
|
||||||
|
projectRoots = [ "~/p" "~/src/private" ];
|
||||||
|
configPath = ".pi/agent";
|
||||||
|
};
|
||||||
|
teammate = {
|
||||||
|
projectRoots = [ "~/projects" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = mkOption {
|
||||||
|
type = types.attrsOf types.anything;
|
||||||
|
default = {};
|
||||||
|
description = ''
|
||||||
|
Nix-managed Pi settings merged into isolated `${cfg.stateDir}/.pi/agent/settings.json`.
|
||||||
|
Merge precedence: synced host settings first, Nix-managed values override recursively.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
defaultModel = "anthropic/claude-sonnet-4";
|
||||||
|
defaultProvider = "anthropic";
|
||||||
|
quietStartup = true;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
default = {};
|
||||||
|
description = ''
|
||||||
|
Non-secret Nix-managed environment variables appended into isolated
|
||||||
|
`${cfg.stateDir}/.pi/.env` after synced host values.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
environmentFiles = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
Paths to env files (secrets/tokens) appended to isolated `${cfg.stateDir}/.pi/.env`
|
||||||
|
after `environment` entries.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraPackages = mkOption {
|
||||||
|
type = types.listOf types.package;
|
||||||
|
default = [];
|
||||||
|
description = "Extra packages added to isolated runtime PATH.";
|
||||||
|
};
|
||||||
|
|
||||||
|
projectGroup = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
When set, the pi-agent user is added to this group and the group is
|
||||||
|
passed as SupplementaryGroups to the systemd-run sandbox. This allows
|
||||||
|
pi-agent to write to project directories that grant group write access.
|
||||||
|
The user must ensure project directories have appropriate group ownership
|
||||||
|
and permissions (e.g. setgid + group write).
|
||||||
|
'';
|
||||||
|
example = "users";
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Enable host-side wrapper command that enforces policy and runs isolated Pi.";
|
||||||
|
};
|
||||||
|
|
||||||
|
commandName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "pi";
|
||||||
|
description = "Host wrapper command name.";
|
||||||
|
};
|
||||||
|
|
||||||
|
runnerName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "m3ta-pi-agent-runner";
|
||||||
|
description = "Privileged runner command invoked via scoped sudo rule.";
|
||||||
|
};
|
||||||
|
|
||||||
|
hideDirectBinary = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
When true and wrapper is enabled, do not add the raw Pi package to host PATH,
|
||||||
|
reducing bypass risk by making wrapper the canonical entrypoint.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
syncConfigFromHost = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Sync host Pi config directory into isolated `${cfg.stateDir}/.pi/agent`
|
||||||
|
on each invocation.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostConfigPath = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = ".pi/agent";
|
||||||
|
description = ''
|
||||||
|
Default source path for host Pi config sync. Relative paths resolve from
|
||||||
|
the invoking user's home. Per-user hostUsers.<name>.configPath overrides this.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraRunArgs = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
description = "Extra arguments inserted before user-provided Pi args.";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraEnvironment = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
default = {};
|
||||||
|
description = "Additional environment variables passed to isolated Pi runtime.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
assertions =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
assertion = cfg.hostUsers != {};
|
||||||
|
message = "m3ta.pi-agent.hostUsers must define at least one authorized host user.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = (!cfg.wrapper.enable) || (cfg.hostUsers != {});
|
||||||
|
message = "m3ta.pi-agent.hostUsers must not be empty when wrapper is enabled.";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
++ mapAttrsToList (user: userCfg: {
|
||||||
|
assertion = userCfg.projectRoots != [];
|
||||||
|
message = "m3ta.pi-agent.hostUsers.${user}.projectRoots must not be empty.";
|
||||||
|
})
|
||||||
|
cfg.hostUsers;
|
||||||
|
|
||||||
|
users.groups = mkIf cfg.createUser {
|
||||||
|
"${cfg.group}" = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users = mkIf cfg.createUser {
|
||||||
|
"${cfg.user}" = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = mkIf (cfg.projectGroup != null) [cfg.projectGroup];
|
||||||
|
description = "Isolated Pi agent user";
|
||||||
|
home = cfg.stateDir;
|
||||||
|
createHome = true;
|
||||||
|
shell = pkgs.bashInteractive;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.pi 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.pi/agent 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.pi/agent/sessions 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.project-mounts 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/projects 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.npm 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.npm-global 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.npm-global/bin 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
|
||||||
|
# disabled by default to reduce bypass opportunities.
|
||||||
|
environment.systemPackages =
|
||||||
|
optional cfg.wrapper.enable wrapper
|
||||||
|
++ optional ((!cfg.wrapper.enable) || (!cfg.wrapper.hideDirectBinary)) cfg.package;
|
||||||
|
|
||||||
|
security.sudo.extraRules = mkIf (cfg.wrapper.enable && hostUserNames != []) [
|
||||||
|
{
|
||||||
|
users = hostUserNames;
|
||||||
|
commands = [
|
||||||
|
{
|
||||||
|
command = "${runner}/bin/${cfg.wrapper.runnerName}";
|
||||||
|
options = ["NOPASSWD"];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 { ... };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
nix-update-script,
|
nix-update-script,
|
||||||
}: let
|
}: let
|
||||||
pname = "eigent";
|
pname = "eigent";
|
||||||
version = "0.0.89";
|
version = "0.0.90";
|
||||||
src = fetchurl {
|
src = fetchurl {
|
||||||
url = "https://github.com/eigent-ai/eigent/releases/download/v${version}/Eigent-${version}.AppImage";
|
url = "https://github.com/eigent-ai/eigent/releases/download/v${version}/Eigent-${version}.AppImage";
|
||||||
hash = "sha256-9KuiFjegfXhCu1W/FCinWX4ae/DsNPudeBcXFfW18Hc=";
|
hash = "sha256-mwCBx+D6mgGqQa8bDuUpo3h49EwFVkwasJwaYc6aXFE=";
|
||||||
};
|
};
|
||||||
appimageContents = appimageTools.extractType2 {inherit pname version src;};
|
appimageContents = appimageTools.extractType2 {inherit pname version src;};
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -25,20 +25,20 @@
|
|||||||
in
|
in
|
||||||
stdenv.mkDerivation (finalAttrs: {
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
pname = "n8n";
|
pname = "n8n";
|
||||||
version = "stable";
|
version = "2.14.2";
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
src = fetchFromGitHub {
|
||||||
owner = "n8n-io";
|
owner = "n8n-io";
|
||||||
repo = "n8n";
|
repo = "n8n";
|
||||||
tag = "${finalAttrs.version}";
|
tag = "n8n@${finalAttrs.version}";
|
||||||
hash = "sha256-/atba0ymCqhh5Rt61UxwC2xf8SGrRsEKtlsDCIkg37Y=";
|
hash = "sha256-nWV3DFDkBlfDdoOxwYB0HSrTyKpTt70YxAQYUPartkE=";
|
||||||
};
|
};
|
||||||
|
|
||||||
pnpmDeps = fetchPnpmDeps {
|
pnpmDeps = fetchPnpmDeps {
|
||||||
inherit (finalAttrs) pname version src;
|
inherit (finalAttrs) pname version src;
|
||||||
pnpm = pnpm_10;
|
pnpm = pnpm_10;
|
||||||
fetcherVersion = 3;
|
fetcherVersion = 3;
|
||||||
hash = "sha256-YGplNNvIOIY1BthWmejAzucXujq8AkgPJus774GmWCA=";
|
hash = "sha256-0SnPF3CgIja3M1ubLrwyFcx7vY0eHz9DEgn/gDLXN80=";
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs =
|
nativeBuildInputs =
|
||||||
|
|||||||
@@ -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
26
tests/lib/agents-test.nix
Normal 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;
|
||||||
|
}
|
||||||
48
tests/lib/coding-rules-test.nix
Normal file
48
tests/lib/coding-rules-test.nix
Normal 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
4
tests/lib/default.nix
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
coding-rules = import ./coding-rules-test.nix;
|
||||||
|
agents = import ./agents-test.nix;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user