11 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 80 | 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 4–5 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
PermissionPrompterclass behind a mockable interface. - Reduce the "add a prompt parameter" surface from 4–5 files to 2 files: the
PromptPermissionDetailstype and thePermissionPrompterimplementation. - Eliminate
PermissionForwardingDeps.requestPermissionDecisionFromUias a standalone injected function — it becomes an internal detail of the prompter. - Preserve identical user-visible behavior and test ergonomics (
HandlerDepsstill exposes a mockablepromptPermissionmethod).
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-systemslash 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
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
/** Mockable contract exposed to handlers via HandlerDeps. */
export interface PermissionPrompterApi {
prompt(
ctx: ExtensionContext,
details: PromptPermissionDetails,
): Promise<PermissionPromptDecision>;
}
New class
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:
const forwardingDeps: PermissionForwardingDeps = { … };
// …
promptPermission: (ctx, details) =>
promptPermission(runtime, forwardingDeps, ctx, details),
With:
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 inpolling.tsbut 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
-
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) -
feat: Create
src/permission-prompter.tswithPermissionPrompterApi,PermissionPrompterDeps, andPermissionPrompterclass extracting logic fromruntime.ts::promptPermission(). Commit:feat: extract PermissionPrompter class (#80) -
feat: Wire
PermissionPrompterinsrc/index.ts, removepromptPermissionfree function fromruntime.ts, narrowPermissionForwardingDeps. Commit:feat: wire PermissionPrompter and remove runtime promptPermission (#80) -
test: Update
tests/unit/runtime.test.ts— remove now-deadpromptPermissiontests. Updatetests/unit/polling.test.tsmock to narrowedPermissionForwardingDeps. Commit:test: update runtime and polling tests for PermissionPrompter extraction (#80) -
docs: Add
docs/architecture/permission-prompter.mddescribing 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
processForwardedPermissionRequestsshould also move into the prompter class (deferred — it has its own polling lifecycle unrelated to single-prompt flow). - Whether
PermissionPrompterDepsshould include awriteDebugLogmethod for trace-level output (can be added later without signature changes to handlers).