2026-09-19 Declarative Pi Packages and MCP Integration

What I set out to do

Add declarative management for Pi (@earendil-works/pi-coding-agent) packages and extensions in Nix/Home Manager, enabling Model Context Protocol (MCP) servers via pi-mcp-adapter to match the other managed coding agents (Claude Code, Codex, and Gemini).

What I actually did

  1. Investigated Pi configuration model: Pi reads installed packages from ~/.pi/agent/settings.json under "packages": [...]. On startup, missing packages declared in settings are automatically fetched and installed by Pi into ~/.pi/agent/npm/.
  2. Evaluated MCP discovery: Checked pi-mcp-adapter’s discovery hierarchy. The adapter natively searches standard user-global MCP files before Pi-specific overrides:
    • ~/.config/mcp/mcp.json (User-global standard MCP)
    • ~/.pi/agent/mcp.json (Pi global override) Home Manager’s programs.mcp.enable = true in coding-agents/common/mcp.nix already deploys all shared MCP servers (acl-anthology, arxiv, jupyter, litellm, obsidian, playwright, signoz) directly to ~/.config/mcp/mcp.json.
  3. Implemented module options in pi.nix:
    • programs.pi.enable (defaults to true)
    • programs.pi.packages (list of package strings, e.g. "npm:pi-mcp-adapter")
    • programs.pi.extensions (mapping of local TypeScript extension paths to link into ~/.pi/agent/extensions/)
    • programs.pi.enableMcpIntegration (defaults to config.programs.mcp.enable, auto-injecting "npm:pi-mcp-adapter")
  4. Managed settings.json via home.mutableFile: Used RFC 7396 JSON merge patch to inject packages = lib.unique cfg.packages; into ~/.pi/agent/settings.json. Preserves Pi runtime keys (theme, lastChangelogVersion, defaultProvider, defaultModel).
  5. Documented Managed Components in AGENTS.md: Added Pi module tracking under Managed Components.

Verification

  • just check: all pre-commit hooks, linters, nixpkgs-fmt, and formatting passed cleanly.
  • home-manager build and hm switch: generation built and activated without collision or error.
  • Verified ~/.pi/agent/settings.json merged properly with "packages": [ "npm:pi-mcp-adapter" ].
  • Live test via pi -p: verified pi-mcp-adapter discovers and queries tools across both HTTP servers (obsidian, 18 tools) and stdio servers (arxiv, 16 tools).

Commits

  • 7b718c5 feat(pi): manage pi packages, extensions, and mcp integration in nix
  • bb99c06 refactor(pi): extend upstream programs.pi-coding-agent module
  • 5518243 feat(pi): manage skills and wire portable skills in nix
  • f3d338d feat(pi): configure context, prompt templates, and subagents
  • 809d1ca feat(pi): enable automode guardrail via @czottmann/pi-automode
  • 3eb38d3 feat(pi): configure automode to start enabled by default with in-tree allow rules
  • 8fd71bf feat(pi): configure OpenTelemetry GenAI tracing via pi-otel

Follow-up: Extending Upstream programs.pi-coding-agent

Rather than maintaining a standalone programs.pi option namespace, we refactored pi.nix to extend Home Manager’s upstream programs.pi-coding-agent module directly:

  1. Upstream integration:
    • Extended options.programs.pi-coding-agent with packages, extensions, and enableMcpIntegration.
    • Enabled programs.pi-coding-agent by default in the desktop profile (lib.mkDefault true), which automatically installs pkgs.pi-coding-agent (0.85.1) wrapped with pkgs.nodejs in extraPackages into Tier 1 Home Manager packages.
  2. Mutable settings coordination:
    • Upstream generates a read-only Nix store symlink for settings.json whenever cfg.settings != { }. Pi requires this file to be mutable for tracking runtime state (such as lastChangelogVersion and theme).
    • Force-disabled upstream’s symlink (home.file."${cfg.configDir}/settings.json".enable = lib.mkForce false;).
    • Managed settings.json through home.mutableFile using an RFC 7396 merge patch to unify cfg.settings and packages = lib.unique (cfg.packages ++ (cfg.settings.packages or [ ])).
  3. Verification:
    • just check passed cleanly.
    • hm switch built and activated successfully.
    • Verified /Users/achhina/.nix-profile/bin/pi is on PATH and runs version 0.85.1.
    • Ran live verification query with pi -p confirming all 18 Obsidian tools are discovered via pi-mcp-adapter.

Follow-up: Declarative Agent Skills Integration

