Spaces:
Paused
Paused
| import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js"; | |
| import type { ExecApprovalDecision } from "../../infra/exec-approvals.js"; | |
| import type { ExecApprovalManager } from "../exec-approval-manager.js"; | |
| import type { GatewayRequestHandlers } from "./types.js"; | |
| import { | |
| ErrorCodes, | |
| errorShape, | |
| formatValidationErrors, | |
| validateExecApprovalRequestParams, | |
| validateExecApprovalResolveParams, | |
| } from "../protocol/index.js"; | |
| export function createExecApprovalHandlers( | |
| manager: ExecApprovalManager, | |
| opts?: { forwarder?: ExecApprovalForwarder }, | |
| ): GatewayRequestHandlers { | |
| return { | |
| "exec.approval.request": async ({ params, respond, context }) => { | |
| if (!validateExecApprovalRequestParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid exec.approval.request params: ${formatValidationErrors( | |
| validateExecApprovalRequestParams.errors, | |
| )}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const p = params as { | |
| id?: string; | |
| command: string; | |
| cwd?: string; | |
| host?: string; | |
| security?: string; | |
| ask?: string; | |
| agentId?: string; | |
| resolvedPath?: string; | |
| sessionKey?: string; | |
| timeoutMs?: number; | |
| twoPhase?: boolean; | |
| }; | |
| const twoPhase = p.twoPhase === true; | |
| const timeoutMs = typeof p.timeoutMs === "number" ? p.timeoutMs : 120_000; | |
| const explicitId = typeof p.id === "string" && p.id.trim().length > 0 ? p.id.trim() : null; | |
| if (explicitId && manager.getSnapshot(explicitId)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape(ErrorCodes.INVALID_REQUEST, "approval id already pending"), | |
| ); | |
| return; | |
| } | |
| const request = { | |
| command: p.command, | |
| cwd: p.cwd ?? null, | |
| host: p.host ?? null, | |
| security: p.security ?? null, | |
| ask: p.ask ?? null, | |
| agentId: p.agentId ?? null, | |
| resolvedPath: p.resolvedPath ?? null, | |
| sessionKey: p.sessionKey ?? null, | |
| }; | |
| const record = manager.create(request, timeoutMs, explicitId); | |
| // Use register() to synchronously add to pending map before sending any response. | |
| // This ensures the approval ID is valid immediately after the "accepted" response. | |
| let decisionPromise: Promise< | |
| import("../../infra/exec-approvals.js").ExecApprovalDecision | null | |
| >; | |
| try { | |
| decisionPromise = manager.register(record, timeoutMs); | |
| } catch (err) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape(ErrorCodes.INVALID_REQUEST, `registration failed: ${String(err)}`), | |
| ); | |
| return; | |
| } | |
| context.broadcast( | |
| "exec.approval.requested", | |
| { | |
| id: record.id, | |
| request: record.request, | |
| createdAtMs: record.createdAtMs, | |
| expiresAtMs: record.expiresAtMs, | |
| }, | |
| { dropIfSlow: true }, | |
| ); | |
| void opts?.forwarder | |
| ?.handleRequested({ | |
| id: record.id, | |
| request: record.request, | |
| createdAtMs: record.createdAtMs, | |
| expiresAtMs: record.expiresAtMs, | |
| }) | |
| .catch((err) => { | |
| context.logGateway?.error?.(`exec approvals: forward request failed: ${String(err)}`); | |
| }); | |
| // Only send immediate "accepted" response when twoPhase is requested. | |
| // This preserves single-response semantics for existing callers. | |
| if (twoPhase) { | |
| respond( | |
| true, | |
| { | |
| status: "accepted", | |
| id: record.id, | |
| createdAtMs: record.createdAtMs, | |
| expiresAtMs: record.expiresAtMs, | |
| }, | |
| undefined, | |
| ); | |
| } | |
| const decision = await decisionPromise; | |
| // Send final response with decision for callers using expectFinal:true. | |
| respond( | |
| true, | |
| { | |
| id: record.id, | |
| decision, | |
| createdAtMs: record.createdAtMs, | |
| expiresAtMs: record.expiresAtMs, | |
| }, | |
| undefined, | |
| ); | |
| }, | |
| "exec.approval.waitDecision": async ({ params, respond }) => { | |
| const p = params as { id?: string }; | |
| const id = typeof p.id === "string" ? p.id.trim() : ""; | |
| if (!id) { | |
| respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "id is required")); | |
| return; | |
| } | |
| const decisionPromise = manager.awaitDecision(id); | |
| if (!decisionPromise) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape(ErrorCodes.INVALID_REQUEST, "approval expired or not found"), | |
| ); | |
| return; | |
| } | |
| // Capture snapshot before await (entry may be deleted after grace period) | |
| const snapshot = manager.getSnapshot(id); | |
| const decision = await decisionPromise; | |
| // Return decision (can be null on timeout) - let clients handle via askFallback | |
| respond( | |
| true, | |
| { | |
| id, | |
| decision, | |
| createdAtMs: snapshot?.createdAtMs, | |
| expiresAtMs: snapshot?.expiresAtMs, | |
| }, | |
| undefined, | |
| ); | |
| }, | |
| "exec.approval.resolve": async ({ params, respond, client, context }) => { | |
| if (!validateExecApprovalResolveParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid exec.approval.resolve params: ${formatValidationErrors( | |
| validateExecApprovalResolveParams.errors, | |
| )}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const p = params as { id: string; decision: string }; | |
| const decision = p.decision as ExecApprovalDecision; | |
| if (decision !== "allow-once" && decision !== "allow-always" && decision !== "deny") { | |
| respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid decision")); | |
| return; | |
| } | |
| const resolvedBy = client?.connect?.client?.displayName ?? client?.connect?.client?.id; | |
| const ok = manager.resolve(p.id, decision, resolvedBy ?? null); | |
| if (!ok) { | |
| respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown approval id")); | |
| return; | |
| } | |
| context.broadcast( | |
| "exec.approval.resolved", | |
| { id: p.id, decision, resolvedBy, ts: Date.now() }, | |
| { dropIfSlow: true }, | |
| ); | |
| void opts?.forwarder | |
| ?.handleResolved({ id: p.id, decision, resolvedBy, ts: Date.now() }) | |
| .catch((err) => { | |
| context.logGateway?.error?.(`exec approvals: forward resolve failed: ${String(err)}`); | |
| }); | |
| respond(true, { ok: true }, undefined); | |
| }, | |
| }; | |
| } | |