mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-10 12:32:27 +00:00
211 lines
7.1 KiB
TypeScript
211 lines
7.1 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js";
|
|
import type { ExecApprovalDecision } from "../../infra/exec-approvals.js";
|
|
import type { PluginApprovalRequestPayload } from "../../infra/plugin-approvals.js";
|
|
import {
|
|
DEFAULT_PLUGIN_APPROVAL_TIMEOUT_MS,
|
|
MAX_PLUGIN_APPROVAL_TIMEOUT_MS,
|
|
resolvePluginApprovalRequestAllowedDecisions,
|
|
} from "../../infra/plugin-approvals.js";
|
|
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
|
import type { ExecApprovalManager } from "../exec-approval-manager.js";
|
|
import {
|
|
ErrorCodes,
|
|
errorShape,
|
|
formatValidationErrors,
|
|
validatePluginApprovalRequestParams,
|
|
validatePluginApprovalResolveParams,
|
|
} from "../protocol/index.js";
|
|
import {
|
|
handleApprovalResolve,
|
|
handleApprovalWaitDecision,
|
|
handlePendingApprovalRequest,
|
|
isApprovalDecision,
|
|
} from "./approval-shared.js";
|
|
import type { GatewayRequestHandlers } from "./types.js";
|
|
|
|
export function createPluginApprovalHandlers(
|
|
manager: ExecApprovalManager<PluginApprovalRequestPayload>,
|
|
opts?: { forwarder?: ExecApprovalForwarder },
|
|
): GatewayRequestHandlers {
|
|
return {
|
|
"plugin.approval.list": async ({ respond }) => {
|
|
respond(
|
|
true,
|
|
manager.listPendingRecords().map((record) => ({
|
|
id: record.id,
|
|
request: record.request,
|
|
createdAtMs: record.createdAtMs,
|
|
expiresAtMs: record.expiresAtMs,
|
|
})),
|
|
undefined,
|
|
);
|
|
},
|
|
"plugin.approval.request": async ({ params, client, respond, context }) => {
|
|
if (!validatePluginApprovalRequestParams(params)) {
|
|
respond(
|
|
false,
|
|
undefined,
|
|
errorShape(
|
|
ErrorCodes.INVALID_REQUEST,
|
|
`invalid plugin.approval.request params: ${formatValidationErrors(
|
|
validatePluginApprovalRequestParams.errors,
|
|
)}`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
const p = params as {
|
|
pluginId?: string | null;
|
|
title: string;
|
|
description: string;
|
|
severity?: string | null;
|
|
toolName?: string | null;
|
|
toolCallId?: string | null;
|
|
allowedDecisions?: string[] | null;
|
|
agentId?: string | null;
|
|
sessionKey?: string | null;
|
|
turnSourceChannel?: string | null;
|
|
turnSourceTo?: string | null;
|
|
turnSourceAccountId?: string | null;
|
|
turnSourceThreadId?: string | number | null;
|
|
timeoutMs?: number;
|
|
twoPhase?: boolean;
|
|
};
|
|
const twoPhase = p.twoPhase === true;
|
|
const timeoutMs = Math.min(
|
|
typeof p.timeoutMs === "number" ? p.timeoutMs : DEFAULT_PLUGIN_APPROVAL_TIMEOUT_MS,
|
|
MAX_PLUGIN_APPROVAL_TIMEOUT_MS,
|
|
);
|
|
|
|
const normalizeTrimmedString = (value?: string | null): string | null =>
|
|
normalizeOptionalString(value) || null;
|
|
|
|
const request: PluginApprovalRequestPayload = {
|
|
pluginId: p.pluginId ?? null,
|
|
title: p.title,
|
|
description: p.description,
|
|
severity: (p.severity as PluginApprovalRequestPayload["severity"]) ?? null,
|
|
toolName: p.toolName ?? null,
|
|
toolCallId: p.toolCallId ?? null,
|
|
...(Array.isArray(p.allowedDecisions)
|
|
? {
|
|
allowedDecisions: resolvePluginApprovalRequestAllowedDecisions({
|
|
allowedDecisions: p.allowedDecisions,
|
|
}),
|
|
}
|
|
: {}),
|
|
agentId: p.agentId ?? null,
|
|
sessionKey: p.sessionKey ?? null,
|
|
turnSourceChannel: normalizeTrimmedString(p.turnSourceChannel),
|
|
turnSourceTo: normalizeTrimmedString(p.turnSourceTo),
|
|
turnSourceAccountId: normalizeTrimmedString(p.turnSourceAccountId),
|
|
turnSourceThreadId: p.turnSourceThreadId ?? null,
|
|
};
|
|
|
|
// Always server-generate the ID — never accept plugin-provided IDs.
|
|
// Kind-prefix so /approve routing can distinguish plugin vs exec IDs deterministically.
|
|
const record = manager.create(request, timeoutMs, `plugin:${randomUUID()}`);
|
|
|
|
let decisionPromise: Promise<ExecApprovalDecision | null>;
|
|
try {
|
|
decisionPromise = manager.register(record, timeoutMs);
|
|
} catch (err) {
|
|
respond(
|
|
false,
|
|
undefined,
|
|
errorShape(ErrorCodes.INVALID_REQUEST, `registration failed: ${String(err)}`),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const requestEvent = {
|
|
id: record.id,
|
|
request: record.request,
|
|
createdAtMs: record.createdAtMs,
|
|
expiresAtMs: record.expiresAtMs,
|
|
};
|
|
|
|
await handlePendingApprovalRequest({
|
|
manager,
|
|
record,
|
|
decisionPromise,
|
|
respond,
|
|
context,
|
|
clientConnId: client?.connId,
|
|
requestEventName: "plugin.approval.requested",
|
|
requestEvent,
|
|
twoPhase,
|
|
deliverRequest: () => {
|
|
if (!opts?.forwarder?.handlePluginApprovalRequested) {
|
|
return false;
|
|
}
|
|
return opts.forwarder.handlePluginApprovalRequested(requestEvent).catch((err) => {
|
|
context.logGateway?.error?.(`plugin approvals: forward request failed: ${String(err)}`);
|
|
return false;
|
|
});
|
|
},
|
|
});
|
|
},
|
|
|
|
"plugin.approval.waitDecision": async ({ params, respond }) => {
|
|
await handleApprovalWaitDecision({
|
|
manager,
|
|
inputId: (params as { id?: string }).id,
|
|
respond,
|
|
});
|
|
},
|
|
|
|
"plugin.approval.resolve": async ({ params, respond, client, context }) => {
|
|
if (!validatePluginApprovalResolveParams(params)) {
|
|
respond(
|
|
false,
|
|
undefined,
|
|
errorShape(
|
|
ErrorCodes.INVALID_REQUEST,
|
|
`invalid plugin.approval.resolve params: ${formatValidationErrors(
|
|
validatePluginApprovalResolveParams.errors,
|
|
)}`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
const p = params as { id: string; decision: string };
|
|
if (!isApprovalDecision(p.decision)) {
|
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid decision"));
|
|
return;
|
|
}
|
|
const decision = p.decision;
|
|
await handleApprovalResolve({
|
|
manager,
|
|
inputId: p.id,
|
|
decision,
|
|
respond,
|
|
context,
|
|
client,
|
|
exposeAmbiguousPrefixError: false,
|
|
validateDecision: (snapshot) =>
|
|
resolvePluginApprovalRequestAllowedDecisions(snapshot.request).includes(decision)
|
|
? null
|
|
: {
|
|
message: `${decision} is unavailable for this plugin approval`,
|
|
details: {
|
|
allowedDecisions: resolvePluginApprovalRequestAllowedDecisions(snapshot.request),
|
|
},
|
|
},
|
|
resolvedEventName: "plugin.approval.resolved",
|
|
buildResolvedEvent: ({ approvalId, decision, resolvedBy, snapshot, nowMs }) => ({
|
|
id: approvalId,
|
|
decision,
|
|
resolvedBy,
|
|
ts: nowMs,
|
|
request: snapshot.request,
|
|
}),
|
|
forwardResolved: (resolvedEvent) =>
|
|
opts?.forwarder?.handlePluginApprovalResolved?.(resolvedEvent),
|
|
forwardResolvedErrorLabel: "plugin approvals: forward resolve failed",
|
|
});
|
|
},
|
|
};
|
|
}
|