179 lines
5.6 KiB
Nix
Executable File
179 lines
5.6 KiB
Nix
Executable File
{
|
|
lib,
|
|
pkgs,
|
|
config,
|
|
namespace,
|
|
...
|
|
}:
|
|
let
|
|
inherit (lib) mkIf;
|
|
|
|
helpers = import ./lib.nix { inherit lib; };
|
|
llamaSwapConfig = import ./../../../../nixos/services/llama-swap/config.nix { inherit pkgs; };
|
|
|
|
cfg = config.${namespace}.programs.terminal.pi;
|
|
|
|
# Nix-Owned Plugin List - Source of truth for `packages` in settings.json.
|
|
# Merged into the (mutable) settings.json on activation so pi can keep
|
|
# writing other fields (current model, etc.) without us clobbering them.
|
|
piPackages = [
|
|
"https://gitea.va.reichard.io/evan/pi-lsp.git@main"
|
|
"https://gitea.va.reichard.io/evan/pi-statusline.git@main"
|
|
];
|
|
|
|
piPackagesJson = pkgs.writeText "pi-packages.json" (builtins.toJSON piPackages);
|
|
|
|
# Pi Auth Api Keys - These are merged into mutable auth.json on activation.
|
|
# Add another entry here to manage another top-level provider auth key.
|
|
piAuthApiKeys = [
|
|
{
|
|
provider = "zai";
|
|
secretName = "zai_apikey";
|
|
jqVar = "zai";
|
|
sopsFile = lib.snowfall.fs.get-file "secrets/common/evanreichard.yaml";
|
|
}
|
|
];
|
|
|
|
piAuthJqRawfiles = lib.concatStringsSep " \\\n " (
|
|
map (
|
|
auth: ''--rawfile ${auth.jqVar} "${config.sops.secrets.${auth.secretName}.path}"''
|
|
) piAuthApiKeys
|
|
);
|
|
|
|
piAuthJqFilter = lib.concatStringsSep " | " (
|
|
map (
|
|
auth: ''.["${auth.provider}"] = { type: "api_key", key: ($'' + auth.jqVar + ''| rtrimstr("\n")) }''
|
|
) piAuthApiKeys
|
|
);
|
|
|
|
piAuthMergeScript = pkgs.writeShellScript "pi-auth-merge" ''
|
|
set -euo pipefail
|
|
|
|
PI_AUTH="$HOME/.pi/agent/auth.json"
|
|
mkdir -p "$(dirname "$PI_AUTH")"
|
|
|
|
if [ -L "$PI_AUTH" ]; then
|
|
rm "$PI_AUTH"
|
|
fi
|
|
|
|
for secret in ${
|
|
lib.concatStringsSep " " (
|
|
map (auth: ''"${config.sops.secrets.${auth.secretName}.path}"'') piAuthApiKeys
|
|
)
|
|
}; do
|
|
if [ ! -e "$secret" ]; then
|
|
echo "Skipping pi auth merge; missing sops secret: $secret" >&2
|
|
exit 0
|
|
fi
|
|
done
|
|
|
|
[ -s "$PI_AUTH" ] || echo '{}' > "$PI_AUTH"
|
|
tmp=$(mktemp)
|
|
${pkgs.jq}/bin/jq ${piAuthJqRawfiles} \
|
|
'${piAuthJqFilter}' "$PI_AUTH" > "$tmp"
|
|
mv "$tmp" "$PI_AUTH"
|
|
chmod 600 "$PI_AUTH"
|
|
'';
|
|
in
|
|
{
|
|
options.${namespace}.programs.terminal.pi = {
|
|
enable = lib.mkEnableOption "enable pi";
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
# Enable Glimpse
|
|
${namespace}.programs.terminal.glimpse.enable = true;
|
|
|
|
# Add Pi Coding Agent to Home Packages
|
|
home.packages = with pkgs; [ reichard.pi-coding-agent ];
|
|
|
|
# Define Pi Configuration
|
|
home.file = {
|
|
|
|
".pi/agent/AGENTS.md" = {
|
|
source = ./config/AGENTS.md;
|
|
};
|
|
".pi/agent/skills" = {
|
|
source = ./config/skills;
|
|
recursive = true;
|
|
};
|
|
".pi/agent/prompts" = {
|
|
source = ./config/prompts;
|
|
recursive = true;
|
|
};
|
|
".pi/agent/extensions" = {
|
|
source = ./config/extensions;
|
|
recursive = true;
|
|
};
|
|
};
|
|
|
|
# Pi Models Config - Inject llama-swap API key from sops into models.json
|
|
# so pi can authenticate against the llm-api endpoint.
|
|
sops = lib.mkIf config.${namespace}.security.sops.enable {
|
|
secrets = {
|
|
"llama_swap_api_keys/pi" = {
|
|
sopsFile = lib.snowfall.fs.get-file "secrets/common/llama-swap.yaml";
|
|
};
|
|
}
|
|
// lib.listToAttrs (
|
|
map (auth: {
|
|
name = auth.secretName;
|
|
value.sopsFile = auth.sopsFile;
|
|
}) piAuthApiKeys
|
|
);
|
|
templates."pi-models.json" = {
|
|
path = "${config.home.homeDirectory}/.pi/agent/models.json";
|
|
content = builtins.toJSON {
|
|
providers = {
|
|
"llama-swap" = {
|
|
baseUrl = "https://llm-api.va.reichard.io/v1";
|
|
api = "openai-completions";
|
|
apiKey = config.sops.placeholder."llama_swap_api_keys/pi";
|
|
models = helpers.toPiModels llamaSwapConfig;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
# Merge Nix-Defined Plugins Into Mutable settings.json - We can't symlink
|
|
# this file into the nix store because pi rewrites it at runtime (e.g. to
|
|
# persist the last-used model). Instead, on every activation we use jq to
|
|
# set `.packages` from Nix while preserving every other field.
|
|
home.activation.piSettingsMerge = config.lib.dag.entryAfter [ "writeBoundary" ] ''
|
|
PI_SETTINGS="$HOME/.pi/agent/settings.json"
|
|
mkdir -p "$(dirname "$PI_SETTINGS")"
|
|
[ -s "$PI_SETTINGS" ] || echo '{}' > "$PI_SETTINGS"
|
|
tmp=$(mktemp)
|
|
${pkgs.jq}/bin/jq --slurpfile pkgs ${piPackagesJson} \
|
|
'.packages = $pkgs[0]' "$PI_SETTINGS" > "$tmp"
|
|
mv "$tmp" "$PI_SETTINGS"
|
|
'';
|
|
|
|
# Merge Api Key Auth Into Mutable auth.json - Pi needs auth.json to stay
|
|
# writable, so merge sops-managed API keys instead of symlinking the whole
|
|
# file. Existing provider auth entries are preserved.
|
|
home.activation.piAuthMerge = lib.mkIf config.${namespace}.security.sops.enable (
|
|
config.lib.dag.entryAfter [ "sops-nix" "writeBoundary" ] ''
|
|
${piAuthMergeScript}
|
|
''
|
|
);
|
|
|
|
# Run Pi Auth Merge After Sops - During NixOS system activation, sops-nix
|
|
# can be restarted asynchronously and secrets may not exist yet. This user
|
|
# service retries the merge in the normal user systemd graph after sops-nix.
|
|
systemd.user.services.pi-auth-merge = lib.mkIf config.${namespace}.security.sops.enable {
|
|
Unit = {
|
|
Description = "Merge sops-managed Pi auth entries";
|
|
After = [ "sops-nix.service" ];
|
|
Requires = [ "sops-nix.service" ];
|
|
};
|
|
Service = {
|
|
Type = "oneshot";
|
|
ExecStart = piAuthMergeScript;
|
|
};
|
|
Install.WantedBy = [ "default.target" ];
|
|
};
|
|
};
|
|
}
|