atagen

roundabout

public
Star0
Watch0

Commit 052a1a49371c

tarball

atagen <boss@atagen.co>2026-09-27 03:59 UTC

5 files changed+139-16
nix: nixos module with socket activation and read-only claude hooks

The daemon runs from a systemd user socket by default. Whatever writes the claude settings can pull hooks, skill and plugin from programs.rabt.claude instead of going through claude plugin install. Hooks live in one file so the plugin and the module can't drift.
Modified file: README.md+10 additions
@@ -31,6 +31,16 @@ claude plugin marketplace add ./result
3131 claude plugin install roundabout@roundabout
3232 ```
3333
34+or on nixos
35+
36+```nix
37+imports = [ roundabout.nixosModules.default ];
38+programs.roundabout.enable = true;
39+programs.roundabout.settings.budget.ram = "16GiB"; # lands in /etc/roundabout/config.toml
40+```
41+
42+that installs roundabout and a socket-activated systemd user unit for the daemon (`socketActivation = false` to let clients spawn it instead). `programs.roundabout.claude` has read-only `hooks`, `skill` and `plugin` for whatever writes your claude config, e.g. `settings.hooks = config.programs.roundabout.claude.hooks`.
43+
3444 the plugin brings a skill and hooks. unwrapped builds get bounced back with instructions, overdue builds get mentioned to the agent that started them, and agents over their artifact cap get told to tidy up before they stop. `rabt` needs to be on PATH for the agent too, so install `.#roundabout` alongside.
3545
3646 ## config
Modified file: flake.nix+2 additions
@@ -16,6 +16,8 @@
1616 pkgsFor = system: nixpkgs.legacyPackages.${system};
1717 in
1818 {
19+ nixosModules.default = import ./nix/nixos-module.nix self;
20+
1921 packages = forAllSystems (
2022 system:
2123 let
Added file: nix/hooks.nix+20 additions
@@ -0,0 +1,20 @@
1+{ lib, roundabout }:
2+
3+let
4+ hook = event: {
5+ type = "command";
6+ command = "${lib.getExe roundabout} hook ${event}";
7+ };
8+in
9+{
10+ PreToolUse = [
11+ {
12+ matcher = "Bash";
13+ hooks = [ (hook "pre-tool-use") ];
14+ }
15+ ];
16+ PostToolUse = [ { hooks = [ (hook "post-tool-use") ]; } ];
17+ UserPromptSubmit = [ { hooks = [ (hook "prompt-submit") ]; } ];
18+ Stop = [ { hooks = [ (hook "stop") ]; } ];
19+ SubagentStop = [ { hooks = [ (hook "subagent-stop") ]; } ];
20+}
Added file: nix/nixos-module.nix+106 additions
@@ -0,0 +1,106 @@
1+self:
2+{
3+ config,
4+ lib,
5+ pkgs,
6+ ...
7+}:
8+
9+let
10+ cfg = config.programs.roundabout;
11+ toml = pkgs.formats.toml { };
12+ plugin = pkgs.callPackage ./plugin.nix { roundabout = cfg.package; };
13+in
14+{
15+ options.programs.roundabout = {
16+ enable = lib.mkEnableOption "roundabout, build admission for agents";
17+
18+ package = lib.mkOption {
19+ type = lib.types.package;
20+ default = self.packages.${pkgs.stdenv.hostPlatform.system}.roundabout;
21+ defaultText = lib.literalExpression "roundabout.packages.\${system}.roundabout";
22+ description = "the roundabout package";
23+ };
24+
25+ socketActivation = lib.mkOption {
26+ type = lib.types.bool;
27+ default = true;
28+ description = ''
29+ let a systemd user socket own `$XDG_RUNTIME_DIR/roundabout.sock` and start the
30+ daemon on demand. when disabled, the first client spawns daemon instead
31+ '';
32+ };
33+
34+ settings = lib.mkOption {
35+ inherit (toml) type;
36+ default = { };
37+ example = lib.literalExpression ''
38+ {
39+ budget.ram = "16GiB";
40+ enforce.unwrapped = "wrap";
41+ artifacts.stop = "block";
42+ }
43+ '';
44+ description = ''
45+ written to `/etc/roundabout/config.toml`, which anyone without a
46+ `~/.config/roundabout/config.toml` of their own gets. see readme
47+ '';
48+ };
49+
50+ claude = {
51+ hooks = lib.mkOption {
52+ type = lib.types.attrs;
53+ readOnly = true;
54+ default = import ./hooks.nix {
55+ inherit lib;
56+ roundabout = cfg.package;
57+ };
58+ defaultText = lib.literalMD "roundabout's hooks, keyed by event";
59+ description = ''
60+ hooks in `settings.json` shape, for whatever writes your claude config.
61+ lists concatenate under the json format type, so they sit alongside
62+ hooks you already have
63+ '';
64+ };
65+
66+ skill = lib.mkOption {
67+ type = lib.types.path;
68+ readOnly = true;
69+ default = "${plugin}/plugins/roundabout/skills/roundabout";
70+ defaultText = lib.literalMD "the roundabout skill directory";
71+ description = "the skill directory, for configs that manage `~/.claude/skills` themselves";
72+ };
73+
74+ plugin = lib.mkOption {
75+ type = lib.types.package;
76+ readOnly = true;
77+ default = plugin;
78+ defaultText = lib.literalMD "the roundabout plugin marketplace";
79+ description = "hooks and skill as a marketplace, for `claude plugin marketplace add`";
80+ };
81+ };
82+ };
83+
84+ config = lib.mkIf cfg.enable {
85+ environment.systemPackages = [ cfg.package ];
86+
87+ # a real file rather than the usual /etc/static symlink, so a rebuild
88+ # renames over it where the daemon's inotify watch can see
89+ environment.etc."roundabout/config.toml" = lib.mkIf (cfg.settings != { }) {
90+ source = toml.generate "roundabout-config.toml" cfg.settings;
91+ mode = "0444";
92+ };
93+
94+ systemd.user.sockets.roundabout = lib.mkIf cfg.socketActivation {
95+ description = "roundabout build admission socket";
96+ wantedBy = [ "sockets.target" ];
97+ listenStreams = [ "%t/roundabout.sock" ];
98+ socketConfig.SocketMode = "0600";
99+ };
100+
101+ systemd.user.services.roundabout = lib.mkIf cfg.socketActivation {
102+ description = "roundabout build admission daemon";
103+ serviceConfig.ExecStart = "${lib.getExe cfg.package} daemon";
104+ };
105+ };
106+}
Modified file: nix/plugin.nix+1 additions−16 deletions
@@ -6,24 +6,9 @@
66 }:
77
88 let
9- hook = event: {
10- type = "command";
11- command = "${lib.getExe roundabout} hook ${event}";
12- };
139 hooks = writeText "hooks.json" (
1410 builtins.toJSON {
15- hooks = {
16- PreToolUse = [
17- {
18- matcher = "Bash";
19- hooks = [ (hook "pre-tool-use") ];
20- }
21- ];
22- PostToolUse = [ { hooks = [ (hook "post-tool-use") ]; } ];
23- UserPromptSubmit = [ { hooks = [ (hook "prompt-submit") ]; } ];
24- Stop = [ { hooks = [ (hook "stop") ]; } ];
25- SubagentStop = [ { hooks = [ (hook "subagent-stop") ]; } ];
26- };
11+ hooks = import ./hooks.nix { inherit lib roundabout; };
2712 }
2813 );
2914 plugin = writeText "plugin.json" (