| import { hasApprovalTurnSourceRoute } from "../../infra/approval-turn-source.js"; |
| import { sanitizeExecApprovalDisplayText } from "../../infra/exec-approval-command-display.js"; |
| import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js"; |
| import { |
| DEFAULT_EXEC_APPROVAL_TIMEOUT_MS, |
| type ExecApprovalDecision, |
| } from "../../infra/exec-approvals.js"; |
| import { |
| buildSystemRunApprovalBinding, |
| buildSystemRunApprovalEnvBinding, |
| } from "../../infra/system-run-approval-binding.js"; |
| import { resolveSystemRunApprovalRequestContext } from "../../infra/system-run-approval-context.js"; |
| import type { ExecApprovalManager } from "../exec-approval-manager.js"; |
| import { |
| ErrorCodes, |
| errorShape, |
| formatValidationErrors, |
| validateExecApprovalRequestParams, |
| validateExecApprovalResolveParams, |
| } from "../protocol/index.js"; |
| import type { GatewayRequestHandlers } from "./types.js"; |
|
|
| const APPROVAL_NOT_FOUND_DETAILS = { |
| reason: "APPROVAL_NOT_FOUND", |
| } as const; |
|
|
| export function createExecApprovalHandlers( |
| manager: ExecApprovalManager, |
| opts?: { forwarder?: ExecApprovalForwarder }, |
| ): GatewayRequestHandlers { |
| return { |
| "exec.approval.request": async ({ params, respond, context, client }) => { |
| 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; |
| commandArgv?: string[]; |
| env?: Record<string, string>; |
| cwd?: string; |
| systemRunPlan?: unknown; |
| nodeId?: string; |
| host?: string; |
| security?: string; |
| ask?: string; |
| agentId?: string; |
| resolvedPath?: string; |
| sessionKey?: string; |
| turnSourceChannel?: string; |
| turnSourceTo?: string; |
| turnSourceAccountId?: string; |
| turnSourceThreadId?: string | number; |
| timeoutMs?: number; |
| twoPhase?: boolean; |
| }; |
| const twoPhase = p.twoPhase === true; |
| const timeoutMs = |
| typeof p.timeoutMs === "number" ? p.timeoutMs : DEFAULT_EXEC_APPROVAL_TIMEOUT_MS; |
| const explicitId = typeof p.id === "string" && p.id.trim().length > 0 ? p.id.trim() : null; |
| const host = typeof p.host === "string" ? p.host.trim() : ""; |
| const nodeId = typeof p.nodeId === "string" ? p.nodeId.trim() : ""; |
| const approvalContext = resolveSystemRunApprovalRequestContext({ |
| host, |
| command: p.command, |
| commandArgv: p.commandArgv, |
| systemRunPlan: p.systemRunPlan, |
| cwd: p.cwd, |
| agentId: p.agentId, |
| sessionKey: p.sessionKey, |
| }); |
| const effectiveCommandArgv = approvalContext.commandArgv; |
| const effectiveCwd = approvalContext.cwd; |
| const effectiveAgentId = approvalContext.agentId; |
| const effectiveSessionKey = approvalContext.sessionKey; |
| const effectiveCommandText = approvalContext.commandText; |
| if (host === "node" && !nodeId) { |
| respond( |
| false, |
| undefined, |
| errorShape(ErrorCodes.INVALID_REQUEST, "nodeId is required for host=node"), |
| ); |
| return; |
| } |
| if (host === "node" && !approvalContext.plan) { |
| respond( |
| false, |
| undefined, |
| errorShape(ErrorCodes.INVALID_REQUEST, "systemRunPlan is required for host=node"), |
| ); |
| return; |
| } |
| if (!effectiveCommandText) { |
| respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "command is required")); |
| return; |
| } |
| if ( |
| host === "node" && |
| (!Array.isArray(effectiveCommandArgv) || effectiveCommandArgv.length === 0) |
| ) { |
| respond( |
| false, |
| undefined, |
| errorShape(ErrorCodes.INVALID_REQUEST, "commandArgv is required for host=node"), |
| ); |
| return; |
| } |
| const envBinding = buildSystemRunApprovalEnvBinding(p.env); |
| const systemRunBinding = |
| host === "node" |
| ? buildSystemRunApprovalBinding({ |
| argv: effectiveCommandArgv, |
| cwd: effectiveCwd, |
| agentId: effectiveAgentId, |
| sessionKey: effectiveSessionKey, |
| env: p.env, |
| }) |
| : null; |
| if (explicitId && manager.getSnapshot(explicitId)) { |
| respond( |
| false, |
| undefined, |
| errorShape(ErrorCodes.INVALID_REQUEST, "approval id already pending"), |
| ); |
| return; |
| } |
| const request = { |
| command: sanitizeExecApprovalDisplayText(effectiveCommandText), |
| commandPreview: |
| host === "node" || !approvalContext.commandPreview |
| ? undefined |
| : sanitizeExecApprovalDisplayText(approvalContext.commandPreview), |
| commandArgv: host === "node" ? undefined : effectiveCommandArgv, |
| envKeys: envBinding.envKeys.length > 0 ? envBinding.envKeys : undefined, |
| systemRunBinding: systemRunBinding?.binding ?? null, |
| systemRunPlan: approvalContext.plan, |
| cwd: effectiveCwd ?? null, |
| nodeId: host === "node" ? nodeId : null, |
| host: host || null, |
| security: p.security ?? null, |
| ask: p.ask ?? null, |
| agentId: effectiveAgentId ?? null, |
| resolvedPath: p.resolvedPath ?? null, |
| sessionKey: effectiveSessionKey ?? null, |
| turnSourceChannel: |
| typeof p.turnSourceChannel === "string" ? p.turnSourceChannel.trim() || null : null, |
| turnSourceTo: typeof p.turnSourceTo === "string" ? p.turnSourceTo.trim() || null : null, |
| turnSourceAccountId: |
| typeof p.turnSourceAccountId === "string" ? p.turnSourceAccountId.trim() || null : null, |
| turnSourceThreadId: p.turnSourceThreadId ?? null, |
| }; |
| const record = manager.create(request, timeoutMs, explicitId); |
| record.requestedByConnId = client?.connId ?? null; |
| record.requestedByDeviceId = client?.connect?.device?.id ?? null; |
| record.requestedByClientId = client?.connect?.client?.id ?? null; |
| |
| |
| 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 }, |
| ); |
| const hasExecApprovalClients = context.hasExecApprovalClients?.(client?.connId) ?? false; |
| const hasTurnSourceRoute = hasApprovalTurnSourceRoute({ |
| turnSourceChannel: record.request.turnSourceChannel, |
| turnSourceAccountId: record.request.turnSourceAccountId, |
| }); |
| let forwarded = false; |
| if (opts?.forwarder) { |
| try { |
| forwarded = await 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)}`); |
| } |
| } |
|
|
| if (!hasExecApprovalClients && !forwarded && !hasTurnSourceRoute) { |
| manager.expire(record.id, "no-approval-route"); |
| respond( |
| true, |
| { |
| id: record.id, |
| decision: null, |
| createdAtMs: record.createdAtMs, |
| expiresAtMs: record.expiresAtMs, |
| }, |
| undefined, |
| ); |
| return; |
| } |
|
|
| |
| |
| if (twoPhase) { |
| respond( |
| true, |
| { |
| status: "accepted", |
| id: record.id, |
| createdAtMs: record.createdAtMs, |
| expiresAtMs: record.expiresAtMs, |
| }, |
| undefined, |
| ); |
| } |
|
|
| const decision = await decisionPromise; |
| |
| 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; |
| } |
| |
| const snapshot = manager.getSnapshot(id); |
| const decision = await decisionPromise; |
| |
| 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 resolvedId = manager.lookupPendingId(p.id); |
| if (resolvedId.kind === "none") { |
| respond( |
| false, |
| undefined, |
| errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id", { |
| details: APPROVAL_NOT_FOUND_DETAILS, |
| }), |
| ); |
| return; |
| } |
| if (resolvedId.kind === "ambiguous") { |
| const candidates = resolvedId.ids.slice(0, 3).join(", "); |
| const remainder = resolvedId.ids.length > 3 ? ` (+${resolvedId.ids.length - 3} more)` : ""; |
| respond( |
| false, |
| undefined, |
| errorShape( |
| ErrorCodes.INVALID_REQUEST, |
| `ambiguous approval id prefix; matches: ${candidates}${remainder}. Use the full id.`, |
| ), |
| ); |
| return; |
| } |
| const approvalId = resolvedId.id; |
| const snapshot = manager.getSnapshot(approvalId); |
| const resolvedBy = client?.connect?.client?.displayName ?? client?.connect?.client?.id; |
| const ok = manager.resolve(approvalId, decision, resolvedBy ?? null); |
| if (!ok) { |
| respond( |
| false, |
| undefined, |
| errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id"), |
| ); |
| return; |
| } |
| context.broadcast( |
| "exec.approval.resolved", |
| { id: approvalId, decision, resolvedBy, ts: Date.now(), request: snapshot?.request }, |
| { dropIfSlow: true }, |
| ); |
| void opts?.forwarder |
| ?.handleResolved({ |
| id: approvalId, |
| decision, |
| resolvedBy, |
| ts: Date.now(), |
| request: snapshot?.request, |
| }) |
| .catch((err) => { |
| context.logGateway?.error?.(`exec approvals: forward resolve failed: ${String(err)}`); |
| }); |
| respond(true, { ok: true }, undefined); |
| }, |
| }; |
| } |
|
|