| 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); |
| }); |
| }, |
| }; |
|
|