feat: vendor permission system source

This commit is contained in:
云服务部-叶林立
2026-08-19 14:35:19 +08:00
parent 198584daf8
commit 410c50a3e5
809 changed files with 157793 additions and 139 deletions
@@ -0,0 +1,171 @@
---
issue: 80
issue_title: "Extract PermissionPrompter class to unify prompt/log/forwarding chain"
---
# Extract PermissionPrompter class
## Problem Statement
Adding a new parameter to the permission prompt flow (e.g. `sessionLabel` from #51) currently requires coordinated signature changes across 45 files: `HandlerDeps` (types), `runtime.ts` (yolo-mode + logging), `polling.ts` (UI vs. forwarding branch), and `index.ts` (wiring).
Each layer exists for a valid reason, but the threading cost is disproportionate to the semantic change.
## Goals
- Encapsulate yolo-mode auto-approval, review-log writes, and UI-vs-forwarding branching in a single `PermissionPrompter` class behind a mockable interface.
- Reduce the "add a prompt parameter" surface from 45 files to 2 files: the `PromptPermissionDetails` type and the `PermissionPrompter` implementation.
- Eliminate `PermissionForwardingDeps.requestPermissionDecisionFromUi` as a standalone injected function — it becomes an internal detail of the prompter.
- Preserve identical user-visible behavior and test ergonomics (`HandlerDeps` still exposes a mockable `promptPermission` method).
## Non-Goals
- Changing user-facing permission dialog options or wording.
- Altering policy resolution or evaluation logic.
- Modifying the forwarded-permission file protocol.
- Renaming the `/permission-system` slash command.
## Background
### Permission surfaces involved
This is a cross-cutting refactor that touches the **prompt delivery** mechanism used by all permission surfaces (tools, bash, mcp, skills, special, external_directory) — but does not change any surface's evaluation logic.
### Relevant modules
| File | Role today |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `src/handlers/types.ts` | Defines `HandlerDeps.promptPermission` and `PromptPermissionDetails` |
| `src/runtime.ts` | `promptPermission()` free function: yolo check → review log → `confirmPermission()` |
| `src/forwarded-permissions/polling.ts` | `confirmPermission()`: UI-present branch vs. subagent forwarding; `PermissionForwardingDeps` interface |
| `src/index.ts` | Wires `forwardingDeps` and binds `promptPermission` into `HandlerDeps` |
| `src/permission-dialog.ts` | `requestPermissionDecisionFromUi()` — shows the actual select dialog |
| `src/yolo-mode.ts` | `shouldAutoApprovePermissionState()` — yolo-mode predicate |
### Current call chain
```text
handler → deps.promptPermission(ctx, details)
→ runtime.ts::promptPermission(runtime, forwardingDeps, ctx, details)
→ yolo check → reviewPermissionDecision (waiting)
→ confirmPermission(ctx, message, forwardingDeps, options)
→ ctx.hasUI? requestPermissionDecisionFromUi(…) : forward
→ reviewPermissionDecision (approved|denied)
```
## Design Overview
### New interface
```typescript
/** Mockable contract exposed to handlers via HandlerDeps. */
export interface PermissionPrompterApi {
prompt(
ctx: ExtensionContext,
details: PromptPermissionDetails,
): Promise<PermissionPromptDecision>;
}
```
### New class
```typescript
export class PermissionPrompter implements PermissionPrompterApi {
constructor(private readonly deps: PermissionPrompterDeps) {}
async prompt(
ctx: ExtensionContext,
details: PromptPermissionDetails,
): Promise<PermissionPromptDecision> { /* … */ }
}
export interface PermissionPrompterDeps {
/** Read current config for yolo-mode check. */
getConfig(): PermissionSystemExtensionConfig;
/** Write review log entries. */
writeReviewLog(event: string, details: Record<string, unknown>): void;
/** Subagent sessions dir for forwarding context check. */
subagentSessionsDir: string;
/** Forwarding dir for file-based permission requests. */
forwardingDir: string;
/** Show the actual permission dialog UI. */
requestPermissionDecisionFromUi(
ui: ExtensionContext["ui"],
title: string,
message: string,
options?: RequestPermissionOptions,
): Promise<PermissionPromptDecision>;
}
```
### Wiring change
In `src/index.ts`, replace:
```typescript
const forwardingDeps: PermissionForwardingDeps = { };
// …
promptPermission: (ctx, details) =>
promptPermission(runtime, forwardingDeps, ctx, details),
```
With:
```typescript
const prompter = new PermissionPrompter({ });
// …
promptPermission: (ctx, details) => prompter.prompt(ctx, details),
```
The `PermissionForwardingDeps` interface narrows to only what `processForwardedPermissionRequests` and `waitForForwardedPermissionApproval` need (forwarding-only concerns); the prompter owns the union of yolo + logging + confirm.
### Edge cases
- **Yolo mode**: auto-approval is checked inside `PermissionPrompter.prompt()` exactly as today.
- **Subagent forwarding**: `confirmPermission()` stays in `polling.ts` but is called by the prompter, not by the runtime free function.
- **Review log ordering**: waiting → (auto_approved | decision) is preserved.
## Module-Level Changes
| File | Change |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/permission-prompter.ts` (new) | `PermissionPrompterApi` interface, `PermissionPrompterDeps` interface, `PermissionPrompter` class |
| `src/runtime.ts` | Remove `promptPermission()` free function and `reviewPermissionDecision()` helper (moved into class) |
| `src/forwarded-permissions/polling.ts` | Narrow `PermissionForwardingDeps` — remove `requestPermissionDecisionFromUi` (kept only for `processForwardedPermissionRequests`); export `confirmPermission` as-is for the prompter to call |
| `src/index.ts` | Instantiate `PermissionPrompter`, pass it to `HandlerDeps`; simplify `forwardingDeps` to forwarding-only subset |
| `src/handlers/types.ts` | No signature change — `promptPermission` remains `(ctx, details) => Promise<PermissionPromptDecision>` |
| `tests/unit/runtime.test.ts` | Remove tests for `promptPermission` free function |
| `tests/unit/permission-prompter.test.ts` (new) | Unit tests for `PermissionPrompter` class |
| `tests/unit/polling.test.ts` | Adjust `PermissionForwardingDeps` mock to match narrowed interface |
| `docs/architecture/permission-prompter.md` (new) | Short architectural note explaining the class's responsibilities |
## TDD Order
1. **test:** Add unit tests for `PermissionPrompter.prompt()` covering yolo-mode auto-approve, UI-present approval, UI-present denial, and subagent forwarding path.
Commit: `test: add PermissionPrompter unit tests (#80)`
2. **feat:** Create `src/permission-prompter.ts` with `PermissionPrompterApi`, `PermissionPrompterDeps`, and `PermissionPrompter` class extracting logic from `runtime.ts::promptPermission()`.
Commit: `feat: extract PermissionPrompter class (#80)`
3. **feat:** Wire `PermissionPrompter` in `src/index.ts`, remove `promptPermission` free function from `runtime.ts`, narrow `PermissionForwardingDeps`.
Commit: `feat: wire PermissionPrompter and remove runtime promptPermission (#80)`
4. **test:** Update `tests/unit/runtime.test.ts` — remove now-dead `promptPermission` tests.
Update `tests/unit/polling.test.ts` mock to narrowed `PermissionForwardingDeps`.
Commit: `test: update runtime and polling tests for PermissionPrompter extraction (#80)`
5. **docs:** Add `docs/architecture/permission-prompter.md` describing responsibility boundaries.
Commit: `docs: add permission-prompter architecture note (#80)`
## Risks and Mitigations
| Risk | Mitigation |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Could this silently weaken a permission? | No — the class preserves identical decision logic (yolo check → log → confirm). Integration tests for each permission surface remain unchanged. |
| Forwarding tests break due to narrowed deps | Step 4 explicitly updates the mock interface; CI catches regressions. |
| Circular import between prompter and polling | `PermissionPrompter` imports `confirmPermission` from polling; polling does not import the prompter. One-way dependency, no cycle. |
| Review log entry format drift | The class reuses the existing `reviewPermissionDecision` helper (moved into the class as a private method), preserving exact field names. |
## Open Questions
- Whether `processForwardedPermissionRequests` should also move into the prompter class (deferred — it has its own polling lifecycle unrelated to single-prompt flow).
- Whether `PermissionPrompterDeps` should include a `writeDebugLog` method for trace-level output (can be added later without signature changes to handlers).