Files
my-pi/pi-permission-system/docs/plans/archive/0053-home-directory-expansion.md

160 lines
11 KiB
Markdown

---
issue: 53
issue_title: "Support ~/$HOME expansion in permission config patterns"
---
# Support `~`/`$HOME` expansion in permission config patterns
## Problem Statement
Permission config patterns that reference home-relative paths (e.g., `external_directory` rules, skill globs) require fully expanded absolute paths today.
This makes configs non-portable across machines and users.
Users expect to write `~/development/pi/*` or `$HOME/development/pi/*` and have it match the expanded path at evaluation time.
## Goals
- Expand `~`, `~/...`, `$HOME`, and `$HOME/...` prefixes in rule patterns at normalization time.
- Keep the stored/displayed pattern as the user wrote it (the `Rule.pattern` field retains `~/...` for readability in logs and `matchedPattern`).
- Apply expansion only to the compiled/matching form, not to pattern identity.
- Add an `expandHomePath()` utility function.
- Update schema docs and example config to document the feature.
- Cover edge cases: Windows-style `~\`, bare `~`, `$HOME` alone, patterns without home prefix (no-op).
## Non-Goals
- Environment variable expansion beyond `$HOME` (e.g., `$USER`, `$XDG_CONFIG_HOME`) — out of scope, file a separate issue if needed.
- Expanding `~` in the **value** side of evaluation (tool input paths) — the external-directory handler already does this in `normalizePathForComparison()`.
- Changing the config merge semantics or adding new surfaces.
## Background
### Relevant modules
| Module | Role |
| --------------------------- | -------------------------------------------------------------------------------------------------------- |
| `src/normalize.ts` | `normalizeFlatConfig()` converts flat permission config into a `Ruleset`. |
| `src/rule.ts` | `evaluate()` calls `wildcardMatch(r.pattern, value)` — patterns must already be in final matchable form. |
| `src/wildcard-matcher.ts` | `wildcardMatch()` / `compileWildcardPattern()` compile `*`-glob patterns into regexes. |
| `src/external-directory.ts` | `normalizePathForComparison()` already expands `~` on the value side (tool input paths). |
| `src/input-normalizer.ts` | For `external_directory`, passes the raw `path` from tool input as the value to evaluate against. |
### Permission surface involved
Primarily `external_directory` and `special`, but expansion applies generically to any rule pattern that starts with `~` or `$HOME`.
In practice, path-bearing patterns appear in `external_directory` and potentially `bash` command patterns.
### How evaluation works today
1. Config patterns → `normalizeFlatConfig()``Rule[]` (pattern stored verbatim).
2. At check time: `evaluate(surface, value, rules)``wildcardMatch(rule.pattern, value)`.
3. The value for `external_directory` is the **raw path** from tool input (e.g., `/Users/chris/development/pi/file.ts`).
4. So `rule.pattern` must be an absolute path glob to match — `~/development/pi/*` would fail today.
### Design decision: where to expand
Expansion at **`wildcardMatch()` / `compileWildcardPattern()` time** is the cleanest approach:
- The `Rule.pattern` field retains the user-written form for display in logs and `matchedPattern`.
- The regex used for matching sees the expanded path.
- No changes needed to `normalizeFlatConfig()`, `evaluate()`, or any caller.
## Design Overview
### New utility: `expandHomePath()`
```typescript
// src/expand-home.ts
import { homedir } from "node:os";
import { join } from "node:path";
/**
* Expand `~` and `$HOME` prefixes in a pattern to the OS home directory.
* Returns the input unchanged if no home prefix is present.
*/
export function expandHomePath(pattern: string): string {
if (pattern === "~" || pattern === "$HOME") {
return homedir();
}
if (pattern.startsWith("~/") || pattern.startsWith("~\\")) {
return join(homedir(), pattern.slice(2));
}
if (pattern.startsWith("$HOME/") || pattern.startsWith("$HOME\\")) {
return join(homedir(), pattern.slice(6));
}
return pattern;
}
```
### Integration point: `compileWildcardPattern()`
Apply `expandHomePath()` to the pattern before splitting on `*` and building the regex.
The `pattern` field in the returned `CompiledWildcardPattern` retains the original (unexpanded) value.
```typescript
export function compileWildcardPattern<TState>(
pattern: string,
state: TState,
): CompiledWildcardPattern<TState> {
const expanded = expandHomePath(pattern);
const escaped = expanded
.split("*")
.map((part) => escapeRegExp(part))
.join(".*");
return {
pattern, // original for display
state,
regex: new RegExp(`^${escaped}$`, "s"),
};
}
```
The standalone `wildcardMatch()` function also calls `compileWildcardPattern`, so it inherits expansion automatically.
### Edge cases
- `~` alone → matches exactly `homedir()`.
- `$HOME` alone → matches exactly `homedir()`.
- `~/` → matches `homedir() + "/"` (trailing slash).
- Patterns not starting with `~` or `$HOME` → unchanged (no-op).
- Windows: `~\foo` is handled by the `startsWith("~\\")` branch.
## Module-Level Changes
| File | Change |
| ------------------------------------------ | ------------------------------------------------------------------------------------------ |
| `src/expand-home.ts` | **New**`expandHomePath()` utility. |
| `src/wildcard-matcher.ts` | Import `expandHomePath`; apply in `compileWildcardPattern()`. |
| `schemas/permissions.schema.json` | Add `markdownDescription` noting `~`/`$HOME` support in pattern keys. |
| `config/config.example.json` | Add example using `~/...` in `external_directory`. |
| `tests/expand-home.test.ts` | **New** — unit tests for `expandHomePath()`. |
| `tests/wildcard-matcher.test.ts` | Add tests for home-expanded patterns via `wildcardMatch()` and `compileWildcardPattern()`. |
| `tests/permission-manager-unified.test.ts` | Integration test: `external_directory` rule with `~/...` pattern matches expanded path. |
| `docs/architecture/target-architecture.md` | Note home expansion in wildcard-matcher description if applicable. |
## TDD Order
1. **test: add expandHomePath unit tests**`tests/expand-home.test.ts`: covers `~`, `~/path`, `$HOME`, `$HOME/path`, bare `~`, no-op patterns, Windows separator.
2. **feat: implement expandHomePath utility**`src/expand-home.ts`: pure function, green tests from step 1.
3. **test: add wildcard-matcher home expansion tests**`tests/wildcard-matcher.test.ts`: `wildcardMatch("~/dev/*", "/Users/chris/dev/foo")` returns true; `compileWildcardPattern("~/dev/*", …).pattern` retains `~/dev/*`.
4. **feat: integrate expandHomePath into compileWildcardPattern**`src/wildcard-matcher.ts`: import and apply expansion.
Green tests from step 3.
5. **test: integration test for external_directory with ~ pattern**`tests/permission-manager-unified.test.ts`: config with `"~/trusted/*": "allow"`, `checkPermission("external_directory", { path: "<homedir>/trusted/repo" })` returns allow.
6. **feat: green integration test (no code change expected)** — Step 4 already makes this pass; confirm and commit together with step 5 if trivial.
7. **docs: update schema and example config**`schemas/permissions.schema.json`: add note about `~`/`$HOME` expansion in pattern descriptions. `config/config.example.json`: add `~/...` example in `external_directory`.
## Risks and Mitigations
| Risk | Mitigation |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Could this silently weaken a permission? No — expansion only makes existing explicit patterns matchable. A pattern that previously failed to match (because `~` was not expanded) now correctly matches, which is the user's intent. No new implicit allows are introduced. | Confirm via integration test that only explicitly written `~`-prefixed rules gain match power. |
| `homedir()` returns different values across platforms/users | This is the intended behavior — portability is the goal. Tests mock `homedir()` to a known value. |
| Expanding `~` in non-path surfaces (e.g., bash command patterns like `~something`) | Unlikely to cause harm — bash commands starting with `~/` are legitimate path references. Patterns like `~username/` are not supported (documented as non-goal). |
| `$HOME` prefix conflicts with literal `$HOME` in a non-path pattern | Extremely unlikely in practice. Document that `$HOME` is expanded; users who need a literal `$HOME` prefix can avoid it. |
## Open Questions
- Should `$HOME` expansion also handle the case where the `HOME` env var differs from `os.homedir()`?
For now, use `os.homedir()` consistently (matches `normalizePathForComparison` behavior).
Revisit if a user reports a mismatch.