mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:46 +01:00
refactor(agents): manage agent config in a home-manager module (#367)
This commit is contained in:
1 parent
0d2665c286
commit
afdaa3b869
15 files changed
+116
-241
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
This flake manages NixOS hosts, macOS via nix-darwin, shared Home Manager profiles, and Colmena
|
||||
deployments. Keep repository guidance here; reusable global rules live in `agents/AGENTS.md`. See
|
||||
[agents/README.md](./agents/README.md) for their scope and symlink installation targets.
|
||||
[agents/README.md](./agents/README.md) for their scope and deployment targets.
|
||||
|
||||
## Where Changes Belong
|
||||
|
||||
|
||||
+23
-37
@@ -1,9 +1,10 @@
|
||||
# agents
|
||||
|
||||
Reusable, symlink-first agent resources shared across projects.
|
||||
Portable agent resources shared across projects: the global baseline rules, behavioral scenarios for
|
||||
those rules, and reference snippets for external tooling.
|
||||
|
||||
This directory is the canonical source for baseline agent rules and supporting command references.
|
||||
The primary workflow is to symlink files from here into each agent runtime/config location.
|
||||
Home Manager links the rules from here into each agent's config location, so `agents/` itself stays
|
||||
plain Markdown with no Nix dependency.
|
||||
|
||||
It is shared across projects. Repo-scoped task procedures for one repository belong elsewhere: put
|
||||
them in that repository's `.agents/skills/` (note the leading dot), which OpenCode and compatible
|
||||
@@ -20,27 +21,23 @@ Keep one canonical home for each fact; link between layers instead of copying pa
|
||||
|
||||
- `AGENTS.md`: global baseline rules for coding agents.
|
||||
- `evals/global-rules.md`: behavioral scenarios for validating changes to the global rules.
|
||||
- `install-rules.py`: installs the baseline by creating symlinks in supported agent config dirs.
|
||||
- `test_install_rules.py`: regression tests for file preservation and independent target handling.
|
||||
- `install-cli.md`: curated CLI install/update command snippets.
|
||||
- `install-skills.md`: curated `npx skills` command snippets.
|
||||
- `install-skills.md`: curated `npx skills` command snippets, plus the `npx ctx7` docs-tool setup.
|
||||
|
||||
The Nix side of the agents — deploying these rules, the agent CLIs, and their environment — lives in
|
||||
the Home Manager module [`home/base/tui/agents/`](../home/base/tui/agents/README.md).
|
||||
|
||||
## Core workflow
|
||||
|
||||
1. Maintain shared rules in `agents/AGENTS.md`.
|
||||
2. Configure permissions directly in the agent runtime; auto-approval is generally used.
|
||||
3. Run `install-rules.py` to refresh symlinks in local agent homes.
|
||||
4. Use `install-cli.md` and `install-skills.md` as reference snippets when needed.
|
||||
3. Edit the rules; Home Manager links them out-of-store, so the change reaches the next agent
|
||||
session without a rebuild. Run a Home Manager switch only when the deployed target set changes.
|
||||
4. Use `install-skills.md` as a reference when installing external skills.
|
||||
|
||||
## Install baseline rules (symlink-based)
|
||||
## Deployment
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 agents/install-rules.py
|
||||
```
|
||||
|
||||
Current targets:
|
||||
[`home/base/tui/agents/rules.nix`](../home/base/tui/agents/rules.nix) links `AGENTS.md` into every
|
||||
supported agent config directory as an out-of-store symlink, so edits apply without a rebuild:
|
||||
|
||||
- Codex: `AGENTS.md` -> `${CODEX_HOME:-~/.codex}/AGENTS.md`
|
||||
- OpenCode: `AGENTS.md` -> `${XDG_CONFIG_HOME:-~/.config}/opencode/AGENTS.md`
|
||||
@@ -48,30 +45,20 @@ Current targets:
|
||||
- OMP: `AGENTS.md` -> `~/.omp/agent/AGENTS.md`
|
||||
- Generic cross-tool (read by Kimi Code): `AGENTS.md` -> `~/.agents/AGENTS.md`
|
||||
|
||||
Behavior:
|
||||
The module is imported through `home/base/tui`, so it covers the hosts that import
|
||||
`home/linux/gui.nix` or the macOS `home/darwin` stack; core-only servers are unchanged.
|
||||
|
||||
- Each target is handled independently.
|
||||
- Target errors are reported while remaining targets are attempted; any error yields a nonzero exit.
|
||||
- Missing destination directories are skipped.
|
||||
- Existing regular files are preserved as `.bak` backups (numbered when a backup already exists).
|
||||
- Destination links are replaced atomically; a failed link creation leaves the destination intact.
|
||||
|
||||
The installer links only `AGENTS.md`; it does not install permission configuration, skills, or CLIs.
|
||||
The repository-root `AGENTS.md` contains guidance for this Nix configuration repository. It is not
|
||||
the global rules source and is not installed by this script.
|
||||
Only `AGENTS.md` is deployed to the agents. Permission configuration and skills are not installed by
|
||||
Nix; the CLIs themselves are, via the same module's `packages.nix`. The repository-root `AGENTS.md`
|
||||
contains guidance for this Nix configuration repository. It is not the global rules source and is
|
||||
not deployed.
|
||||
|
||||
Auto-approval controls tool prompting. The global rules still define task authorization, safety, and
|
||||
secret handling.
|
||||
|
||||
Run installer regression tests without changing installed rules:
|
||||
## About `install-skills.md`
|
||||
|
||||
```bash
|
||||
python3 -B -m unittest discover -s agents -p test_install_rules.py
|
||||
```
|
||||
|
||||
## About `install-cli.md` and `install-skills.md`
|
||||
|
||||
Use them as snippet libraries:
|
||||
Use it as a snippet library:
|
||||
|
||||
- review the commands
|
||||
- select what you need
|
||||
@@ -79,8 +66,7 @@ Use them as snippet libraries:
|
||||
|
||||
## TODO
|
||||
|
||||
Ideas worth adopting once a concrete need appears; nothing here is implemented yet, and the install
|
||||
scripts above do not cover it.
|
||||
Ideas worth adopting once a concrete need appears; nothing here is implemented yet.
|
||||
|
||||
- **Shared instructions and skills.** Deploy one rules body plus declared skills to every agent
|
||||
instead of maintaining a copy per agent. Reference: `mirkolenz/infra`
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Agent CLI Commands
|
||||
|
||||
Reference commands for installing and updating agent CLIs. Run only the commands you need.
|
||||
|
||||
## Install CLIs
|
||||
|
||||
Installed via Nix:
|
||||
|
||||
- codex
|
||||
- opencode
|
||||
- kimi-code
|
||||
- pi
|
||||
- omp
|
||||
|
||||
## Optional tooling
|
||||
|
||||
```bash
|
||||
# context7: up-to-date docs and code examples for LLMs and agents
|
||||
npx ctx7 setup
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
The agent CLIs come from Nix (`home/base/gui/dev-tools.nix`) through the `llm-agents` flake input,
|
||||
so update them with the flake rather than npm.
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def install_one(target_dir: Path, source_file: Path, target_name: str) -> None:
|
||||
if not target_dir.exists():
|
||||
print(f"skipped {target_dir} (not found)")
|
||||
return
|
||||
|
||||
target_file = target_dir / target_name
|
||||
|
||||
if target_file.is_file() and not target_file.is_symlink():
|
||||
# Preserve the old inode without reading its contents or replacing an earlier backup.
|
||||
index = 0
|
||||
while True:
|
||||
suffix = ".bak" if index == 0 else f".bak.{index}"
|
||||
backup = target_file.with_name(target_file.name + suffix)
|
||||
try:
|
||||
os.link(target_file, backup)
|
||||
break
|
||||
except FileExistsError:
|
||||
index += 1
|
||||
print(f"backed up {target_file} -> {backup}")
|
||||
|
||||
# Create the link on the same filesystem before atomically replacing the destination.
|
||||
with tempfile.TemporaryDirectory(prefix=".install-rules-", dir=target_dir) as staging:
|
||||
link = Path(staging) / target_name
|
||||
link.symlink_to(source_file)
|
||||
link.replace(target_file)
|
||||
print(f"linked {target_file} -> {source_file}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
agents_file = script_dir / "AGENTS.md"
|
||||
|
||||
if not agents_file.is_file():
|
||||
print(f"Missing source file: {agents_file}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
codex_dir = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
|
||||
xdg_config_home = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser()
|
||||
opencode_dir = xdg_config_home / "opencode"
|
||||
pi_dir = Path("~/.pi/agent").expanduser()
|
||||
omp_dir = Path("~/.omp/agent").expanduser()
|
||||
agents_dir = Path("~/.agents").expanduser()
|
||||
targets = (
|
||||
(codex_dir, "AGENTS.md"),
|
||||
(opencode_dir, "AGENTS.md"),
|
||||
(pi_dir, "AGENTS.md"),
|
||||
(omp_dir, "AGENTS.md"),
|
||||
(agents_dir, "AGENTS.md"),
|
||||
)
|
||||
failed = False
|
||||
for target_dir, target_name in targets:
|
||||
try:
|
||||
install_one(target_dir, agents_file, target_name)
|
||||
except OSError as error:
|
||||
print(f"failed {target_dir / target_name}: {error}", file=sys.stderr)
|
||||
failed = True
|
||||
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -81,6 +81,13 @@ npx skills add anthropics/skills --skill 'pdf'
|
||||
npx skills add trailofbits/skills --skill 'codeql' --skill 'semgrep'
|
||||
```
|
||||
|
||||
## Other agent tooling (npx)
|
||||
|
||||
```bash
|
||||
# context7: up-to-date library docs and code examples for LLMs and agents
|
||||
npx ctx7 setup
|
||||
```
|
||||
|
||||
References:
|
||||
|
||||
- https://github.com/obra/superpowers
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"install_rules", Path(__file__).with_name("install-rules.py")
|
||||
)
|
||||
installer = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(installer)
|
||||
|
||||
|
||||
class InstallRulesTests(unittest.TestCase):
|
||||
def test_main_links_all_supported_agent_targets(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
home = Path(directory)
|
||||
target_names = (
|
||||
(home / ".codex", "AGENTS.md"),
|
||||
(home / ".config" / "opencode", "AGENTS.md"),
|
||||
(home / ".pi" / "agent", "AGENTS.md"),
|
||||
(home / ".omp" / "agent", "AGENTS.md"),
|
||||
(home / ".agents", "AGENTS.md"),
|
||||
)
|
||||
for target_dir, _ in target_names:
|
||||
target_dir.mkdir(parents=True)
|
||||
|
||||
with patch.dict(os.environ, {"HOME": str(home)}, clear=True):
|
||||
self.assertEqual(installer.main(), 0)
|
||||
|
||||
for target_dir, target_name in target_names:
|
||||
target = target_dir / target_name
|
||||
self.assertTrue(target.is_symlink(), target)
|
||||
self.assertEqual(target.resolve(), installer.Path(__file__).with_name("AGENTS.md"))
|
||||
|
||||
def test_target_failure_does_not_stop_remaining_targets(self):
|
||||
with (
|
||||
patch.object(
|
||||
installer,
|
||||
"install_one",
|
||||
side_effect=[PermissionError("denied"), None, None, None, None],
|
||||
) as install,
|
||||
patch("sys.stderr", new_callable=io.StringIO) as stderr,
|
||||
):
|
||||
self.assertEqual(installer.main(), 1)
|
||||
self.assertEqual(install.call_count, 5)
|
||||
self.assertIn("denied", stderr.getvalue())
|
||||
|
||||
def test_successful_targets_return_success(self):
|
||||
with patch.object(installer, "install_one") as install:
|
||||
self.assertEqual(installer.main(), 0)
|
||||
self.assertEqual(install.call_count, 5)
|
||||
|
||||
def test_existing_file_is_backed_up_without_overwriting_backup(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / "source.md"
|
||||
source.write_text("global rules")
|
||||
target = root / "AGENTS.md"
|
||||
target.write_text("personal rules")
|
||||
backup = root / "AGENTS.md.bak"
|
||||
backup.write_text("older rules")
|
||||
installer.install_one(root, source, target.name)
|
||||
self.assertEqual(target.resolve(), source)
|
||||
self.assertEqual(backup.read_text(), "older rules")
|
||||
self.assertEqual((root / "AGENTS.md.bak.1").read_text(), "personal rules")
|
||||
|
||||
def test_failed_link_creation_preserves_existing_file(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / "source.md"
|
||||
source.write_text("global rules")
|
||||
target = root / "AGENTS.md"
|
||||
target.write_text("personal rules")
|
||||
with patch.object(Path, "symlink_to", side_effect=OSError("link failed")):
|
||||
with self.assertRaises(OSError):
|
||||
installer.install_one(root, source, target.name)
|
||||
self.assertTrue(target.is_file())
|
||||
self.assertFalse(target.is_symlink())
|
||||
self.assertEqual(target.read_text(), "personal rules")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -15,6 +15,7 @@ home/
|
||||
│ │ ├── terminal/ # Terminal emulators (Ghostty, Kitty, etc.)
|
||||
│ │ └── ...
|
||||
│ ├── tui/ # Terminal/TUI applications
|
||||
│ │ ├── agents/ # AI coding agents (rules deployment, CLIs, env)
|
||||
│ │ ├── editors/ # Heavy editor/tooling pkgs only (`packages.nix`; core editors live under `core/editors`)
|
||||
│ │ ├── encryption/ # Encryption and security tooling
|
||||
│ │ └── ...
|
||||
|
||||
+3
-2
@@ -24,7 +24,7 @@ systems.
|
||||
### Desktop Environment
|
||||
|
||||
- **gui/**: Cross-platform GUI applications and configurations
|
||||
- **dev-tools.nix**: Development tools and IDEs
|
||||
- **dev-tools.nix**: Misc development and network tools (mitmproxy, wireshark)
|
||||
- **media.nix**: Media players and utilities
|
||||
- **zed-editor.nix**: Zed editor configuration (primary GUI editor)
|
||||
- **terminal/**: Terminal emulator configurations
|
||||
@@ -34,7 +34,8 @@ systems.
|
||||
### Terminal Interface
|
||||
|
||||
- **tui/**: Terminal-based interface configurations
|
||||
- **agent-env.nix**: Telemetry/auto-update opt-outs for AI coding agents
|
||||
- **agents/**: AI coding agents — global rules deployment, CLIs, and telemetry/auto-update
|
||||
opt-outs
|
||||
- **cloud/**: Cloud development tools (Terraform, etc.)
|
||||
- **container.nix**: Container tools (Docker, Podman)
|
||||
- **dev-tools.nix**: Terminal-based development tools
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
llm-agents,
|
||||
...
|
||||
}:
|
||||
let
|
||||
agentPackages = llm-agents.packages.${pkgs.stdenv.hostPlatform.system};
|
||||
in
|
||||
{
|
||||
home.packages =
|
||||
with pkgs;
|
||||
@@ -17,17 +13,5 @@ in
|
||||
++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [
|
||||
mitmproxy # http/https proxy tool
|
||||
wireshark # network analyzer
|
||||
]
|
||||
# AI Agent Tools
|
||||
++ [
|
||||
# Agents
|
||||
agentPackages.codex
|
||||
agentPackages.opencode2
|
||||
agentPackages.kimi-code
|
||||
agentPackages.pi
|
||||
agentPackages.omp
|
||||
|
||||
# MCP servers, configured project-scoped in opencode.jsonc / .codex/config.toml
|
||||
mcp-grafana
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agents
|
||||
|
||||
Home Manager module that owns the Nix side of the AI coding agents: deploying the global rules, the
|
||||
agent CLIs, and their environment.
|
||||
|
||||
The canonical rules text, behavioral scenarios, and external-tool snippets live in the top-level
|
||||
[`agents/`](../../../../agents/README.md) directory. That directory stays portable Markdown with no
|
||||
Nix dependency; this module only wires it into place.
|
||||
|
||||
## Files
|
||||
|
||||
- `rules.nix`: links the global rules to every agent's config location.
|
||||
- `packages.nix`: agent CLIs (`codex`, `opencode2`, `kimi-code`, `pi`, `omp`) and the Grafana MCP
|
||||
server, from the `llm-agents` flake input.
|
||||
- `env.nix`: telemetry and auto-update opt-outs for the agents.
|
||||
|
||||
## Deployed rule targets
|
||||
|
||||
`rules.nix` creates one out-of-store symlink per target, all pointing at
|
||||
`~/nix-config/agents/AGENTS.md`:
|
||||
|
||||
| Agent | Target |
|
||||
| ---------------------- | ------------------------------ |
|
||||
| Codex | `~/.codex/AGENTS.md` |
|
||||
| OpenCode | `~/.config/opencode/AGENTS.md` |
|
||||
| Pi | `~/.pi/agent/AGENTS.md` |
|
||||
| OMP | `~/.omp/agent/AGENTS.md` |
|
||||
| Cross-tool (Kimi Code) | `~/.agents/AGENTS.md` |
|
||||
|
||||
Out-of-store means an edit to `agents/AGENTS.md` takes effect on the next agent session without a
|
||||
Home Manager switch. The trade-off is that the rules stay writable in the checkout; use a store
|
||||
symlink (`home.file.<target>.source = ../../../../agents/AGENTS.md`) if immutability matters more
|
||||
than fast iteration.
|
||||
|
||||
The module is imported through `home/base/tui`, so it applies to every host that imports
|
||||
`home/linux/gui.nix` or the macOS `home/darwin` stack. Core-only servers do not get it.
|
||||
@@ -0,0 +1,4 @@
|
||||
{ mylib, ... }:
|
||||
{
|
||||
imports = mylib.scanPaths ./.;
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,21 @@
|
||||
{
|
||||
pkgs,
|
||||
llm-agents,
|
||||
...
|
||||
}:
|
||||
let
|
||||
agentPackages = llm-agents.packages.${pkgs.stdenv.hostPlatform.system};
|
||||
in
|
||||
{
|
||||
home.packages = [
|
||||
# Agents
|
||||
agentPackages.codex
|
||||
agentPackages.opencode2
|
||||
agentPackages.kimi-code
|
||||
agentPackages.pi
|
||||
agentPackages.omp
|
||||
|
||||
# MCP servers, configured project-scoped in opencode.jsonc / .codex/config.toml
|
||||
pkgs.mcp-grafana
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{ config, ... }:
|
||||
let
|
||||
# The global rules are edited often, so link them out-of-store: an edit reaches
|
||||
# the next agent session without a Home Manager switch. Same rationale as
|
||||
# `home/base/tui/tuios`; the checkout path is hardcoded, like the repo's other
|
||||
# out-of-store links.
|
||||
rules = "${config.home.homeDirectory}/nix-config/agents/AGENTS.md";
|
||||
link = config.lib.file.mkOutOfStoreSymlink rules;
|
||||
in
|
||||
{
|
||||
# One canonical rules file, projected into every agent's config location.
|
||||
home.file = {
|
||||
".codex/AGENTS.md".source = link; # Codex
|
||||
".pi/agent/AGENTS.md".source = link; # Pi
|
||||
".omp/agent/AGENTS.md".source = link; # OMP
|
||||
".agents/AGENTS.md".source = link; # cross-tool, read by Kimi Code
|
||||
};
|
||||
|
||||
xdg.configFile."opencode/AGENTS.md".source = link; # OpenCode
|
||||
}
|
||||
@@ -32,9 +32,6 @@ let
|
||||
modules.secrets.preservation.enable = true;
|
||||
}
|
||||
];
|
||||
# home-modules = map mylib.relativeToRoot [
|
||||
# "home/linux/tui.nix"
|
||||
# ];
|
||||
};
|
||||
|
||||
systemArgs = modules // args;
|
||||
|
||||
Reference in new issue
Block a user