import type { PermissionOption, PermissionRequest, PermissionResponse, PermissionUiContext, } from "./types.js"; import { isRecord } from "./types.js"; function parsePermissionOption(value: unknown): PermissionOption | undefined { if (!isRecord(value)) return undefined; if (typeof value.optionId !== "string" || typeof value.name !== "string" || typeof value.kind !== "string") { return undefined; } return { optionId: value.optionId, name: value.name, kind: value.kind }; } export function parsePermissionRequest(value: unknown): PermissionRequest | undefined { if (!isRecord(value)) return undefined; const toolCall = isRecord(value.toolCall) ? { ...(typeof value.toolCall.toolCallId === "string" ? { toolCallId: value.toolCall.toolCallId } : {}), ...(typeof value.toolCall.title === "string" ? { title: value.toolCall.title } : {}), ...(typeof value.toolCall.kind === "string" ? { kind: value.toolCall.kind } : {}), ...("rawInput" in value.toolCall ? { rawInput: value.toolCall.rawInput } : {}), } : undefined; const options = Array.isArray(value.options) ? value.options.map(parsePermissionOption).filter((option): option is PermissionOption => option !== undefined) : []; return { ...(typeof value.sessionId === "string" ? { sessionId: value.sessionId } : {}), ...(toolCall ? { toolCall } : {}), options, }; } function cancelled(): PermissionResponse { return { outcome: { outcome: "cancelled" } }; } function selected(optionId: string): PermissionResponse { return { outcome: { outcome: "selected", optionId } }; } function rejectionOption(options: readonly PermissionOption[]): PermissionOption | undefined { return options.find((option) => option.kind === "reject_once") ?? options.find((option) => option.kind === "reject_always"); } function formatInput(value: unknown): string { if (value === undefined) return ""; let text: string; try { text = typeof value === "string" ? value : JSON.stringify(value, null, 2); } catch { text = "[unserializable input]"; } return text.length > 4_000 ? `${text.slice(0, 4_000)}\n…` : text; } function optionLabel(option: PermissionOption, index: number): string { return `${index + 1}. ${option.name} [${option.kind}]`; } export class PermissionBroker { private tail: Promise = Promise.resolve(); constructor(private readonly getContext: () => PermissionUiContext | undefined) {} handle(value: unknown, signal?: AbortSignal): Promise { const run = this.tail.then(() => this.handleNow(value, signal)); this.tail = run.then( () => undefined, () => undefined, ); return run; } private async handleNow(value: unknown, signal?: AbortSignal): Promise { if (signal?.aborted) return cancelled(); const request = parsePermissionRequest(value); if (!request || !request.options || request.options.length === 0) return cancelled(); const context = this.getContext(); if (!context?.hasUI) { const reject = rejectionOption(request.options); return reject ? selected(reject.optionId) : cancelled(); } const labels = request.options.map(optionLabel); const input = formatInput(request.toolCall?.rawInput); const title = [ `Grok Build requests permission: ${request.toolCall?.title ?? request.toolCall?.kind ?? "tool"}`, input ? `\n${input}` : "", ].join(""); const choice = await context.ui.select(title, labels, signal ? { signal } : undefined); if (signal?.aborted) return cancelled(); const choiceIndex = choice ? labels.indexOf(choice) : -1; if (choiceIndex >= 0) { const option = request.options[choiceIndex]; if (option) return selected(option.optionId); } const reject = rejectionOption(request.options); return reject ? selected(reject.optionId) : cancelled(); } }