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

185 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 413
issue_title: "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 `*`)
```jsonc
{
"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`.