mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
185 lines
12 KiB
Markdown
185 lines
12 KiB
Markdown
---
|
||
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`.
|