Skip to content

fix(pi): scope the runtime config and agent identity to the checkout - #110

Merged
ubercylon8 merged 1 commit into
mainfrom
fix/pi-project-scope
Aug 22, 2026
Merged

ubercylon8 merged 1 commit into
mainfrom
fix/pi-project-scope

Conversation

@ubercylon8

Copy link
Copy Markdown
Owner

What & why

scripts/sync_pi_config.py defaulted to ~/.pi/agent, so the nine SOC servers
were installed globally: every pi session on the machine — unrelated repos
included — paid for their tool schemas in context and spawned nine uv
subprocesses at startup. f0_pentest already ships project-scoped for exactly
this reason; this brings f0_sectools in line, and adds the root AGENTS.md
entry point that repo already has.

The default is now <checkout>/.pi, created on demand for a fresh clone;
--pi-home ~/.pi/agent is the opt-in for a global install. pi-mcp-extension
reads <cwd>/.pi/mcp.json and lets it win over the global config, so the
servers load only when pi is started from the checkout root (it matches <cwd>
exactly and does not walk up).

Two things this exposed and fixed along the way:

  • The AGENTS.md symlink written beside the rendered mcp.json was never read
    by anything — pi loads context files from ~/.pi/agent and from cwd upward,
    never from <cwd>/.pi/. Project scope now skips it and the root AGENTS.md
    carries the identity instead; a global install still links it, having no repo
    root above it to carry one.
  • A first-time install printed previous saved as .bak without having written
    a backup.

Note for anyone syncing an existing install: after this, run
uv run python scripts/sync_pi_config.py once and remove the repo's servers
from ~/.pi/agent/mcp.json by hand — the script deliberately does not reach
into a pi home it was not pointed at.

Verified end to end on a live install: pi started in the checkout loads the
router and all nine servers; started anywhere else it loads no context file and
only the unrelated f0-library server.

Checklist (mirrors the Critical Rules in CLAUDE.md)

  • Read-only by default — no tool or gating change; this is local install tooling.
  • Returns the findings schema — n/a, no tool changes.
  • Redaction at the boundary — n/a, no tool changes.
  • Safety logic stays in core/ — untouched.
  • Small-model-safe — unchanged; scoping the servers reduces the schema count in unrelated sessions.
  • Eval task added — n/a, no new tools.
  • No secrets staged — docs, a script, its tests, AGENTS.md, .gitignore.
  • uv run pytest (1089 passed, 1 skipped) and uv run ruff check . pass.

🤖 Generated with Claude Code

`scripts/sync_pi_config.py` defaulted to `~/.pi/agent`, so the nine SOC
servers were installed globally: every pi session on the machine — unrelated
repos included — paid for their tool schemas in context and spawned nine `uv`
subprocesses at startup. f0_pentest already ships project-scoped for exactly
this reason; this brings f0_sectools in line.

- Default `--pi-home` is now `<checkout>/.pi`, created on demand for a fresh
  clone. `--pi-home ~/.pi/agent` is the opt-in for a global install and still
  errors if that directory is missing, since that means pi is not installed.
- Add a root `AGENTS.md`, modelled on f0_pentest's: pi and opencode read the
  first context file per directory, so `AGENTS.md` is read *instead of*
  `CLAUDE.md` rather than alongside it. It routes between developing the repo
  and operating a SOC instead of being an operator persona itself.
- Project scope no longer writes an `AGENTS.md` beside the rendered
  `mcp.json`: pi reads context files from `~/.pi/agent` and from cwd upward,
  never from `<cwd>/.pi/`, so that copy was one pi never loaded. A global
  install still links the identity, having no repo root to carry one.
- Report "created" vs "updated, previous saved as .bak" honestly — the old
  message named a backup it had not written on a first install.
- Ignore `/.pi/`, matching f0_pentest.

Verified end to end: pi started in the checkout loads the router and the nine
servers; started anywhere else it loads no context file and only `f0-library`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Claude finished @ubercylon8's task in 1m 56s —— View job


Security review — PR #110

  • Gather context (PR diff, changed files)
  • Review .gitignore change
  • Review AGENTS.md (new file)
  • Review docs/user-guide/runtimes/pi.md changes
  • Review scripts/sync_pi_config.py changes (core logic)
  • Review scripts/test_sync_pi_config.py changes (test coverage)
  • Post final findings

Summary

This PR is local dev/install tooling for the pi runtime — it doesn't touch core/, any MCP server, gating, redaction, or the findings schema. Against the CLAUDE.md Critical Rules I checked for: no gated-write behavior change, no secret handling change, no tool schema change. I found no significant security issues.

What I checked

  • Secrets: integrations/pi/mcp.json (the template rendered by the script) contains only server launch commands (uv run --directory <repo> f0-*-mcp) — no credentials, tokens, or .env contents. render_mcp_json (scripts/sync_pi_config.py:43) does plain placeholder substitution and validates the result is still JSON before writing (json.loads(text) at line 47) — it can't silently write malformed/partial config.
  • New /.pi/ gitignore entry (.gitignore:69): correctly anchored to the repo root (leading /), matching DEFAULT_PI_HOME = REPO / ".pi" (scripts/sync_pi_config.py:39). The generated mcp.json/.bak files (paths only, no secrets) stay out of git; .pi/ was never tracked previously, so this isn't unignoring anything sensitive.
  • merge_into_existing (scripts/sync_pi_config.py:51-83, unchanged by this PR): ownership of existing entries is decided by checking whether the repo's own path string appears in the entry JSON (marker not in json.dumps(entry)) — a reasonable heuristic for "not naming this checkout," not a new attack surface since input is the operator's own local pi config, not untrusted/remote data.
  • Backup-message bugfix (scripts/sync_pi_config.py:118-123): had_previous now correctly gates both the .bak copy and the "previous saved as .bak" message — previously the message could print without a backup existing. Correctly fixed and matches the PR description.
  • Project-scope default (DEFAULT_PI_HOME = REPO / ".pi", scripts/sync_pi_config.py:39,95-98): only creates the directory when not --check (no side effects in check mode), stays inside the checkout (no path traversal / no writes outside the repo tree), and correctly skips the AGENTS.md symlink step for project scope (avoids a second, never-read identity copy) while still symlinking it for --pi-home (global) installs (scripts/sync_pi_config.py:129-150).
  • Test coverage (scripts/test_sync_pi_config.py:68-109): good coverage of the new behavior — default stays project-scoped and never resolves to ~, fresh-clone bootstrap, explicit --pi-home typo still errors instead of silently creating a directory, project scope leaves AGENTS.md alone, global scope still symlinks it. These map directly onto the scope-change described in the PR body.

Minor observations (non-blocking)

  • integrations/pi/mcp.json still includes f0-pa-actions (the gated-write ProjectAchilles companion server) in the default template — unchanged by this PR, and consistent with the repo's model: the server loading is separate from its write tools being gated by PROJECTACHILLES_ALLOW_WRITE + confirmation, so this isn't a regression, just noting it was in scope of what I checked.
  • Nothing else stood out — the diff is docs + local tooling + tests, matches its stated intent, and the CI checklist in the PR body (pytest/ruff) is consistent with what I read.

@ubercylon8
ubercylon8 merged commit 3d07e9f into main Aug 22, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant