--- 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 393–430) already shows `"~/development/*": "allow"`; the four-layer / most-restrictive-wins table is at ≈ lines 343–347. - `README.md` — the four-layer one-liner is at ≈ line 75; the Quick Start config (≈ lines 38–52) 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`.