diff --git a/modules/home-manager/coding/agents/pi.nix b/modules/home-manager/coding/agents/pi.nix index 99b762c..8aa74d8 100644 --- a/modules/home-manager/coding/agents/pi.nix +++ b/modules/home-manager/coding/agents/pi.nix @@ -202,6 +202,37 @@ See pi docs/settings.md for all options. ''; }; + + # ── Pi Guardrails ───────────────────────────────────────────── + guardrails = mkOption { + type = types.nullOr (types.submodule { + options = { + enable = mkEnableOption + "Generate ~/.pi/agent/extensions/guardrails.json for pi-guardrails. " + + "Adds @aliou/pi-guardrails to packages automatically."; + + config = mkOption { + type = types.attrsOf types.anything; + default = {}; + description = '' + Guardrails configuration written to ~/.pi/agent/extensions/guardrails.json. + See https://github.com/aliou/pi-guardrails for config schema. + + IMPORTANT: Path access checks are lexical (not symlink-safe). + Local project .pi/extensions/guardrails.json can override same rule IDs + (memory > local > global > defaults). For immutable global policies, + consider a wrapper or upstream patch. + ''; + }; + }; + }); + default = null; + description = '' + Pi Guardrails security configuration. + Generates ~/.pi/agent/extensions/guardrails.json when enabled. + The @aliou/pi-guardrails package is added to settings.packages automatically. + ''; + }; }; config = with lib; let @@ -224,8 +255,33 @@ attrs ); + # Base settings (already filtered) piSettings = filterNulls cfg.settings; + # Guardrails package to inject when guardrails is enabled + guardrailsPackage = "npm:@aliou/pi-guardrails@0.11.1"; + + # Guardrails config (only when guardrails is enabled) + guardrailsJson = if (cfg.guardrails != null && cfg.guardrails.enable) + then builtins.toJSON cfg.guardrails.config + else null; + + # Merge guardrails package into settings.packages when guardrails is enabled + piSettingsWithGuardrails = let + baseSettings = cfg.settings; + basePackages = baseSettings.packages or []; + hasGuardrailsPackage = lib.any + (p: lib.hasPrefix "npm:@aliou/pi-guardrails" p || + (lib.hasPrefix "git:" p && lib.hasSuffix "/pi-guardrails" p)) + basePackages; + packagesWithGuardrails = if (cfg.guardrails != null && cfg.guardrails.enable && !hasGuardrailsPackage) + then basePackages ++ [guardrailsPackage] + else basePackages; + in + if packagesWithGuardrails != basePackages + then filterNulls (baseSettings // {packages = packagesWithGuardrails;}) + else piSettings; + # Coding rules config for renderForPi (only when both agentsInput and codingRules are set) piCodingRules = if cfg.agentsInput != null && cfg.codingRules != null @@ -269,10 +325,16 @@ # ── ~/.pi/agent/settings.json ────────────────────────────────── { - ".pi/agent/settings.json".text = builtins.toJSON piSettings; + ".pi/agent/settings.json".text = builtins.toJSON piSettingsWithGuardrails; ".pi/agent/settings.json".force = true; } + # ── pi-guardrails config ───────────────────────────────────── + (mkIf (guardrailsJson != null) { + ".pi/agent/extensions/guardrails.json".text = guardrailsJson; + ".pi/agent/extensions/guardrails.json".force = true; + }) + # ── AGENTS.md — agent descriptions and specialist listing ────── (mkIf (cfg.agentsInput != null) { ".pi/agent/AGENTS.md".source = "${rendered}/AGENTS.md";