Files
my-pi/pi-permission-system/docs/plans/0413-document-external-directory-allow-list.md

12 KiB
Raw Permalink Blame History

issue, issue_title
issue issue_title
413 Explicitly allow some external directories relative to the home directory

Document the external_directory allow-list for caches like ~/.cargo/registry

Problem Statement

A user wants to stop being prompted every time an agent reads a local cache outside the working directory (e.g. ~/.cargo/registry). They reached for the path surface and configured "~/.cargo/registry": "allow", but it did not work.

Two things defeated that attempt:

  1. Wrong surface for the intent. Access to paths outside the current working directory is governed by the external_directory gate, not the cross-cutting path gate. The four permission layers compose with most-restrictive-wins, so a path allow does not widen access past an external_directory: ask boundary — ask is more restrictive than allow, so the prompt still fires. This is the intended composition (the same invariant that makes a path deny beat a per-tool allow), not a bug.
  2. Glob gap. ~/.cargo/registry (no trailing *) matches only the directory entry itself, not the files beneath it.

The capability the user wants already exists: the external_directory surface accepts a pattern map, so "external_directory": { "*": "ask", "~/.cargo/registry/*": "allow" } does exactly what they asked. The real gap is discoverability — the docs and example config do not show a worked "allow an outside-CWD cache directory" recipe, and they do not clearly distinguish when to use path versus external_directory.

This is a third-party issue (filed by michaelmior, not the maintainer). The operator confirmed the resolution: docs-only. Do not change behavior; in particular, do not make a path allow suppress the external_directory gate — that would violate most-restrictive-wins and silently widen access on existing configs.

Goals

  • Make the existing external_directory allow-list discoverable so users stop reaching for path to allow outside-CWD directories.
  • Add a worked recipe that allows a cache directory such as ~/.cargo/registry via external_directory, using a single trailing *.
  • Clarify the distinction between the path surface (which file paths are allowed, anywhere) and the external_directory surface (whether reaching outside CWD is allowed), and reaffirm most-restrictive-wins.
  • Make the external_directory surface description in the README clear and add a concrete pattern-map example.
  • Add an inline external_directory allow example to the example config and the schema.

