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,210 @@
|
||||
---
|
||||
issue: 45
|
||||
issue_title: "Add \"approve for this session\" option to permission prompts"
|
||||
---
|
||||
|
||||
# Session-scoped approvals for permission prompts
|
||||
|
||||
## Problem Statement
|
||||
|
||||
When the `external_directory` gate is set to `ask`, every file access outside CWD prompts the user individually.
|
||||
Investigating a sibling project can trigger dozens of identical approval dialogs in a single session.
|
||||
There is no way to say "yes, allow this class of access for the rest of the session" without changing the on-disk policy to `allow`.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add a third dialog option ("Allow for session") alongside "Yes" and "No" in the permission confirmation UI.
|
||||
- Introduce an in-memory `SessionApprovalCache` that records directory-prefix approvals.
|
||||
- Before prompting, check the cache; if a matching session approval exists, skip the dialog and log `resolution: "session_approved"`.
|
||||
- Scope session approvals to the **external-directory** surface only (both file-tool and bash variants).
|
||||
- Clear the cache on `session_shutdown`.
|
||||
- Do **not** persist approvals to disk — they are ephemeral by design.
|
||||
- Record session-approved decisions in the review log with a distinct resolution value.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Extending session approvals to tool/bash-pattern/MCP/skill surfaces (future work noted in the issue).
|
||||
- Per-agent scoping of session approvals (use the same flat cache regardless of active agent).
|
||||
- Persisting approvals across sessions — that is what policy config is for.
|
||||
- Changing the on-disk schema, example config, or `defaultPolicy` values.
|
||||
- Changing the `/permission-system` slash command.
|
||||
|
||||
## Background
|
||||
|
||||
### Permission surfaces involved
|
||||
|
||||
`special.external_directory` — evaluated before normal tool/bash checks for path-bearing file tools and bash commands referencing external paths.
|
||||
|
||||
### Existing modules
|
||||
|
||||
| Module | Role |
|
||||
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `src/permission-dialog.ts` | `requestPermissionDecisionFromUi()` presents Yes/No/No-with-reason via `PermissionDecisionUi.select()`. |
|
||||
| `src/permission-gate.ts` | `applyPermissionGate()` — pure deny/ask/allow branching. Receives a `promptForApproval` callback. |
|
||||
| `src/external-directory.ts` | Path normalization, outside-CWD detection, message formatting. |
|
||||
| `src/index.ts` | Wires the gate for file-tool and bash external-directory checks. Calls `promptPermission()` which delegates to `requestPermissionDecisionFromUi()`. |
|
||||
| `src/types.ts` | `PermissionPromptDecision`, `PermissionDecisionState` types. |
|
||||
|
||||
### Flow today
|
||||
|
||||
1. `tool_call` handler detects external path.
|
||||
2. Calls `applyPermissionGate({ state: extCheck.state, ... })`.
|
||||
3. If `state === "ask"`, gate calls `promptForApproval()` → `promptPermission()` → `requestPermissionDecisionFromUi()`.
|
||||
4. User sees Yes / No / No-with-reason.
|
||||
5. Decision is logged and returned.
|
||||
|
||||
### Flow after this change
|
||||
|
||||
1. `tool_call` handler detects external path.
|
||||
2. **New**: before calling the gate, check `SessionApprovalCache` for a matching directory prefix.
|
||||
If found → log `session_approved`, return `{ action: "allow" }` without prompting.
|
||||
3. If not cached → call `applyPermissionGate()` as today, but with the new three-option dialog.
|
||||
4. If user selects "Allow for session" → record the directory prefix in the cache, return approved.
|
||||
5. `session_shutdown` clears the cache.
|
||||
|
||||
## Design Overview
|
||||
|
||||
### `SessionApprovalCache`
|
||||
|
||||
A small class with a `Map<string, Set<string>>` keyed by surface type (initially only `"external_directory"`).
|
||||
Values are normalized directory prefixes.
|
||||
|
||||
```typescript
|
||||
export class SessionApprovalCache {
|
||||
private approvals = new Map<string, Set<string>>();
|
||||
|
||||
approve(surface: string, prefix: string): void;
|
||||
has(surface: string, path: string): boolean; // prefix match
|
||||
clear(): void;
|
||||
}
|
||||
```
|
||||
|
||||
`has()` checks whether any stored prefix for the surface is an ancestor of (or equal to) the given path using `isPathWithinDirectory()` from `external-directory.ts`.
|
||||
|
||||
### Extended dialog
|
||||
|
||||
`PermissionDecisionState` gains a fourth value: `"approved_for_session"`.
|
||||
`PermissionPromptDecision` already carries `state`; callers inspect it to decide whether to cache.
|
||||
|
||||
The dialog options become:
|
||||
|
||||
```text
|
||||
Yes | Yes, for this session | No | No, provide reason
|
||||
```
|
||||
|
||||
`requestPermissionDecisionFromUi()` returns `{ approved: true, state: "approved_for_session" }` for the session option.
|
||||
|
||||
### Deriving the approval prefix
|
||||
|
||||
For file-tool external-directory checks, the prefix is the **parent directory** of the target path (so approving access to `~/other-project/src/foo.ts` covers `~/other-project/src/`).
|
||||
For bash external-directory checks, each extracted external path's parent directory is recorded.
|
||||
|
||||
A helper `deriveApprovalPrefix(normalizedPath: string): string` returns `dirname(normalizedPath)` with a trailing separator, ensuring prefix matching works correctly.
|
||||
|
||||
### Review log
|
||||
|
||||
When a request is satisfied from the cache:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"event": "permission_request.session_approved",
|
||||
"resolution": "session_approved",
|
||||
"sessionApprovalPrefix": "/Users/.../other-project/src/"
|
||||
// ... standard log context
|
||||
}
|
||||
```
|
||||
|
||||
### Integration in `src/index.ts`
|
||||
|
||||
- Instantiate `SessionApprovalCache` alongside `permissionManager` at the top of `piPermissionSystemExtension()`.
|
||||
- Clear it in `session_shutdown`.
|
||||
- In both external-directory gate sites (file-tool and bash), insert a cache check **before** `applyPermissionGate()`.
|
||||
- After a successful prompt where `decision.state === "approved_for_session"`, call `cache.approve(...)`.
|
||||
|
||||
## Module-Level Changes
|
||||
|
||||
### New file: `src/session-approval-cache.ts`
|
||||
|
||||
- `SessionApprovalCache` class.
|
||||
- `deriveApprovalPrefix(normalizedPath: string): string` helper.
|
||||
- Exports only pure logic; no IO.
|
||||
|
||||
### Modified: `src/permission-dialog.ts`
|
||||
|
||||
- Add `"approved_for_session"` to `PermissionDecisionState`.
|
||||
- Add a fourth option constant `APPROVE_FOR_SESSION_OPTION = "Yes, for this session"`.
|
||||
- Update `PERMISSION_DECISION_OPTIONS` array.
|
||||
- Handle the new option in `requestPermissionDecisionFromUi()`.
|
||||
- Update `isPermissionDecisionState()` guard.
|
||||
|
||||
### Modified: `src/types.ts`
|
||||
|
||||
No changes needed — `PermissionDecisionState` lives in `permission-dialog.ts`.
|
||||
|
||||
### Modified: `src/index.ts`
|
||||
|
||||
- Import `SessionApprovalCache` and `deriveApprovalPrefix`.
|
||||
- Instantiate cache in `piPermissionSystemExtension()`.
|
||||
- Clear cache in `session_shutdown` handler.
|
||||
- File-tool external-directory block: add cache-check before gate, cache-write after session-approved decision.
|
||||
- Bash external-directory block: same pattern, iterating over each extracted external path.
|
||||
|
||||
### New file: `tests/session-approval-cache.test.ts`
|
||||
|
||||
- Unit tests for `SessionApprovalCache` (approve, has, prefix matching, clear, cross-surface isolation).
|
||||
|
||||
### Modified: `tests/permission-dialog.test.ts`
|
||||
|
||||
- Test the new "Yes, for this session" option returns `approved_for_session`.
|
||||
- Test `isPermissionDecisionState` includes the new value.
|
||||
|
||||
### Modified: `tests/index.test.ts` (or integration-level test)
|
||||
|
||||
- Test that a session-approved external-directory decision skips subsequent prompts for paths under the same prefix.
|
||||
- Test that `session_shutdown` clears session approvals.
|
||||
- Test that session approvals do not leak across surfaces.
|
||||
|
||||
## TDD Order
|
||||
|
||||
1. **Red → Green**: `SessionApprovalCache` — approve, has (prefix match), clear, surface isolation.
|
||||
`test: cover SessionApprovalCache approve/has/clear`
|
||||
|
||||
2. **Red → Green**: `deriveApprovalPrefix` — returns parent dir with trailing separator, handles root paths.
|
||||
`test: cover deriveApprovalPrefix edge cases`
|
||||
|
||||
3. **Feat**: implement `SessionApprovalCache` and `deriveApprovalPrefix` in `src/session-approval-cache.ts`.
|
||||
`feat: add SessionApprovalCache for ephemeral session approvals`
|
||||
|
||||
4. **Red → Green**: permission dialog returns `approved_for_session` for session option.
|
||||
`test: cover "Yes, for this session" dialog option`
|
||||
|
||||
5. **Feat**: extend `requestPermissionDecisionFromUi()` with the session option.
|
||||
`feat: add "approve for session" option to permission dialog`
|
||||
|
||||
6. **Red → Green**: integration — file-tool external-directory cache check skips prompt; bash variant likewise; shutdown clears cache.
|
||||
`test: cover session-approved external-directory flow`
|
||||
|
||||
7. **Feat**: wire `SessionApprovalCache` into `src/index.ts` external-directory gates.
|
||||
`feat: wire session approvals into external-directory gates`
|
||||
|
||||
8. **Docs**: update README permission-dialog section if it documents the Yes/No options.
|
||||
`docs: document session-scoped approval option`
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Session approval silently weakens a permission by covering more paths than intended. | Prefix is derived from `dirname()` of the specific path, not the top-level external directory. Approving `~/other/src/foo.ts` covers `~/other/src/` but not `~/other/`. Users must approve broader prefixes explicitly via repeated prompts or policy config. |
|
||||
| `approved_for_session` state breaks callers that only expect three states. | `isPermissionDecisionState()` is updated in the same commit. Only `src/index.ts` inspects `decision.state` for caching; all other callers check `decision.approved` (boolean). |
|
||||
| Cache grows without bound during long sessions. | External-directory prefixes are short strings; even hundreds of approvals are negligible. No eviction needed. |
|
||||
| Bash external-directory extracts multiple paths — unclear which to cache. | Cache each extracted path's parent individually. This is consistent: each path that was flagged gets its prefix recorded. |
|
||||
| Yolo mode interaction — session approval is redundant when yolo auto-approves. | No conflict: yolo mode short-circuits before the dialog is shown, so the cache is never consulted. No special handling needed. |
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the dialog show the resolved prefix being approved (e.g., "Allow all access to ~/other-project/src/ for this session")?
|
||||
Leaning yes for transparency, but can be deferred to a follow-up polish pass.
|
||||
- Should the session approval cover the exact directory of the path or its parent?
|
||||
Current design uses `dirname()` (parent of the file).
|
||||
If the user is accessing `~/other-project/README.md`, the prefix is `~/other-project/` which seems right.
|
||||
For directory-bearing tools like `find` and `ls` where the path *is* a directory, using the path itself as the prefix may be more appropriate — worth validating in tests.
|
||||
Reference in New Issue
Block a user