fix: pi permissions

This commit is contained in:
m3tm3re
2026-04-14 05:06:40 +02:00
parent cab1f73c89
commit a1d4dfc8c9

View File

@@ -7,9 +7,7 @@
with lib; let with lib; let
cfg = config.coding.agents.pi; cfg = config.coding.agents.pi;
mcpCfg = config.programs.mcp or null; mcpCfg = config.programs.mcp or null;
hasPiPackage = pkgs ? pi; hasPiPackage = pkgs ? pi;
defaultPiImageArchive = defaultPiImageArchive =
if hasPiPackage if hasPiPackage
then then
@@ -43,30 +41,29 @@ with lib; let
in { 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";
container = mkOption { container = mkOption {
description = "Run Pi through a rootless Podman container while keeping a native host UX."; description = "Run Pi through a rootless Podman container while keeping a native host UX.";
default = {}; default = {};
type = types.submodule { type = types.submodule {
options = { options = {
enable = mkEnableOption "Containerized Pi wrapper"; enable = mkEnableOption "Containerized Pi wrapper";
name = mkOption { name = mkOption {
type = types.str; type = types.str;
default = "pi-agent"; default = "pi-agent";
description = "Container name used by the Pi wrapper."; description = "Container name used by the Pi wrapper.";
}; };
image = mkOption { image = mkOption {
type = types.str; type = types.str;
default = if hasPiPackage then "pi-agent:latest" else "docker.io/nixos/nix:latest"; default =
if hasPiPackage
then "pi-agent:latest"
else "docker.io/nixos/nix:latest";
description = '' description = ''
Podman image to run for Pi. Podman image to run for Pi.
Defaults to a local declarative Pi-ready image when `pkgs.pi` exists, Defaults to a local declarative Pi-ready image when `pkgs.pi` exists,
otherwise falls back to docker.io/nixos/nix:latest. otherwise falls back to docker.io/nixos/nix:latest.
''; '';
}; };
imageArchive = mkOption { imageArchive = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = defaultPiImageArchive; default = defaultPiImageArchive;
@@ -76,7 +73,6 @@ in {
generated when `pkgs.pi` is available. generated when `pkgs.pi` is available.
''; '';
}; };
projectRoots = mkOption { projectRoots = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [];
@@ -86,13 +82,11 @@ in {
''; '';
example = ["/home/m3tam3re/p"]; example = ["/home/m3tam3re/p"];
}; };
autoStart = mkOption { autoStart = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
description = "Automatically start container when wrapper is invoked and it is not running."; description = "Automatically start container when wrapper is invoked and it is not running.";
}; };
autoNixDevelop = mkOption { autoNixDevelop = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
@@ -102,13 +96,11 @@ in {
inside the container. inside the container.
''; '';
}; };
extraRunArgs = mkOption { extraRunArgs = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [];
description = "Additional Podman create args appended after safe defaults."; description = "Additional Podman create args appended after safe defaults.";
}; };
extraEnv = mkOption { extraEnv = mkOption {
type = types.attrsOf types.str; type = types.attrsOf types.str;
default = {}; default = {};
@@ -117,10 +109,12 @@ in {
}; };
}; };
}; };
mcpServers = mkOption { mcpServers = mkOption {
type = types.attrsOf types.anything; type = types.attrsOf types.anything;
default = if mcpCfg != null then mcpCfg.servers else {}; default =
if mcpCfg != null
then mcpCfg.servers
else {};
defaultText = literalExpression "config.programs.mcp.servers"; defaultText = literalExpression "config.programs.mcp.servers";
description = '' description = ''
MCP server configurations for Pi (pi-mcp-adapter). MCP server configurations for Pi (pi-mcp-adapter).
@@ -128,7 +122,6 @@ in {
Automatically inherits from config.programs.mcp.servers. Automatically inherits from config.programs.mcp.servers.
''; '';
}; };
agentsInput = mkOption { agentsInput = mkOption {
type = types.nullOr types.anything; type = types.nullOr types.anything;
default = null; default = null;
@@ -138,7 +131,6 @@ in {
all agents are listed in AGENTS.md, and subagent .md files are deployed. all agents are listed in AGENTS.md, and subagent .md files are deployed.
''; '';
}; };
modelOverrides = mkOption { modelOverrides = mkOption {
type = types.attrsOf types.str; type = types.attrsOf types.str;
default = {}; default = {};
@@ -148,7 +140,6 @@ in {
{ chiron = "anthropic/claude-sonnet-4"; chiron-forge = "anthropic/claude-sonnet-4"; } { chiron = "anthropic/claude-sonnet-4"; chiron-forge = "anthropic/claude-sonnet-4"; }
''; '';
}; };
primaryAgent = mkOption { primaryAgent = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
@@ -157,7 +148,6 @@ in {
When null, the first agent with mode="primary" is used. When null, the first agent with mode="primary" is used.
''; '';
}; };
settings = mkOption { settings = mkOption {
type = types.submodule { type = types.submodule {
freeformType = types.attrsOf types.anything; freeformType = types.attrsOf types.anything;
@@ -170,43 +160,36 @@ in {
These are written to ~/.pi/agent/settings.json. These are written to ~/.pi/agent/settings.json.
''; '';
}; };
defaultProvider = mkOption { defaultProvider = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
description = "Default LLM provider (e.g. 'anthropic', 'openai', 'zai')."; description = "Default LLM provider (e.g. 'anthropic', 'openai', 'zai').";
}; };
defaultModel = mkOption { defaultModel = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
description = "Default model ID."; description = "Default model ID.";
}; };
defaultThinkingLevel = mkOption { defaultThinkingLevel = mkOption {
type = types.nullOr (types.enum ["off" "minimal" "low" "medium" "high" "xhigh"]); type = types.nullOr (types.enum ["off" "minimal" "low" "medium" "high" "xhigh"]);
default = null; default = null;
description = "Default extended thinking level."; description = "Default extended thinking level.";
}; };
theme = mkOption { theme = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
description = "Pi theme name."; description = "Pi theme name.";
}; };
hideThinkingBlock = mkOption { hideThinkingBlock = mkOption {
type = types.nullOr types.bool; type = types.nullOr types.bool;
default = null; default = null;
description = "Hide thinking blocks in output."; description = "Hide thinking blocks in output.";
}; };
quietStartup = mkOption { quietStartup = mkOption {
type = types.nullOr types.bool; type = types.nullOr types.bool;
default = null; default = null;
description = "Hide startup header."; description = "Hide startup header.";
}; };
compaction = mkOption { compaction = mkOption {
type = types.nullOr (types.submodule { type = types.nullOr (types.submodule {
options = { options = {
@@ -227,25 +210,21 @@ in {
default = null; default = null;
description = "Auto-compaction settings."; description = "Auto-compaction settings.";
}; };
enabledModels = mkOption { enabledModels = mkOption {
type = types.nullOr (types.listOf types.str); type = types.nullOr (types.listOf types.str);
default = null; default = null;
description = "Model patterns for Ctrl+P cycling."; description = "Model patterns for Ctrl+P cycling.";
}; };
sessionDir = mkOption { sessionDir = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
description = "Directory where session files are stored."; description = "Directory where session files are stored.";
}; };
extensions = mkOption { extensions = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [];
description = "Local extension file paths or directories."; description = "Local extension file paths or directories.";
}; };
skills = mkOption { skills = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [];
@@ -261,7 +240,6 @@ in {
''; '';
}; };
}; };
config = mkIf cfg.enable (let config = mkIf cfg.enable (let
# Build settings.json by filtering out null values recursively # Build settings.json by filtering out null values recursively
filterNulls = attrs: filterNulls = attrs:
@@ -271,12 +249,13 @@ 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;
projectRoots = map toString cfg.container.projectRoots; projectRoots = map toString cfg.container.projectRoots;
projectRootsShell = concatStringsSep " " (map escapeShellArg projectRoots); projectRootsShell = concatStringsSep " " (map escapeShellArg projectRoots);
extraRunArgsShell = concatStringsSep " " (map escapeShellArg cfg.container.extraRunArgs); extraRunArgsShell = concatStringsSep " " (map escapeShellArg cfg.container.extraRunArgs);
@@ -288,39 +267,44 @@ in {
if cfg.container.imageArchive != null if cfg.container.imageArchive != null
then escapeShellArg (toString cfg.container.imageArchive) then escapeShellArg (toString cfg.container.imageArchive)
else ""; else "";
piWrapper = pkgs.writeShellScriptBin "pi" '' piWrapper = pkgs.writeShellScriptBin "pi" ''
set -euo pipefail set -euo pipefail
PODMAN="${pkgs.podman}/bin/podman" PODMAN="${pkgs.podman}/bin/podman"
REALPATH="${pkgs.coreutils}/bin/realpath" REALPATH="${pkgs.coreutils}/bin/realpath"
CONTAINER_NAME=${escapeShellArg cfg.container.name} CONTAINER_NAME=${escapeShellArg cfg.container.name}
IMAGE=${escapeShellArg cfg.container.image} IMAGE=${escapeShellArg cfg.container.image}
IMAGE_ARCHIVE=${imageArchiveShell} IMAGE_ARCHIVE=${imageArchiveShell}
AUTO_START=${if cfg.container.autoStart then "1" else "0"} AUTO_START=${
AUTO_NIX_DEVELOP=${if cfg.container.autoNixDevelop then "1" else "0"} if cfg.container.autoStart
then "1"
else "0"
}
AUTO_NIX_DEVELOP=${
if cfg.container.autoNixDevelop
then "1"
else "0"
}
HOST_PI_DIR=${hostPiDirShell} HOST_PI_DIR=${hostPiDirShell}
PI_CONFIG_MOUNT="/pi-config"
WRAPPER_SCHEMA_VERSION="4"
RUNTIME_HOME="/tmp"
RUNTIME_NPM_PREFIX="/tmp/.npm-global"
RUNTIME_NPM_CACHE="/tmp/.npm-cache"
RUNTIME_TMPDIR="/tmp/.runtime-tmp"
PROJECT_ROOTS=(${projectRootsShell}) PROJECT_ROOTS=(${projectRootsShell})
EXTRA_RUN_ARGS=(${extraRunArgsShell}) EXTRA_RUN_ARGS=(${extraRunArgsShell})
EXTRA_ENV_VARS=(${extraEnvShell}) EXTRA_ENV_VARS=(${extraEnvShell})
err() { err() {
printf "pi-wrapper: %s\n" "$1" >&2 printf "pi-wrapper: %s\n" "$1" >&2
exit 1 exit 1
} }
if [ "''${#PROJECT_ROOTS[@]}" -eq 0 ]; then if [ "''${#PROJECT_ROOTS[@]}" -eq 0 ]; then
err "No allowed projectRoots configured. Set coding.agents.pi.container.projectRoots." err "No allowed projectRoots configured. Set coding.agents.pi.container.projectRoots."
fi fi
if ! command -v "$PODMAN" >/dev/null 2>&1; then if ! command -v "$PODMAN" >/dev/null 2>&1; then
err "podman binary not found at $PODMAN" err "podman binary not found at $PODMAN"
fi fi
CWD="$($REALPATH -m "$PWD")" CWD="$($REALPATH -m "$PWD")"
cwd_allowed=0 cwd_allowed=0
NORMALIZED_ROOTS=() NORMALIZED_ROOTS=()
for root in "''${PROJECT_ROOTS[@]}"; do for root in "''${PROJECT_ROOTS[@]}"; do
@@ -332,7 +316,6 @@ in {
;; ;;
esac esac
done done
if [ "$cwd_allowed" -ne 1 ]; then if [ "$cwd_allowed" -ne 1 ]; then
{ {
printf "pi-wrapper: cwd '%s' is outside allowed projectRoots.\n" "$CWD" printf "pi-wrapper: cwd '%s' is outside allowed projectRoots.\n" "$CWD"
@@ -343,88 +326,96 @@ in {
} >&2 } >&2
exit 1 exit 1
fi fi
tty_args=() tty_args=()
if [ -t 0 ] && [ -t 1 ]; then if [ -t 0 ] && [ -t 1 ]; then
tty_args=(-it) tty_args=(-it)
fi fi
ensure_image_available() { ensure_image_available() {
if [ -n "$IMAGE_ARCHIVE" ] && [ -f "$IMAGE_ARCHIVE" ]; then if [ -n "$IMAGE_ARCHIVE" ] && [ -f "$IMAGE_ARCHIVE" ]; then
"$PODMAN" load -i "$IMAGE_ARCHIVE" >/dev/null "$PODMAN" load -i "$IMAGE_ARCHIVE" >/dev/null
fi fi
if ! "$PODMAN" image exists "$IMAGE"; then if ! "$PODMAN" image exists "$IMAGE"; then
err "Container image '$IMAGE' is not available and no valid imageArchive was provided." err "Container image '$IMAGE' is not available and no valid imageArchive was provided."
fi fi
} }
create_container() { create_container() {
mount_args=() mount_args=()
for root in "''${NORMALIZED_ROOTS[@]}"; do for root in "''${NORMALIZED_ROOTS[@]}"; do
mount_args+=("-v" "$root:$root:rw") mount_args+=("-v" "$root:$root:rw")
done done
if [ ! -S /nix/var/nix/daemon-socket/socket ]; then if [ ! -S /nix/var/nix/daemon-socket/socket ]; then
err "Host Nix daemon socket not found at /nix/var/nix/daemon-socket/socket" err "Host Nix daemon socket not found at /nix/var/nix/daemon-socket/socket"
fi fi
mount_args+=("-v" "/nix/var/nix/daemon-socket/socket:/nix/var/nix/daemon-socket/socket:rw") mount_args+=("-v" "/nix/var/nix/daemon-socket/socket:/nix/var/nix/daemon-socket/socket:rw")
mkdir -p "$HOST_PI_DIR" mkdir -p "$HOST_PI_DIR"
mount_args+=("-v" "$HOST_PI_DIR:/tmp/.pi:rw") mount_args+=("-v" "$HOST_PI_DIR:$PI_CONFIG_MOUNT:rw")
if [ -d /nix/store ]; then if [ -d /nix/store ]; then
mount_args+=("-v" "/nix/store:/nix/store:ro") mount_args+=("-v" "/nix/store:/nix/store:ro")
fi fi
if [ -e /etc/nix/nix.conf ]; then if [ -e /etc/nix/nix.conf ]; then
mount_args+=("-v" "/etc/nix/nix.conf:/etc/nix/nix.conf:ro") mount_args+=("-v" "/etc/nix/nix.conf:/etc/nix/nix.conf:ro")
fi fi
if [ -d /etc/ssl/certs ]; then if [ -d /etc/ssl/certs ]; then
mount_args+=("-v" "/etc/ssl/certs:/etc/ssl/certs:ro") mount_args+=("-v" "/etc/ssl/certs:/etc/ssl/certs:ro")
fi fi
if [ -d /etc/pki ]; then if [ -d /etc/pki ]; then
mount_args+=("-v" "/etc/pki:/etc/pki:ro") mount_args+=("-v" "/etc/pki:/etc/pki:ro")
fi fi
env_args=() env_args=()
for kv in "''${EXTRA_ENV_VARS[@]}"; do for kv in "''${EXTRA_ENV_VARS[@]}"; do
env_args+=("--env" "$kv") env_args+=("--env" "$kv")
done done
"$PODMAN" create \ "$PODMAN" create \
--name "$CONTAINER_NAME" \ --name "$CONTAINER_NAME" \
--hostname "$CONTAINER_NAME" \ --hostname "$CONTAINER_NAME" \
--label io.m3ta.pi-wrapper-version="$WRAPPER_SCHEMA_VERSION" \
--userns keep-id \ --userns keep-id \
--user "$(${pkgs.coreutils}/bin/id -u):$(${pkgs.coreutils}/bin/id -g)" \ --user "$(${pkgs.coreutils}/bin/id -u):$(${pkgs.coreutils}/bin/id -g)" \
--security-opt no-new-privileges \ --security-opt no-new-privileges \
--workdir /tmp \ --workdir /tmp \
--tmpfs /tmp:rw,nodev,nosuid \ --tmpfs /tmp:rw,nodev,nosuid,mode=1777 \
--env HOME=/tmp \ --env HOME="$RUNTIME_HOME" \
--env PI_CODING_AGENT_DIR="$PI_CONFIG_MOUNT/agent" \
--env NIX_REMOTE=daemon \ --env NIX_REMOTE=daemon \
--env NPM_CONFIG_PREFIX=/tmp/.npm-global \ --env TMPDIR="$RUNTIME_TMPDIR" \
--env npm_config_prefix=/tmp/.npm-global \ --env NPM_CONFIG_PREFIX="$RUNTIME_NPM_PREFIX" \
--env NPM_CONFIG_CACHE=/tmp/.npm \ --env npm_config_prefix="$RUNTIME_NPM_PREFIX" \
--env npm_config_cache=/tmp/.npm \ --env NPM_CONFIG_CACHE="$RUNTIME_NPM_CACHE" \
--env PATH=/tmp/.npm-global/bin:/bin:/usr/bin \ --env npm_config_cache="$RUNTIME_NPM_CACHE" \
--env PATH="$RUNTIME_NPM_PREFIX/bin:/bin:/usr/bin" \
"''${mount_args[@]}" \ "''${mount_args[@]}" \
"''${env_args[@]}" \ "''${env_args[@]}" \
"''${EXTRA_RUN_ARGS[@]}" \ "''${EXTRA_RUN_ARGS[@]}" \
"$IMAGE" \ "$IMAGE" \
sleep infinity >/dev/null sleep infinity >/dev/null
} }
runtime_env_args=(
--env "HOME=$RUNTIME_HOME"
--env "PI_CODING_AGENT_DIR=$PI_CONFIG_MOUNT/agent"
--env NIX_REMOTE=daemon
--env "TMPDIR=$RUNTIME_TMPDIR"
--env "NPM_CONFIG_PREFIX=$RUNTIME_NPM_PREFIX"
--env "npm_config_prefix=$RUNTIME_NPM_PREFIX"
--env "NPM_CONFIG_CACHE=$RUNTIME_NPM_CACHE"
--env "npm_config_cache=$RUNTIME_NPM_CACHE"
--env "PATH=$RUNTIME_NPM_PREFIX/bin:/bin:/usr/bin"
)
recreate_container() {
"$PODMAN" rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
ensure_image_available
create_container
}
ensure_container_running() { ensure_container_running() {
if ! "$PODMAN" container exists "$CONTAINER_NAME"; then if ! "$PODMAN" container exists "$CONTAINER_NAME"; then
ensure_image_available ensure_image_available
create_container create_container
else
schema_version="$("$PODMAN" inspect -f '{{ index .Config.Labels "io.m3ta.pi-wrapper-version" }}' "$CONTAINER_NAME" 2>/dev/null || true)"
if [ "$schema_version" != "$WRAPPER_SCHEMA_VERSION" ]; then
recreate_container
fi
fi fi
running="$("$PODMAN" inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || true)"
running="$($PODMAN inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || true)"
if [ "$running" != "true" ]; then if [ "$running" != "true" ]; then
if [ "$AUTO_START" = "1" ]; then if [ "$AUTO_START" = "1" ]; then
"$PODMAN" start "$CONTAINER_NAME" >/dev/null "$PODMAN" start "$CONTAINER_NAME" >/dev/null
@@ -433,20 +424,24 @@ in {
fi fi
fi fi
} }
ensure_runtime_dirs() {
if ! "$PODMAN" exec "''${runtime_env_args[@]}" --workdir /tmp "$CONTAINER_NAME" sh -lc 'mkdir -p "$NPM_CONFIG_PREFIX" "$NPM_CONFIG_CACHE" "$TMPDIR"'; then
err "Failed to prepare writable npm runtime directories in container '$CONTAINER_NAME'."
fi
if ! "$PODMAN" exec "''${runtime_env_args[@]}" --workdir /tmp "$CONTAINER_NAME" sh -lc 'test -w "$NPM_CONFIG_PREFIX" && test -w "$NPM_CONFIG_CACHE" && test -w "$TMPDIR"'; then
err "Container runtime dirs are not writable for npm. Remove stale container with: podman rm -f $CONTAINER_NAME"
fi
}
ensure_container_running ensure_container_running
ensure_runtime_dirs
if [ "$AUTO_NIX_DEVELOP" = "1" ] && [ -f "$CWD/flake.nix" ]; then if [ "$AUTO_NIX_DEVELOP" = "1" ] && [ -f "$CWD/flake.nix" ]; then
exec "$PODMAN" exec "''${tty_args[@]}" --workdir "$CWD" "$CONTAINER_NAME" nix develop -c pi "$@" exec "$PODMAN" exec "''${tty_args[@]}" "''${runtime_env_args[@]}" --workdir "$CWD" "$CONTAINER_NAME" nix develop -c pi "$@"
fi fi
if "$PODMAN" exec "''${runtime_env_args[@]}" --workdir "$CWD" "$CONTAINER_NAME" sh -lc 'command -v pi >/dev/null 2>&1'; then
if "$PODMAN" exec --workdir "$CWD" "$CONTAINER_NAME" sh -lc 'command -v pi >/dev/null 2>&1'; then exec "$PODMAN" exec "''${tty_args[@]}" "''${runtime_env_args[@]}" --workdir "$CWD" "$CONTAINER_NAME" pi "$@"
exec "$PODMAN" exec "''${tty_args[@]}" --workdir "$CWD" "$CONTAINER_NAME" pi "$@"
fi fi
err "Container '$CONTAINER_NAME' does not have 'pi' in PATH (image: $IMAGE). Use a Pi-ready image or run from a flake project with autoNixDevelop=true." err "Container '$CONTAINER_NAME' does not have 'pi' in PATH (image: $IMAGE). Use a Pi-ready image or run from a flake project with autoNixDevelop=true."
''; '';
# Rendered agents (only computed when agentsInput is set) # Rendered agents (only computed when agentsInput is set)
rendered = rendered =
if cfg.agentsInput != null if cfg.agentsInput != null
@@ -458,23 +453,20 @@ in {
primaryAgent = cfg.primaryAgent; primaryAgent = cfg.primaryAgent;
} }
else null; else null;
# 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 = ".pi/agent/agents/${name}.md";
name = ".pi/agent/agents/${name}.md"; value = {text = builtins.readFile "${rendered}/agents/${name}.md";};
value = {text = builtins.readFile "${rendered}/agents/${name}.md";}; })
}) agentNames
agentNames )
)
else {}; else {};
skillsSource = skillsSource =
if cfg.agentsInput != null if cfg.agentsInput != null
then then
@@ -499,49 +491,52 @@ in {
assertion = all (path: hasPrefix "/" (toString path)) cfg.container.projectRoots; assertion = all (path: hasPrefix "/" (toString path)) cfg.container.projectRoots;
message = "coding.agents.pi.container.projectRoots entries must be absolute paths."; message = "coding.agents.pi.container.projectRoots entries must be absolute paths.";
}; };
home.packages = home.packages =
(optional cfg.container.enable piWrapper) (optional cfg.container.enable piWrapper)
++ (optional (!cfg.container.enable && hasPiPackage) pkgs.pi); ++ (optional (!cfg.container.enable && hasPiPackage) pkgs.pi);
home.file = mkMerge [ home.file = mkMerge [
# ── MCP servers from programs.mcp → ~/.pi/agent/mcp.json ─────── # ── MCP servers from programs.mcp → ~/.pi/agent/mcp.json ───────
(mkIf (cfg.mcpServers != {}) { (mkIf (cfg.mcpServers != {}) {
".pi/agent/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;}; ".pi/agent/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;};
}) })
# ── ~/.pi/agent/settings.json ────────────────────────────────── # ── ~/.pi/agent/settings.json ──────────────────────────────────
{ {
".pi/agent/settings.json".text = builtins.toJSON piSettings; ".pi/agent/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".text = builtins.readFile "${rendered}/AGENTS.md"; ".pi/agent/AGENTS.md".text = builtins.readFile "${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".text = builtins.readFile "${rendered}/SYSTEM.md"; ".pi/agent/SYSTEM.md".text = builtins.readFile "${rendered}/SYSTEM.md";
}) })
# ── Agents — pi-subagents .md files ──────────────────────────── # ── Agents — pi-subagents .md files ────────────────────────────
agentFiles agentFiles
# ── Skills symlinked from AGENTS repo (non-container mode) ───── # ── Skills symlinked from AGENTS repo (non-container mode) ─────
(mkIf (cfg.agentsInput != null && !cfg.container.enable) { (mkIf (cfg.agentsInput != null && !cfg.container.enable) {
".pi/agent/skills".source = skillsSource; ".pi/agent/skills".source = skillsSource;
}) })
]; ];
home.activation.piMaterializeSkills = mkIf (cfg.container.enable && cfg.agentsInput != null) ( home.activation.piMaterializeSkills = mkIf (cfg.container.enable && cfg.agentsInput != null) (
lib.hm.dag.entryAfter ["writeBoundary"] '' lib.hm.dag.entryAfter ["writeBoundary"] ''
skillsSrc=${escapeShellArg "${skillsSource}"} skillsSrc=${escapeShellArg "${skillsSource}"}
skillsDst=${escapeShellArg "${config.home.homeDirectory}/.pi/agent/skills"} skillsDst=${escapeShellArg "${config.home.homeDirectory}/.pi/agent/skills"}
skillsParent="$(${pkgs.coreutils}/bin/dirname "$skillsDst")"
${pkgs.coreutils}/bin/rm -rf "$skillsDst" skillsNew="$skillsParent/.skills-new-$$"
${pkgs.coreutils}/bin/mkdir -p "$skillsDst" skillsOld="$skillsParent/.skills-old-$$"
${pkgs.coreutils}/bin/cp -aL "$skillsSrc"/. "$skillsDst"/ ${pkgs.coreutils}/bin/mkdir -p "$skillsParent"
${pkgs.coreutils}/bin/rm -rf "$skillsNew"
${pkgs.coreutils}/bin/mkdir -p "$skillsNew"
${pkgs.coreutils}/bin/cp -aL "$skillsSrc"/. "$skillsNew"/
if [ -e "$skillsDst" ]; then
${pkgs.coreutils}/bin/mv "$skillsDst" "$skillsOld"
fi
${pkgs.coreutils}/bin/mv "$skillsNew" "$skillsDst"
if [ -e "$skillsOld" ]; then
${pkgs.coreutils}/bin/chmod -R u+w "$skillsOld" 2>/dev/null || true
${pkgs.coreutils}/bin/rm -rf "$skillsOld" 2>/dev/null || true
fi
'' ''
); );
}); });