export type PermissionDecisionState = | "approved" | "approved_for_session" | "denied" | "denied_with_reason"; export type PermissionPromptDecision = { approved: boolean; state: PermissionDecisionState; denialReason?: string; /** * True when the decision was made automatically by yolo mode rather than * by an interactive user prompt. Used by handlers to emit "auto_approved" * rather than "user_approved" in the permissions:decision broadcast. */ autoApproved?: true; }; export interface PermissionDecisionUi { select(title: string, options: string[]): Promise; input(title: string, placeholder?: string): Promise; } const APPROVE_OPTION = "Allow once"; const APPROVE_FOR_SESSION_OPTION = "Allow for this session"; const DENY_OPTION = "Deny"; const DENY_WITH_REASON_OPTION = "Deny with reason"; const LEGACY_APPROVE_OPTION = "Yes"; const LEGACY_APPROVE_FOR_SESSION_OPTION = "Yes, for this session"; const LEGACY_DENY_WITH_REASON_OPTION = "No, provide reason"; const PERMISSION_DECISION_OPTIONS = [ APPROVE_OPTION, APPROVE_FOR_SESSION_OPTION, DENY_OPTION, DENY_WITH_REASON_OPTION, ] as const; export function normalizePermissionDenialReason( value: unknown, ): string | undefined { if (typeof value !== "string") { return undefined; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } export function createDeniedPermissionDecision( denialReason?: string, ): PermissionPromptDecision { const normalizedReason = normalizePermissionDenialReason(denialReason); return normalizedReason ? { approved: false, state: "denied_with_reason", denialReason: normalizedReason, } : { approved: false, state: "denied", }; } export function isPermissionDecisionState( value: unknown, ): value is PermissionDecisionState { return ( value === "approved" || value === "approved_for_session" || value === "denied" || value === "denied_with_reason" ); } export interface RequestPermissionOptions { /** Override the "for this session" option label (e.g. to show the suggested pattern). */ sessionLabel?: string; } export async function requestPermissionDecisionFromUi( ui: PermissionDecisionUi, title: string, message: string, options?: RequestPermissionOptions, ): Promise { const sessionOption = options?.sessionLabel ?? APPROVE_FOR_SESSION_OPTION; const decisionOptions = [ APPROVE_OPTION, sessionOption, DENY_OPTION, DENY_WITH_REASON_OPTION, ] as const; const selected = await ui.select(`${title}\n${message}`, [ ...decisionOptions, ]); if (selected === APPROVE_OPTION || selected === LEGACY_APPROVE_OPTION) { return { approved: true, state: "approved", }; } if (selected === sessionOption || selected === LEGACY_APPROVE_FOR_SESSION_OPTION) { return { approved: true, state: "approved_for_session", }; } if (selected === DENY_WITH_REASON_OPTION || selected === LEGACY_DENY_WITH_REASON_OPTION) { const denialReason = normalizePermissionDenialReason( await ui.input( `${title}\nShare why this request was denied (optional).`, "Reason shown back to the agent", ), ); return createDeniedPermissionDecision(denialReason); } return createDeniedPermissionDecision(); }