mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user