mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
248 lines
7.5 KiB
TypeScript
248 lines
7.5 KiB
TypeScript
import type { ExecAsk, ExecSecurity, SystemRunApprovalPlan } from "../infra/exec-approvals.js";
|
|
import {
|
|
DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS,
|
|
DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
} from "./bash-tools.exec-runtime.js";
|
|
import { callGatewayTool } from "./tools/gateway.js";
|
|
|
|
export type RequestExecApprovalDecisionParams = {
|
|
id: string;
|
|
command?: string;
|
|
commandArgv?: string[];
|
|
systemRunPlan?: SystemRunApprovalPlan;
|
|
env?: Record<string, string>;
|
|
cwd: string;
|
|
nodeId?: string;
|
|
host: "gateway" | "node";
|
|
security: ExecSecurity;
|
|
ask: ExecAsk;
|
|
agentId?: string;
|
|
resolvedPath?: string;
|
|
sessionKey?: string;
|
|
turnSourceChannel?: string;
|
|
turnSourceTo?: string;
|
|
turnSourceAccountId?: string;
|
|
turnSourceThreadId?: string | number;
|
|
};
|
|
|
|
type ExecApprovalRequestToolParams = RequestExecApprovalDecisionParams & {
|
|
timeoutMs: number;
|
|
twoPhase: true;
|
|
};
|
|
|
|
function buildExecApprovalRequestToolParams(
|
|
params: RequestExecApprovalDecisionParams,
|
|
): ExecApprovalRequestToolParams {
|
|
return {
|
|
id: params.id,
|
|
...(params.command ? { command: params.command } : {}),
|
|
...(params.commandArgv ? { commandArgv: params.commandArgv } : {}),
|
|
systemRunPlan: params.systemRunPlan,
|
|
env: params.env,
|
|
cwd: params.cwd,
|
|
nodeId: params.nodeId,
|
|
host: params.host,
|
|
security: params.security,
|
|
ask: params.ask,
|
|
agentId: params.agentId,
|
|
resolvedPath: params.resolvedPath,
|
|
sessionKey: params.sessionKey,
|
|
turnSourceChannel: params.turnSourceChannel,
|
|
turnSourceTo: params.turnSourceTo,
|
|
turnSourceAccountId: params.turnSourceAccountId,
|
|
turnSourceThreadId: params.turnSourceThreadId,
|
|
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
twoPhase: true,
|
|
};
|
|
}
|
|
|
|
type ParsedDecision = { present: boolean; value: string | null };
|
|
|
|
function parseDecision(value: unknown): ParsedDecision {
|
|
if (!value || typeof value !== "object") {
|
|
return { present: false, value: null };
|
|
}
|
|
// Distinguish "field missing" from "field present but null/invalid".
|
|
// Registration responses intentionally omit `decision`; decision waits can include it.
|
|
if (!Object.hasOwn(value, "decision")) {
|
|
return { present: false, value: null };
|
|
}
|
|
const decision = (value as { decision?: unknown }).decision;
|
|
return { present: true, value: typeof decision === "string" ? decision : null };
|
|
}
|
|
|
|
function parseString(value: unknown): string | undefined {
|
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
}
|
|
|
|
function parseExpiresAtMs(value: unknown): number | undefined {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
}
|
|
|
|
export type ExecApprovalRegistration = {
|
|
id: string;
|
|
expiresAtMs: number;
|
|
finalDecision?: string | null;
|
|
};
|
|
|
|
export async function registerExecApprovalRequest(
|
|
params: RequestExecApprovalDecisionParams,
|
|
): Promise<ExecApprovalRegistration> {
|
|
// Two-phase registration is critical: the ID must be registered server-side
|
|
// before exec returns `approval-pending`, otherwise `/approve` can race and orphan.
|
|
const registrationResult = await callGatewayTool<{
|
|
id?: string;
|
|
expiresAtMs?: number;
|
|
decision?: string;
|
|
}>(
|
|
"exec.approval.request",
|
|
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
|
|
buildExecApprovalRequestToolParams(params),
|
|
{ expectFinal: false },
|
|
);
|
|
const decision = parseDecision(registrationResult);
|
|
const id = parseString(registrationResult?.id) ?? params.id;
|
|
const expiresAtMs =
|
|
parseExpiresAtMs(registrationResult?.expiresAtMs) ?? Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
if (decision.present) {
|
|
return { id, expiresAtMs, finalDecision: decision.value };
|
|
}
|
|
return { id, expiresAtMs };
|
|
}
|
|
|
|
export async function waitForExecApprovalDecision(id: string): Promise<string | null> {
|
|
try {
|
|
const decisionResult = await callGatewayTool<{ decision: string }>(
|
|
"exec.approval.waitDecision",
|
|
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
|
|
{ id },
|
|
);
|
|
return parseDecision(decisionResult).value;
|
|
} catch (err) {
|
|
// Timeout/cleanup path: treat missing/expired as no decision so askFallback applies.
|
|
const message = String(err).toLowerCase();
|
|
if (message.includes("approval expired or not found")) {
|
|
return null;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export async function resolveRegisteredExecApprovalDecision(params: {
|
|
approvalId: string;
|
|
preResolvedDecision: string | null | undefined;
|
|
}): Promise<string | null> {
|
|
if (params.preResolvedDecision !== undefined) {
|
|
return params.preResolvedDecision ?? null;
|
|
}
|
|
return await waitForExecApprovalDecision(params.approvalId);
|
|
}
|
|
|
|
export async function requestExecApprovalDecision(
|
|
params: RequestExecApprovalDecisionParams,
|
|
): Promise<string | null> {
|
|
const registration = await registerExecApprovalRequest(params);
|
|
if (Object.hasOwn(registration, "finalDecision")) {
|
|
return registration.finalDecision ?? null;
|
|
}
|
|
return await waitForExecApprovalDecision(registration.id);
|
|
}
|
|
|
|
type HostExecApprovalParams = {
|
|
approvalId: string;
|
|
command?: string;
|
|
commandArgv?: string[];
|
|
systemRunPlan?: SystemRunApprovalPlan;
|
|
env?: Record<string, string>;
|
|
workdir: string;
|
|
host: "gateway" | "node";
|
|
nodeId?: string;
|
|
security: ExecSecurity;
|
|
ask: ExecAsk;
|
|
agentId?: string;
|
|
resolvedPath?: string;
|
|
sessionKey?: string;
|
|
turnSourceChannel?: string;
|
|
turnSourceTo?: string;
|
|
turnSourceAccountId?: string;
|
|
turnSourceThreadId?: string | number;
|
|
};
|
|
|
|
type ExecApprovalRequesterContext = {
|
|
agentId?: string;
|
|
sessionKey?: string;
|
|
};
|
|
|
|
export function buildExecApprovalRequesterContext(params: ExecApprovalRequesterContext): {
|
|
agentId?: string;
|
|
sessionKey?: string;
|
|
} {
|
|
return {
|
|
agentId: params.agentId,
|
|
sessionKey: params.sessionKey,
|
|
};
|
|
}
|
|
|
|
type ExecApprovalTurnSourceContext = {
|
|
turnSourceChannel?: string;
|
|
turnSourceTo?: string;
|
|
turnSourceAccountId?: string;
|
|
turnSourceThreadId?: string | number;
|
|
};
|
|
|
|
export function buildExecApprovalTurnSourceContext(
|
|
params: ExecApprovalTurnSourceContext,
|
|
): ExecApprovalTurnSourceContext {
|
|
return {
|
|
turnSourceChannel: params.turnSourceChannel,
|
|
turnSourceTo: params.turnSourceTo,
|
|
turnSourceAccountId: params.turnSourceAccountId,
|
|
turnSourceThreadId: params.turnSourceThreadId,
|
|
};
|
|
}
|
|
|
|
function buildHostApprovalDecisionParams(
|
|
params: HostExecApprovalParams,
|
|
): RequestExecApprovalDecisionParams {
|
|
return {
|
|
id: params.approvalId,
|
|
command: params.command,
|
|
commandArgv: params.commandArgv,
|
|
systemRunPlan: params.systemRunPlan,
|
|
env: params.env,
|
|
cwd: params.workdir,
|
|
nodeId: params.nodeId,
|
|
host: params.host,
|
|
security: params.security,
|
|
ask: params.ask,
|
|
...buildExecApprovalRequesterContext({
|
|
agentId: params.agentId,
|
|
sessionKey: params.sessionKey,
|
|
}),
|
|
resolvedPath: params.resolvedPath,
|
|
...buildExecApprovalTurnSourceContext(params),
|
|
};
|
|
}
|
|
|
|
export async function requestExecApprovalDecisionForHost(
|
|
params: HostExecApprovalParams,
|
|
): Promise<string | null> {
|
|
return await requestExecApprovalDecision(buildHostApprovalDecisionParams(params));
|
|
}
|
|
|
|
export async function registerExecApprovalRequestForHost(
|
|
params: HostExecApprovalParams,
|
|
): Promise<ExecApprovalRegistration> {
|
|
return await registerExecApprovalRequest(buildHostApprovalDecisionParams(params));
|
|
}
|
|
|
|
export async function registerExecApprovalRequestForHostOrThrow(
|
|
params: HostExecApprovalParams,
|
|
): Promise<ExecApprovalRegistration> {
|
|
try {
|
|
return await registerExecApprovalRequestForHost(params);
|
|
} catch (err) {
|
|
throw new Error(`Exec approval registration failed: ${String(err)}`, { cause: err });
|
|
}
|
|
}
|