Pi natively implements the Agent Skills standard. We added declarative skill management to pi.nix:

  1. Skill Option: Added programs.pi-coding-agent.skills (attrsOf path).
  2. Shared Portable Skills: Defaulted skills to commonLib.portableSkills from coding-agents/common/lib.nix, validating each directory with commonLib.validateSkillDir.
  3. Deployment: Symlinked each declared skill into ${cfg.configDir}/skills/<name> (~/.pi/agent/skills/).
  4. Slash Commands: Enabled "enableSkillCommands": true in settings.json so interactive /skill:<name> invocations work directly.
  5. Verification:
    • Checked ~/.pi/agent/skills/ contains all 10 portable skills (commit-message, conflicts, deep-research, mermaid, python, python-configuration, python-observability, python-testing, session-analysis, update-docs).
    • Queried pi -p live, verifying that Pi discovers and presents all 10 skills and their descriptions.
    • Ran just check and committed atomically.

Follow-up: Global Context, Prompt Templates, and Autonomous Subagents

Added complete agent workflows to pi.nix:

  1. Global Context (AGENTS.md):
    • Configured programs.pi-coding-agent.context = ../common/context/AGENTS.md; to deploy shared rules (communication style, TDD rules, Obsidian capture workflow) directly to ~/.pi/agent/AGENTS.md.
  2. Prompt Templates (~/.pi/agent/prompts/):
    • Added programs.pi-coding-agent.prompts option, defaulting to the 5 shared agents in coding-agents/common/agents/ (comment-remover, commit, github-automation, python-reviewer, template-filler).
    • Symlinked into ~/.pi/agent/prompts/<name>.md, providing autocomplete /name slash commands in the Pi interactive editor with argument expansion.
  3. Autonomous Subagents (@tintinweb/pi-subagents):
    • Added programs.pi-coding-agent.enableSubagents (defaulting to true), auto-registering npm:@tintinweb/pi-subagents under packages.
    • Added programs.pi-coding-agent.agents option, symlinking the 5 shared agent definitions into ~/.pi/agent/agents/<name>.md.
    • Enables the Agent tool in Pi for autonomous child-process delegation with isolated context and custom agent types (commit, python-reviewer, etc.).
  4. Verification:
    • Verified ~/.pi/agent/AGENTS.md symlink exists and points to store path.
    • Verified all 5 prompt templates in ~/.pi/agent/prompts/ and all 5 agent definitions in ~/.pi/agent/agents/.
    • Tested live via pi -p, confirming that prompt templates, subagent tools (Agent, SubagentWorkflow, get_subagent_result, steer_subagent), skills, and MCP tools are all loaded and active.
    • Ran just check and committed atomically.

Follow-up: Declarative Auto Mode Guardrails

Integrated Claude Code-style auto mode guardrails into Pi via @czottmann/pi-automode:

  1. Module Option: Added programs.pi-coding-agent.enableAutoMode (bool, default true) to pi.nix.
  2. Package Registration: Injected npm:@czottmann/pi-automode into programs.pi-coding-agent.packages, ensuring declarative propagation into ~/.pi/agent/settings.json via RFC 7396 merge patching.
  3. Capabilities:
    • Deterministic AST command checking via unbash to block destructive root operations and privilege escalations.
    • Dual-stage classification (fast 1-token filter + detailed JSON review) for file writes and shell execution.
    • Persistent status line in the TUI (AM●) tracking allowed vs. denied tool invocations.
    • Slash commands for runtime control: /automode status, /automode on, /automode off, /automode reload, and /automode defaults.
  4. Declarative Default Configuration:
    • Added programs.pi-coding-agent.autoModeSettings option in pi.nix.
    • Deployed ~/.pi/agent/extensions/pi-automode/config.json via home.mutableFile with enabled = true and allowInsideWorkingDirectory = true.
    • Injected local project development permissions into autoMode.allow so local edits, test runs, and builds run autonomously without unnecessary classification blocks, while preserving hard boundaries against system files, credentials, and sensitive configurations.
  5. Verification:
    • just check passed cleanly.
    • hm switch applied without conflict, creating ~/.pi/agent/extensions/pi-automode/config.json.
    • Verified that Pi starts with auto mode enabled by default (AM●).
    • Committed atomically (809d1ca, 3eb38d3).

Follow-up: Declarative OpenTelemetry GenAI Tracing via pi-otel