Non-Goals

  • No behavior change. Explicitly reject making an explicit path allow suppress the external_directory gate — it breaks most-restrictive-wins and is a security regression, not a fix.
  • No new config field, surface, or schema property.
  • No troubleshooting.md entry (operator deselected it).
  • No ** (globstar) syntax in any new example. A single * is a greedy match that already crosses subdirectory boundaries, so ~/.cargo/registry/* matches every file beneath the directory.
  • No changes to piInfrastructureReadPaths behavior. It remains a read-only auto-allow list; the recipe uses external_directory because the operator chose that surface and it covers all tools, not just reads.
  • No code, no tests — the next stage is /build-plan, not /tdd-plan.

Background

Relevant existing surfaces and docs:

  • external_directory gate — src/handlers/gates/external-directory.ts. Fires for any path outside CWD; resolves via resolver.resolve("external_directory", { path }, …), so it already honors a pattern map keyed by home-expanded path patterns. The pipeline order (src/handlers/gates/tool-call-gate-pipeline.ts) runs the path gate (#2) before external_directory (#3); an allow from the path gate returns null and never short-circuits the later gate.
  • Wildcard semantics — src/wildcard-matcher.ts. compileWildcardPattern home-expands the pattern, then turns each * into .* compiled with the s flag, so * is greedy and crosses / boundaries. ** collapses to the same regex; it is therefore not a distinct globstar and is unnecessary.
  • Home expansion — src/expand-home.ts (issue 350). Both pattern keys and tool/bash path values are home-expanded, so ~/.cargo/registry/* matches a read whose path is ~/.cargo/registry/… or its absolute form.

Docs and config to touch:

  • docs/configuration.md — the external_directory Surface section (≈ lines 393430) already shows "~/development/*": "allow"; the four-layer / most-restrictive-wins table is at ≈ lines 343347.
  • README.md — the four-layer one-liner is at ≈ line 75; the Quick Start config (≈ lines 3852) shows "external_directory": "ask" as a bare string.
  • config/config.example.json — the external_directory map already contains "~/development/*": "allow".
  • schemas/permissions.schema.json — the permission markdownDescription and the examples array (which currently shows "external_directory": "ask").

Constraints from AGENTS.md / package skill:

  • "Keep schema, example config, docs/configuration.md, README.md, and TypeScript types/loaders aligned — changing one without the others is a bug." This change touches docs/example/schema only (no types/loaders), but they must stay mutually consistent.
  • Markdown conventions: one sentence per line; compact tables; sequential list numbering restarting under each heading; fenced blocks need a language; backtick-wrap identifiers and paths.
  • The package skill notes the example config should gate write and edit together — unaffected here (we touch external_directory only).

Design Overview

Documentation only. The mechanism already works; the edits make it visible and teach the right surface.

Mental model to convey

  • path answers "is this file path allowed at all?" and applies everywhere (tools, bash, MCP, extension tools) — use it to deny sensitive files (.env, ~/.ssh/*) globally.
  • external_directory answers "is reaching outside the working directory allowed?" — use it to allow specific outside-CWD directories (caches, sibling projects) without opening all external access.
  • The layers compose with most-restrictive-wins, so allowing an outside-CWD directory belongs in external_directory, and an explicit path allow cannot loosen an external_directory: ask boundary.

The recipe (single *)

{
  "permission": {
    "external_directory": {
      "*": "ask",
      "~/.cargo/registry/*": "allow"
    }
  }
}

A single trailing * is greedy and crosses subdirectory boundaries, so this allows every file under ~/.cargo/registry (e.g. ~/.cargo/registry/index/…, ~/.cargo/registry/src/…/lib.rs). Do not write ~/.cargo/registry/**** is not a distinct globstar and * already recurses.

Edge cases to keep honest in the wording

  • The pattern is stored and displayed as written (~/.cargo/registry/*) in logs and prompts — already documented in the Home Directory Expansion section; the new recipe should not contradict it.
  • For read-only caches, piInfrastructureReadPaths is an alternative that auto-allows reads and bypasses the gate, but it is read-only. Mention it as a one-line cross-reference at most; the primary recipe stays on external_directory (works for all tools).

Module-Level Changes

Docs/config/schema only — no src/ changes.

  • docs/configuration.md
    • In the external_directory Surface section, add a "cache directory" recipe block using ~/.cargo/registry/*, with a sentence on single-* crossing subdirectory boundaries.
    • Add a short "path vs external_directory — which surface?" clarification (a sentence or compact bullet pair) so readers pick external_directory for outside-CWD allows.
    • Reaffirm most-restrictive-wins where the recipe lives (a path allow cannot loosen an external_directory: ask boundary).
    • Do not introduce any ** example; if an adjacent sentence is edited, keep single-* idiom.
  • README.md
    • Replace or augment the bare "external_directory": "ask" in Quick Start (or the four-layer paragraph) with a clear one-sentence description of the surface plus a small pattern-map example allowing an outside-CWD directory.
    • Keep it brief; the full recipe lives in configuration.md.
  • config/config.example.json
    • Add a second external_directory allow entry for a cache directory (e.g. "~/.cargo/registry/*": "allow") alongside the existing "~/development/*": "allow", so the pattern-map idiom is visible.
  • schemas/permissions.schema.json
    • Update the examples array entry to show external_directory as a pattern map (matching the example config) instead of the bare "ask" string, and/or add a sentence to the permission markdownDescription noting that external_directory accepts a pattern map for allowing specific outside-CWD directories.
    • Keep schema examples and config/config.example.json consistent with each other.

No file is added, renamed, or removed; no symbol is removed, so no src//test//SKILL.md symbol grep is required. No docs/architecture/ layout/metric tables reference these doc files.

Test Impact Analysis

Not applicable — docs/config/schema only, no code under test.

Verification is the lint/build gate, not new unit tests:

  • pnpm --filter @gotgenes/pi-permission-system run lint (rumdl markdown rules + JSON).
  • Confirm config/config.example.json and schemas/permissions.schema.json still parse and that the example validates against the schema if a validation script exists.
  • Confirm docs/configuration.md, README.md, config.example.json, and schema agree on the external_directory pattern-map form (the AGENTS.md alignment rule).

Invariants at risk

  • Most-restrictive-wins composition. The docs must not imply a path allow can loosen an external_directory: ask boundary; the new wording reinforces the invariant rather than weakening it. This is a prose invariant — pinned by the existing composition tests in test/ (no code change here, so they stay green).

Build Order

Docs-only; each step ends in a docs: commit.

  1. docs:configuration.md. Add the ~/.cargo/registry/* recipe to the external_directory section, the "path vs external_directory" clarification, and the single-* note; reaffirm most-restrictive-wins. Commit: docs(pi-permission-system): document external_directory allow-list for outside-CWD caches (#413).

  2. docs:README.md. Describe the external_directory surface clearly and add a small pattern-map allow example. Commit: docs(pi-permission-system): clarify external_directory surface in README (#413).

  3. docs:config/config.example.json and schemas/permissions.schema.json. Add the cache-dir allow entry to the example and align the schema example/description to the pattern-map form. Commit: docs(pi-permission-system): show external_directory allow-list in example config and schema (#413).

These three steps may be squashed into one docs: commit if review prefers a single reviewable change; keep them separate if the diff is large. Run the package lint after step 3.

Risks and Mitigations

  • Risk: a new example uses ~/.cargo/registry/** and teaches the wrong idiom. Mitigation: every new example uses a single trailing *; the plan's Non-Goals forbid **.
  • Risk: docs drift between configuration.md, README.md, example config, and schema. Mitigation: step 3 aligns example + schema in one commit; the verification step cross-checks all four surfaces.
  • Risk: wording implies the prior behavior was a bug, inviting a future code change that breaks most-restrictive-wins. Mitigation: frame external_directory as the intended surface and explicitly state that a path allow cannot loosen an external_directory boundary.
  • Risk: the user's underlying case is read-only caches and piInfrastructureReadPaths would be lighter-weight. Mitigation: keep external_directory as the primary recipe (operator's choice; covers all tools) and add at most a one-line cross-reference.

Open Questions

  • Should schemas/permissions.schema.json keep a bare-string external_directory example anywhere (to show the shorthand) while the primary example uses the pattern map? Defer to the build step — prefer the pattern-map example for discoverability; the shorthand is already covered by the surface-shorthand rule.
  • One-line cross-reference to piInfrastructureReadPaths for read-only caches: include in configuration.md only, or omit to keep the recipe focused? Defer to the build step; lean toward a single sentence in configuration.md.