12 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 48 | Auto-allow reads from Pi package and agent directories in external_directory checks |
Auto-allow reads from Pi infrastructure directories
Problem Statement
When external_directory is set to ask or deny, the agent is prompted (or blocked) when reading skill files, prompt templates, and other resources from Pi package directories (e.g. /opt/homebrew/lib/node_modules/pi-ask-user/skills/ask-user/SKILL.md).
These are read-only infrastructure paths — the agent should be able to read them without prompting.
Goals
- Auto-allow reads (tools:
read,find,grep,ls) from Pi infrastructure directories without triggering external-directory prompts. - Discover infrastructure paths reliably across all package managers (npm, pnpm, bun, Homebrew) using self-discovery from
import.meta.url. - Cover all four path categories: agent config dir, project-local Pi packages, git-cloned global packages, and the global npm root.
- Provide an optional config field for users to add additional trusted read paths.
- Continue enforcing external-directory checks for writes (
write,edit) to infrastructure paths. - Continue enforcing external-directory checks for reads from non-infrastructure external paths.
Non-Goals
- Upstream API request to Pi for exposing package paths (deferred — file separately if needed).
- Auto-allowing writes to infrastructure directories (explicitly out of scope).
- Changing the bash external-directory gate for infrastructure paths (deferred to a follow-up; bash commands that reference infrastructure paths are rare and complex to classify as read-only).
Background
Relevant modules
src/external-directory.ts— containsisPathOutsideWorkingDirectory(),isSafeSystemPath(),SAFE_SYSTEM_PATHS, path normalization, and the tree-sitter bash path extractor.src/handlers/tool-call.ts— the file-tool external-directory gate (lines ~160–250) and bash external-directory gate (lines ~252–350). Both callisPathOutsideWorkingDirectory()and then check permissions.src/runtime.ts—ExtensionRuntimeholdsagentDirand is constructed once at startup.src/extension-config.ts— config loading and validation.src/types.ts— TypeScript types for config.
Permission surface
external_directory (under special).
Existing precedent
Issue #44 added SAFE_SYSTEM_PATHS — a static set of OS device paths (/dev/null, /dev/stdin, etc.) that bypass the external-directory check entirely.
The infrastructure paths here are similar in spirit (always safe to read) but differ in that they are environment-dependent, not constant.
Design Overview
Infrastructure path discovery
Build a set of "Pi infrastructure directories" at extension startup (inside createExtensionRuntime):
- Agent config directory —
getAgentDir()(already available asruntime.agentDir). - Project-local Pi packages —
<cwd>/.pi/npm/and<cwd>/.pi/git/(derived fromctx.cwdat check time, not startup). - Git-cloned global packages —
<agentDir>/git/. - Global npm root — discovered via self-discovery: walk up from
import.meta.url(this extension's own install path) to find the enclosingnode_modulesdirectory. This works regardless of package manager since the extension itself is installed in the global npm root.
/**
* Discover the global node_modules root by walking up from this file's location.
* Works for npm, pnpm, bun, Homebrew — any install method.
*/
export function discoverGlobalNodeModulesRoot(): string | null {
const thisFile = fileURLToPath(import.meta.url);
let dir = dirname(thisFile);
while (dir !== dirname(dir)) {
if (basename(dir) === "node_modules") {
return dir;
}
dir = dirname(dir);
}
return null;
}
Read-only enforcement
Only auto-allow for read-only file tools. Define a set of read-only path-bearing tools:
export const READ_ONLY_PATH_BEARING_TOOLS: ReadonlySet<string> = new Set([
"read", "find", "grep", "ls",
]);
Check order
In the file-tool external-directory gate (tool-call.ts), after confirming a path is outside CWD and before calling checkPermission:
- If the tool is in
READ_ONLY_PATH_BEARING_TOOLSAND the normalized path is within any Pi infrastructure directory → skip the external-directory gate (log to review log aspermission_request.infrastructure_auto_allowed). - Otherwise → proceed with existing permission check.
Config override
Add an optional piInfrastructureReadPaths field to the extension config:
interface PermissionSystemExtensionConfig {
// ... existing fields ...
/** Additional directories to treat as Pi infrastructure for read auto-allow. */
piInfrastructureReadPaths?: string[];
}
These are merged with the auto-discovered paths.
Paths support ~ expansion but not globs (they are directory prefixes, not patterns).
Data flow
startup:
import.meta.url → walk up → globalNodeModulesRoot
getAgentDir() → agentDir, agentDir/git/
config.piInfrastructureReadPaths → user overrides
→ Set<string> of infrastructure directory prefixes (stored on ExtensionRuntime)
per tool-call:
cwd → <cwd>/.pi/npm/, <cwd>/.pi/git/ (computed fresh each call)
normalizedPath + toolName → isPiInfrastructureRead() → boolean
Edge cases
import.meta.urlnot resolvable (e.g. bundled/eval context) →discoverGlobalNodeModulesRoot()returnsnull, feature degrades gracefully (only agentDir and project paths are known).- Symlinked installs (Homebrew) →
realpathnot needed; the normalized path from tool input will match the symlink target sinceresolve()is already used. - pnpm virtual store (
.pnpm/) → thenode_moduleswalk still finds the rootnode_modulesdirectory. - Writes to infrastructure paths → NOT auto-allowed; the check only applies when tool is in
READ_ONLY_PATH_BEARING_TOOLS.
Module-Level Changes
src/external-directory.ts
- Add
READ_ONLY_PATH_BEARING_TOOLSset. - Add
discoverGlobalNodeModulesRoot()function. - Add
isPiInfrastructureRead(toolName, normalizedPath, infrastructureDirs, cwd)— pure function that returnstrueif the tool is read-only AND the path is within any infrastructure directory.
src/runtime.ts
- Add
readonly piInfrastructureDirs: string[]toExtensionRuntime. - Compute at construction:
[agentDir, join(agentDir, "git"), globalNodeModulesRoot, ...config.piInfrastructureReadPaths](filtered for non-null).
src/handlers/tool-call.ts
- Before the external-directory permission check, call
isPiInfrastructureRead(). - If it returns
true, log and skip the gate. - Same pattern for bash external-directory gate is deferred (non-goal).
src/extension-config.ts
- Add
piInfrastructureReadPathsto config loading/validation.
src/types.ts
- Add
piInfrastructureReadPaths?: string[]toPermissionSystemExtensionConfig.
schemas/permissions.schema.json
- Add
piInfrastructureReadPathsproperty (array of strings, optional).
config/config.example.json
- Add commented example showing
piInfrastructureReadPaths.
docs/architecture/target-architecture.md
- Update external-directory section to mention infrastructure auto-allow.
tests/
- New file:
tests/pi-infrastructure-read.test.ts— unit tests fordiscoverGlobalNodeModulesRoot(),isPiInfrastructureRead(). - Update:
tests/external-directory.test.ts— integration tests for the gate bypass in tool-call flow.
TDD Order
-
test: Add unit tests for
discoverGlobalNodeModulesRoot()— mockimport.meta.url, verify walk-up logic, null fallback. Commit:test: cover discoverGlobalNodeModulesRoot path walk -
feat: Implement
discoverGlobalNodeModulesRoot()insrc/external-directory.ts. Commit:feat: add discoverGlobalNodeModulesRoot self-discovery -
test: Add unit tests for
isPiInfrastructureRead()— read tool + infra path → true, write tool + infra path → false, read tool + non-infra path → false, project-local.pi/npm/and.pi/git/paths. Commit:test: cover isPiInfrastructureRead pure function -
feat: Implement
READ_ONLY_PATH_BEARING_TOOLSandisPiInfrastructureRead()insrc/external-directory.ts. Commit:feat: add isPiInfrastructureRead check for infrastructure directories -
test: Add tests for runtime construction — verify
piInfrastructureDirsis populated from agentDir, globalNodeModulesRoot, and config overrides. Commit:test: cover piInfrastructureDirs computation in runtime -
feat: Add
piInfrastructureDirstoExtensionRuntime, compute at construction. Updatesrc/types.tswithpiInfrastructureReadPathsconfig field. Updatesrc/extension-config.tsto load/validate the new field. Commit:feat: compute piInfrastructureDirs at runtime construction -
test: Add integration test for tool-call handler — read tool targeting infra path skips gate, write tool targeting same path does not skip. Commit:
test: cover infrastructure read bypass in tool-call handler -
feat: Wire
isPiInfrastructureRead()into the file-tool external-directory gate insrc/handlers/tool-call.ts. Commit:feat: bypass external_directory gate for Pi infrastructure reads -
docs: Update schema, example config, and architecture docs. Commit:
docs: document piInfrastructureReadPaths config and infrastructure auto-allow
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Over-broad node_modules match allows reads from unrelated packages | The check requires the path to be within a Pi infrastructure directory, not arbitrary node_modules paths. The discovered root is the same one Pi itself uses. |
| Could silently weaken external_directory permission for writes | The check is gated on READ_ONLY_PATH_BEARING_TOOLS — writes are never auto-allowed. Explicit test coverage for this case. |
import.meta.url walk finds wrong node_modules in nested installs |
Walk upward from our own file; the first node_modules ancestor is necessarily the one containing us. Nested node_modules deeper in the tree won't be ancestors. |
Config piInfrastructureReadPaths used to bypass security for arbitrary dirs |
Document clearly that these are read-only auto-allow paths. The field name includes "Read" to signal intent. Review log entry makes bypasses visible. |
| Symlinked paths don't match resolved paths | Both sides use normalizePathForComparison() which calls resolve() — symlinks are handled consistently with existing external-directory logic. |
Open Questions
- Should the bash external-directory gate also auto-allow infrastructure reads?
Deferred — bash commands are harder to classify as read-only (e.g.
cat /opt/.../SKILL.mdis a read, but detecting "read-only bash commands" reliably is complex). Can be added in a follow-up. - Should
piInfrastructureReadPathssupport glob patterns or only directory prefixes? Starting with directory prefixes (simpler, consistent withisPathWithinDirectory). Globs can be added later if needed.