Integrated OpenTelemetry GenAI semantic conventions tracing into Pi via pi-otel:

  1. Central Telemetry Option: Added local.telemetry.pi.enable (bool, default cfg.enable) in telemetry.nix.
  2. Module Integration in pi.nix:
    • Added programs.pi-coding-agent.enableTelemetry defaulting to config.local.telemetry.active && config.local.telemetry.pi.enable.
    • Added programs.pi-coding-agent.otelSettings to govern the otel configuration block.
    • Injected npm:pi-otel into programs.pi-coding-agent.packages.
    • Configured settings.json.otel via RFC 7396 merge patch:
      • endpoint = config.local.telemetry.endpoint; (pointing to local SigNoz collector at http://localhost:4317)
      • protocol = "grpc";
      • serviceName = "pi";
      • spanNaming = "genai"; (activates OTel GenAI agent conventions: invoke_agent pi, chat {model}, execute_tool {tool})
      • captureContent: follows config.local.telemetry.logContent ("full" vs "metadata_only")
      • propagateToShell = true; (injects TRACEPARENT into bash child processes)
      • signals = { traces = true; metrics = true; logs = false; };
  3. Verification:
    • just check passed cleanly.
    • hm switch applied without collision, updating ~/.pi/agent/settings.json with the otel configuration and package.
    • Pi auto-downloaded and installed pi-otel (85 packages added).
    • Updated Managed Components in AGENTS.md to record OTel tracking.
    • Committed atomically (8fd71bf).
    • Verified live telemetry stream in SigNoz ClickHouse (signoz_traces.signoz_index_v3): 48 spans recorded under serviceName = 'pi', including top-level invoke_agent pi, conversational turns pi.turn, model completions chat openrouter/z-ai/glm-5.3-flash and chat openrouter/moonshotai/kimi-k3, and tool executions (execute_tool bash, execute_tool read, execute_tool mcp, execute_tool automode_inspect).

Follow-up: Structured Model Slug Parser and Catalog Filtering for LiteLLM

Implemented declarative model slug resolution and catalog filtering directly at the LiteLLM gateway layer:

  1. Problem:

    • Clients (Open WebUI, Pi, Codex, curl) had to specify full provider paths (e.g. openrouter/z-ai/glm-5.3-flash).
    • Pi automode’s turn-level bypass sent bare slugs (z-ai/glm-5.3-flash) directly to LiteLLM, resulting in “no healthy deployments”.
    • Model presets (@preset/...) caused cost calculation failures because LiteLLM’s internal cost map lacked keys with preset suffixes.
    • The /v1/models endpoint exposed hundreds of internal permutation variants (:batch, :nitro, :floor).
  2. Architecture and Implementation:

    • Grammar Decomposition: Built litellm_model_resolver.py parsing incoming slugs across [broker/][author/]model[:modifier][@preset/preset].
    • Candidate Matching and Deterministic Ranking: Resolves flat slugs (glm-5.3-flash) and author paths (z-ai/glm-5.3-flash) to canonical wire paths (openrouter/z-ai/glm-5.3-flash). Disambiguates duplicate family names using canonical author heuristics (e.g. meta-llama for llama-*, z-ai for glm-*) and optional environment pins.
    • Preset Support: Strips @preset/... for pricing lookups while re-attaching it for OpenRouter wire calls. Automatically mirrors base cost entries in litellm.model_cost and stamps openrouter_preset into requester_metadata for SigNoz OTel spans.
    • Catalog Filtering: Wraps /v1/models and /models via LITELLM_WORKER_STARTUP_HOOKS. Projects clean, flat model IDs with owned_by set to author. Preserves :free variants while pruning :batch, :nitro, :floor, and @preset/* permutations.
    • Catalog Listener: Added register_catalog_listener to litellm_openrouter_enrichment.py so live 5-minute OpenRouter updates propagate to the resolver index.
    • Pi Extension Simplification: Removed turn-level rewrite in litellm-route.ts, allowing Pi to send canonical or bare slugs transparently.
  3. Verification:

    • Unit test suite: 22/22 unit tests passing in test_litellm_model_resolver.py.
    • Enrichment suite: 28/28 tests passing in test_litellm_openrouter_enrichment.py.
    • Pre-commit and linters: just check passed cleanly across all hooks (ruff, ruff-format, shellcheck, statix, nixpkgs-fmt).
    • Nix evaluation: homeConfigurations."aarch64-darwin" evaluated cleanly.
    • Committed atomically (110afde).

Catalog Middleware Update for FastAPI and Starlette

In live testing, mutating route.endpoint in app.routes did not intercept /v1/models because Starlette compiles route.app = request_response(route.endpoint) at router construction time, and FastAPI wraps routes inside fastapi.routing._IncludedRouter.

  1. Resolution:
    • Implemented CatalogFilterMiddleware as a standard ASGI middleware on LiteLLM’s FastAPI app.
    • Appended to app.user_middleware and invalidated app.middleware_stack = None inside litellm_model_resolver:init, triggering a clean rebuild of Starlette’s middleware pipeline.
    • Intercepts GET requests to /v1/models and /models, filters the response JSON via filter_catalog_models, and updates Content-Length.
  2. Verification:
    • Added unit tests in test_litellm_model_resolver.py covering ASGI message interception, passthrough of non-matching paths/methods, non-JSON responses, and middleware registration idempotency (25/25 tests passing).
    • just check passed cleanly across all linters and hooks.
    • Deployed via hm switch --override-input media-stack /Users/achhina/projects/github.com/achhina/media-stack.
    • Verified live /v1/models returns clean flat IDs (glm-5.3-flash owned by z-ai), retains :free variants (qwen3.8-27b:free, deepseek-v4-flash-0731:free), and excludes unwanted modifiers.
    • Verified live completions with bare glm-5.3-flash and author-qualified z-ai/glm-5.3-flash return 200 OK.
    • Verified @preset/... routes directly to upstream OpenRouter.