12 KiB
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:
- Wrong surface for the intent.
Access to paths outside the current working directory is governed by the
external_directorygate, not the cross-cuttingpathgate. The four permission layers compose with most-restrictive-wins, so apathallow does not widen access past anexternal_directory: askboundary —askis more restrictive thanallow, so the prompt still fires. This is the intended composition (the same invariant that makes apathdeny beat a per-tool allow), not a bug. - 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_directoryallow-list discoverable so users stop reaching forpathto allow outside-CWD directories. - Add a worked recipe that allows a cache directory such as
~/.cargo/registryviaexternal_directory, using a single trailing*. - Clarify the distinction between the
pathsurface (which file paths are allowed, anywhere) and theexternal_directorysurface (whether reaching outside CWD is allowed), and reaffirm most-restrictive-wins. - Make the
external_directorysurface description in the README clear and add a concrete pattern-map example. - Add an inline
external_directoryallow example to the example config and the schema.
Non-Goals
- No behavior change.
Explicitly reject making an explicit
pathallow suppress theexternal_directorygate — it breaks most-restrictive-wins and is a security regression, not a fix. - No new config field, surface, or schema property.
- No
troubleshooting.mdentry (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
piInfrastructureReadPathsbehavior. It remains a read-only auto-allow list; the recipe usesexternal_directorybecause 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_directorygate —src/handlers/gates/external-directory.ts. Fires for any path outside CWD; resolves viaresolver.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 thepathgate (#2) beforeexternal_directory(#3); anallowfrom thepathgate returnsnulland never short-circuits the later gate.- Wildcard semantics —
src/wildcard-matcher.ts.compileWildcardPatternhome-expands the pattern, then turns each*into.*compiled with thesflag, 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— theexternal_directorySurface 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— theexternal_directorymap already contains"~/development/*": "allow".schemas/permissions.schema.json— thepermissionmarkdownDescriptionand theexamplesarray (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
writeandedittogether — unaffected here (we touchexternal_directoryonly).
Design Overview
Documentation only. The mechanism already works; the edits make it visible and teach the right surface.
Mental model to convey
pathanswers "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_directoryanswers "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 explicitpathallow cannot loosen anexternal_directory: askboundary.
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,
piInfrastructureReadPathsis 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 onexternal_directory(works for all tools).
Module-Level Changes
Docs/config/schema only — no src/ changes.
docs/configuration.md- In the
external_directorySurface 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_directoryfor outside-CWD allows. - Reaffirm most-restrictive-wins where the recipe lives (a
pathallow cannot loosen anexternal_directory: askboundary). - Do not introduce any
**example; if an adjacent sentence is edited, keep single-*idiom.
- In the
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.
- Replace or augment the bare
config/config.example.json- Add a second
external_directoryallow entry for a cache directory (e.g."~/.cargo/registry/*": "allow") alongside the existing"~/development/*": "allow", so the pattern-map idiom is visible.
- Add a second
schemas/permissions.schema.json- Update the
examplesarray entry to showexternal_directoryas a pattern map (matching the example config) instead of the bare"ask"string, and/or add a sentence to thepermissionmarkdownDescriptionnoting thatexternal_directoryaccepts a pattern map for allowing specific outside-CWD directories. - Keep schema
examplesandconfig/config.example.jsonconsistent with each other.
- Update the
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.jsonandschemas/permissions.schema.jsonstill parse and that the example validates against the schema if a validation script exists. - Confirm
docs/configuration.md,README.md,config.example.json, andschemaagree on theexternal_directorypattern-map form (the AGENTS.md alignment rule).
Invariants at risk
- Most-restrictive-wins composition.
The docs must not imply a
pathallow can loosen anexternal_directory: askboundary; the new wording reinforces the invariant rather than weakening it. This is a prose invariant — pinned by the existing composition tests intest/(no code change here, so they stay green).
Build Order
Docs-only; each step ends in a docs: commit.
-
docs:—configuration.md. Add the~/.cargo/registry/*recipe to theexternal_directorysection, 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). -
docs:—README.md. Describe theexternal_directorysurface clearly and add a small pattern-map allow example. Commit:docs(pi-permission-system): clarify external_directory surface in README (#413). -
docs:—config/config.example.jsonandschemas/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_directoryas the intended surface and explicitly state that apathallow cannot loosen anexternal_directoryboundary. - Risk: the user's underlying case is read-only caches and
piInfrastructureReadPathswould be lighter-weight. Mitigation: keepexternal_directoryas the primary recipe (operator's choice; covers all tools) and add at most a one-line cross-reference.
Open Questions
- Should
schemas/permissions.schema.jsonkeep a bare-stringexternal_directoryexample 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
piInfrastructureReadPathsfor read-only caches: include inconfiguration.mdonly, or omit to keep the recipe focused? Defer to the build step; lean toward a single sentence inconfiguration.md.