import { join } from "node:path"; import { getAgentDir, type ExtensionAPI, type ExtensionContext, type ToolCallEvent, type ToolInfo, } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { buildPlannedAction } from "./action.js"; import { createReviewAudit, parseReviewAudit, REVIEW_ENTRY, type ReviewAudit, } from "./audit.js"; import { DenialCircuitBreaker } from "./circuit-breaker.js"; import { loadGlobalConfig, type ApproveForMeConfig, type ConfigResult, type PermissionMode, } from "./config.js"; import { buildReviewPrompt } from "./prompt.js"; import { reviewWithModel, type ReviewResult } from "./reviewer.js"; import { compactTranscript } from "./transcript.js"; import { isGenuineNonMutatingBuiltin } from "./tool-policy.js"; const STATUS_KEY = "approve-for-me"; const BLOCK_GUIDANCE = "Do not circumvent this decision or retry through an indirect tool. Use a materially safer alternative, or obtain explicit informed user authorization before retrying the exact action."; const PLANNED_ACTION_LIMIT_REASON = "The planned action exceeds the approximately 16,000-token review limit. Use a smaller command or split the action into smaller tool calls"; export interface ApproveForMeDependencies { loadConfig: (settingsPath: string) => Promise; review: typeof reviewWithModel; settingsPath: () => string; } const DEFAULT_DEPENDENCIES: ApproveForMeDependencies = { loadConfig: loadGlobalConfig, review: reviewWithModel, settingsPath: () => join(getAgentDir(), "settings.json"), }; function modeLabel(mode: PermissionMode): string { return mode === "approve-for-me" ? "Approve for me" : "Full access"; } function sourceCategory(tool: ToolInfo): ReviewAudit["source"] { return tool.sourceInfo.source === "builtin" ? "builtin" : "custom"; } function failureResult(reason: string): ReviewResult { return { status: "failed-closed", reason, attempts: 0 }; } function blockReason(result: Exclude, tripped: boolean): string { const detail = result.status === "denied" ? result.assessment.rationale : result.reason; const prefix = result.status === "denied" ? "Automatic approval review denied this tool call" : "Automatic approval review failed closed"; const breaker = tripped ? " The denial circuit breaker tripped and the active Pi run was aborted." : ""; return `${prefix}: ${detail}.${breaker}\n${BLOCK_GUIDANCE}`; } export function createApproveForMeExtension( overrides: Partial = {}, ): (pi: ExtensionAPI) => void { const deps = { ...DEFAULT_DEPENDENCIES, ...overrides }; return (pi: ExtensionAPI): void => { let config: ApproveForMeConfig | undefined; let configLoadError: string | undefined; let mode: PermissionMode = "approve-for-me"; let inFlight = 0; const breaker = new DenialCircuitBreaker(); const updateStatus = (ctx: ExtensionContext): void => { if (inFlight > 0) { ctx.ui.setStatus(STATUS_KEY, `Approve for me: Reviewing ${inFlight} request${inFlight === 1 ? "" : "s"}`); return; } const warning = mode === "approve-for-me" && configLoadError !== undefined ? " ⚠ configuration" : ""; ctx.ui.setStatus(STATUS_KEY, `Approve for me: ${modeLabel(mode)}${warning}`); }; pi.registerEntryRenderer(REVIEW_ENTRY, (entry, _options, theme) => { const data = parseReviewAudit(entry.data); if (data === undefined) return undefined; const icon = data.status === "allowed" ? "✓" : data.status === "denied" ? "✗" : "!"; const assessment = data.riskLevel === undefined ? "" : ` · ${data.riskLevel}/${data.userAuthorization}`; return new Text( `${theme.fg(data.status === "allowed" ? "success" : "warning", `[approval ${icon}]`)} ${data.tool}${assessment}`, 0, 0, ); }); pi.on("session_start", async (_event, ctx) => { const loaded = await deps.loadConfig(deps.settingsPath()); if (loaded.ok) { config = loaded.config; configLoadError = undefined; mode = loaded.config.mode; } else { config = undefined; configLoadError = loaded.error; mode = "approve-for-me"; } breaker.resetTurn(); inFlight = 0; updateStatus(ctx); if (configLoadError !== undefined && ctx.hasUI) { ctx.ui.notify(`Approve for me: ${configLoadError}. Reviewed calls will fail closed.`, "error"); } }); pi.on("session_shutdown", (_event, ctx) => { ctx.ui.setStatus(STATUS_KEY, undefined); }); pi.on("agent_start", () => { breaker.resetTurn(); }); pi.on("tool_call", async (event: ToolCallEvent, ctx) => { if (mode === "full-access") return undefined; const tools = pi.getAllTools(); // Bash is never allowlisted in review mode, even if an invalid config is injected by a caller. if (event.toolName !== "bash" && config?.alwaysAllowTools.includes(event.toolName)) return undefined; const tool = tools.find((candidate) => candidate.name === event.toolName)!; if (isGenuineNonMutatingBuiltin(tool)) return undefined; inFlight += 1; updateStatus(ctx); let result: ReviewResult; try { if (config === undefined) { result = failureResult(configLoadError ?? "approveForMe reviewer configuration is missing"); } else { const action = buildPlannedAction( { toolName: event.toolName, input: event.input }, ctx.cwd, tool.sourceInfo, ); if (action.truncated || action.tooLarge) { result = failureResult(PLANNED_ACTION_LIMIT_REASON); } else { const transcript = compactTranscript( ctx.sessionManager.buildContextEntries(), ctx.sessionManager.getBranch(), ); result = await deps.review(config.reviewer, buildReviewPrompt(transcript, action), ctx.modelRegistry, ctx.signal); } } } catch { result = failureResult("The approval review request could not be constructed"); } finally { inFlight -= 1; updateStatus(ctx); } pi.appendEntry(REVIEW_ENTRY, createReviewAudit(event.toolName, sourceCategory(tool), result)); if (result.status === "allowed") { breaker.recordNonDenial(); return undefined; } let tripped = false; if (result.status === "denied") { tripped = breaker.recordExplicitDenial(); if (tripped) { ctx.abort(); ctx.ui.notify("Approval denial circuit breaker tripped; the active Pi run was aborted.", "error"); } } else { breaker.recordNonDenial(); } return { block: true, reason: blockReason(result, tripped) }; }); }; } export default createApproveForMeExtension();