import { ensureExecApprovals, mergeExecApprovalsSocketDefaults, normalizeExecApprovals, readExecApprovalsSnapshot, saveExecApprovals, type ExecApprovalsFile, type ExecApprovalsSnapshot, } from "../../infra/exec-approvals.js"; import { ErrorCodes, errorShape, validateExecApprovalsGetParams, validateExecApprovalsNodeGetParams, validateExecApprovalsNodeSetParams, validateExecApprovalsSetParams, } from "../protocol/index.js"; import { resolveBaseHashParam } from "./base-hash.js"; import { respondUnavailableOnNodeInvokeError, respondUnavailableOnThrow, safeParseJson, } from "./nodes.helpers.js"; import type { GatewayRequestHandlers, RespondFn } from "./types.js"; import { assertValidParams } from "./validation.js"; function requireApprovalsBaseHash( params: unknown, snapshot: ExecApprovalsSnapshot, respond: RespondFn, ): boolean { if (!snapshot.exists) { return true; } if (!snapshot.hash) { respond( false, undefined, errorShape( ErrorCodes.INVALID_REQUEST, "exec approvals base hash unavailable; re-run exec.approvals.get and retry", ), ); return false; } const baseHash = resolveBaseHashParam(params); if (!baseHash) { respond( false, undefined, errorShape( ErrorCodes.INVALID_REQUEST, "exec approvals base hash required; re-run exec.approvals.get and retry", ), ); return false; } if (baseHash !== snapshot.hash) { respond( false, undefined, errorShape( ErrorCodes.INVALID_REQUEST, "exec approvals changed since last load; re-run exec.approvals.get and retry", ), ); return false; } return true; } function redactExecApprovals(file: ExecApprovalsFile): ExecApprovalsFile { const socketPath = file.socket?.path?.trim(); return { ...file, socket: socketPath ? { path: socketPath } : undefined, }; } function toExecApprovalsPayload(snapshot: ExecApprovalsSnapshot) { return { path: snapshot.path, exists: snapshot.exists, hash: snapshot.hash, file: redactExecApprovals(snapshot.file), }; } function resolveNodeIdOrRespond(nodeId: string, respond: RespondFn): string | null { const id = nodeId.trim(); if (!id) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "nodeId required")); return null; } return id; } export const execApprovalsHandlers: GatewayRequestHandlers = { "exec.approvals.get": ({ params, respond }) => { if (!assertValidParams(params, validateExecApprovalsGetParams, "exec.approvals.get", respond)) { return; } ensureExecApprovals(); const snapshot = readExecApprovalsSnapshot(); respond(true, toExecApprovalsPayload(snapshot), undefined); }, "exec.approvals.set": ({ params, respond }) => { if (!assertValidParams(params, validateExecApprovalsSetParams, "exec.approvals.set", respond)) { return; } ensureExecApprovals(); const snapshot = readExecApprovalsSnapshot(); if (!requireApprovalsBaseHash(params, snapshot, respond)) { return; } const incoming = (params as { file?: unknown }).file; if (!incoming || typeof incoming !== "object") { respond( false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "exec approvals file is required"), ); return; } const normalized = normalizeExecApprovals(incoming as ExecApprovalsFile); const next = mergeExecApprovalsSocketDefaults({ normalized, current: snapshot.file }); saveExecApprovals(next); const nextSnapshot = readExecApprovalsSnapshot(); respond(true, toExecApprovalsPayload(nextSnapshot), undefined); }, "exec.approvals.node.get": async ({ params, respond, context }) => { if ( !assertValidParams( params, validateExecApprovalsNodeGetParams, "exec.approvals.node.get", respond, ) ) { return; } const { nodeId } = params as { nodeId: string }; const id = resolveNodeIdOrRespond(nodeId, respond); if (!id) { return; } await respondUnavailableOnThrow(respond, async () => { const res = await context.nodeRegistry.invoke({ nodeId: id, command: "system.execApprovals.get", params: {}, }); if (!respondUnavailableOnNodeInvokeError(respond, res)) { return; } const payload = res.payloadJSON ? safeParseJson(res.payloadJSON) : res.payload; respond(true, payload, undefined); }); }, "exec.approvals.node.set": async ({ params, respond, context }) => { if ( !assertValidParams( params, validateExecApprovalsNodeSetParams, "exec.approvals.node.set", respond, ) ) { return; } const { nodeId, file, baseHash } = params as { nodeId: string; file: ExecApprovalsFile; baseHash?: string; }; const id = resolveNodeIdOrRespond(nodeId, respond); if (!id) { return; } await respondUnavailableOnThrow(respond, async () => { const res = await context.nodeRegistry.invoke({ nodeId: id, command: "system.execApprovals.set", params: { file, baseHash }, }); if (!respondUnavailableOnNodeInvokeError(respond, res)) { return; } const payload = safeParseJson(res.payloadJSON ?? null); respond(true, payload, undefined); }); }, };