Spaces:
Configuration error
Configuration error
| import { | |
| ensureExecApprovals, | |
| normalizeExecApprovals, | |
| readExecApprovalsSnapshot, | |
| resolveExecApprovalsSocketPath, | |
| saveExecApprovals, | |
| type ExecApprovalsFile, | |
| type ExecApprovalsSnapshot, | |
| } from "../../infra/exec-approvals.js"; | |
| import { | |
| ErrorCodes, | |
| errorShape, | |
| formatValidationErrors, | |
| validateExecApprovalsGetParams, | |
| validateExecApprovalsNodeGetParams, | |
| validateExecApprovalsNodeSetParams, | |
| validateExecApprovalsSetParams, | |
| } from "../protocol/index.js"; | |
| import { respondUnavailableOnThrow, safeParseJson } from "./nodes.helpers.js"; | |
| import type { GatewayRequestHandlers, RespondFn } from "./types.js"; | |
| function resolveBaseHash(params: unknown): string | null { | |
| const raw = (params as { baseHash?: unknown })?.baseHash; | |
| if (typeof raw !== "string") return null; | |
| const trimmed = raw.trim(); | |
| return trimmed ? trimmed : null; | |
| } | |
| 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 = resolveBaseHash(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, | |
| }; | |
| } | |
| export const execApprovalsHandlers: GatewayRequestHandlers = { | |
| "exec.approvals.get": ({ params, respond }) => { | |
| if (!validateExecApprovalsGetParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid exec.approvals.get params: ${formatValidationErrors(validateExecApprovalsGetParams.errors)}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| ensureExecApprovals(); | |
| const snapshot = readExecApprovalsSnapshot(); | |
| respond( | |
| true, | |
| { | |
| path: snapshot.path, | |
| exists: snapshot.exists, | |
| hash: snapshot.hash, | |
| file: redactExecApprovals(snapshot.file), | |
| }, | |
| undefined, | |
| ); | |
| }, | |
| "exec.approvals.set": ({ params, respond }) => { | |
| if (!validateExecApprovalsSetParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid exec.approvals.set params: ${formatValidationErrors(validateExecApprovalsSetParams.errors)}`, | |
| ), | |
| ); | |
| 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 currentSocketPath = snapshot.file.socket?.path?.trim(); | |
| const currentToken = snapshot.file.socket?.token?.trim(); | |
| const socketPath = | |
| normalized.socket?.path?.trim() ?? currentSocketPath ?? resolveExecApprovalsSocketPath(); | |
| const token = normalized.socket?.token?.trim() ?? currentToken ?? ""; | |
| const next: ExecApprovalsFile = { | |
| ...normalized, | |
| socket: { | |
| path: socketPath, | |
| token, | |
| }, | |
| }; | |
| saveExecApprovals(next); | |
| const nextSnapshot = readExecApprovalsSnapshot(); | |
| respond( | |
| true, | |
| { | |
| path: nextSnapshot.path, | |
| exists: nextSnapshot.exists, | |
| hash: nextSnapshot.hash, | |
| file: redactExecApprovals(nextSnapshot.file), | |
| }, | |
| undefined, | |
| ); | |
| }, | |
| "exec.approvals.node.get": async ({ params, respond, context }) => { | |
| if (!validateExecApprovalsNodeGetParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid exec.approvals.node.get params: ${formatValidationErrors(validateExecApprovalsNodeGetParams.errors)}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const { nodeId } = params as { nodeId: string }; | |
| const id = nodeId.trim(); | |
| if (!id) { | |
| respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "nodeId required")); | |
| return; | |
| } | |
| await respondUnavailableOnThrow(respond, async () => { | |
| const res = await context.nodeRegistry.invoke({ | |
| nodeId: id, | |
| command: "system.execApprovals.get", | |
| params: {}, | |
| }); | |
| if (!res.ok) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape(ErrorCodes.UNAVAILABLE, res.error?.message ?? "node invoke failed", { | |
| details: { nodeError: res.error ?? null }, | |
| }), | |
| ); | |
| return; | |
| } | |
| const payload = res.payloadJSON ? safeParseJson(res.payloadJSON) : res.payload; | |
| respond(true, payload, undefined); | |
| }); | |
| }, | |
| "exec.approvals.node.set": async ({ params, respond, context }) => { | |
| if (!validateExecApprovalsNodeSetParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid exec.approvals.node.set params: ${formatValidationErrors(validateExecApprovalsNodeSetParams.errors)}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const { nodeId, file, baseHash } = params as { | |
| nodeId: string; | |
| file: ExecApprovalsFile; | |
| baseHash?: string; | |
| }; | |
| const id = nodeId.trim(); | |
| if (!id) { | |
| respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "nodeId required")); | |
| return; | |
| } | |
| await respondUnavailableOnThrow(respond, async () => { | |
| const res = await context.nodeRegistry.invoke({ | |
| nodeId: id, | |
| command: "system.execApprovals.set", | |
| params: { file, baseHash }, | |
| }); | |
| if (!res.ok) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape(ErrorCodes.UNAVAILABLE, res.error?.message ?? "node invoke failed", { | |
| details: { nodeError: res.error ?? null }, | |
| }), | |
| ); | |
| return; | |
| } | |
| const payload = safeParseJson(res.payloadJSON ?? null); | |
| respond(true, payload, undefined); | |
| }); | |
| }, | |
| }; | |