openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
434 lines (433 loc) • 20.3 kB
JavaScript
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { _ as resolveGatewayPort } from "./paths-D2sRr1a_.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-B1bPgeKr.js";
import "./config-Cs0XXL3x.js";
import { r as trimToUndefined } from "./credential-planner-DAMjVc9j.js";
import { i as resolveGatewayCredentialsFromConfig } from "./credentials-CXbS4PS0.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import { n as loadDeviceIdentityIfPresent, r as loadOrCreateDeviceIdentity } from "./device-identity-J83pn_rP.js";
import { o as callGateway } from "./call-BWR5pPgw.js";
import { n as claimAgentRunApprovalAuthority, s as getActiveAgentRunDelegatedAuthority } from "./agent-run-registry-CKYKdfNd.js";
import { s as resolveLeastPrivilegeOperatorScopesForMethod } from "./method-scopes-K6J_UQGL.js";
import { d as readPositiveIntegerParam, h as readToolStringParam } from "./common-Bm6UTDDA.js";
import { r as getGatewayToolCallerIdentity } from "./gateway-caller-context-BxVUFvkR.js";
import { o as createAgentRuntimeExecutionLineageHandoff, r as mintAgentRuntimeIdentityToken, s as readAgentRuntimeExecutionLineage } from "./agent-runtime-identity-token-DoLEaEV9.js";
import { r as resolveMessageActionTurnCapability } from "./message-action-turn-capability-CpL-pT1E.js";
import { t as getOperatorApprovalRuntimeToken } from "./operator-approval-runtime-token-BASQugN0.js";
import { AsyncLocalStorage } from "node:async_hooks";
//#region src/agents/tools/gateway-session-spawn-context.ts
const sessionSpawnContext = new AsyncLocalStorage();
/** Scope signed session-creation authority to one local Gateway tool call. */
function runWithGatewaySessionSpawnContext(context, run) {
return sessionSpawnContext.run(context, run);
}
function getGatewaySessionSpawnContext() {
return sessionSpawnContext.getStore();
}
//#endregion
//#region src/agents/tools/gateway-session-spawn-execution-identity.ts
const parentExecutionIdentityToken = new AsyncLocalStorage();
/** Scope exact parent evidence to the same local Gateway call as spawn authority. */
function runWithGatewaySessionSpawnParentExecutionIdentity(token, run) {
return token ? parentExecutionIdentityToken.run(token, run) : run();
}
function getGatewaySessionSpawnParentExecutionIdentityToken() {
return parentExecutionIdentityToken.getStore();
}
//#endregion
//#region src/agents/tools/gateway.ts
/**
* Gateway call helpers for built-in tools.
*
* Resolves gateway URL/token overrides, local credentials, and least-privilege operator scopes.
*/
/** Reads common gateway options from tool parameters while preserving explicit token whitespace. */
function readGatewayCallOptions(params) {
return {
gatewayUrl: readToolStringParam(params, "gatewayUrl", { trim: false }),
gatewayToken: readToolStringParam(params, "gatewayToken", { trim: false }),
timeoutMs: readPositiveIntegerParam(params, "timeoutMs")
};
}
/**
* Canonicalizes websocket URLs for allowlist comparisons without retaining paths or credentials.
*/
function canonicalizeToolGatewayWsUrl(raw) {
const input = raw.trim();
let url;
try {
url = new URL(input);
} catch (error) {
const message = formatErrorMessage(error);
throw new Error(`invalid gatewayUrl: ${input} (${message})`, { cause: error });
}
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new Error(`invalid gatewayUrl protocol: ${url.protocol} (expected ws:// or wss://)`);
if (url.username || url.password) throw new Error("invalid gatewayUrl: credentials are not allowed");
if (url.search || url.hash) throw new Error("invalid gatewayUrl: query/hash not allowed");
if (url.pathname && url.pathname !== "/") throw new Error("invalid gatewayUrl: path not allowed");
return {
origin: url.origin,
key: `${url.protocol}//${normalizeLowercaseStringOrEmpty(url.host)}`
};
}
function resolveLocalGatewayUrlKeys(cfg) {
const port = resolveGatewayPort(cfg);
return /* @__PURE__ */ new Set([
`ws://127.0.0.1:${port}`,
`wss://127.0.0.1:${port}`,
`ws://localhost:${port}`,
`wss://localhost:${port}`,
`ws://[::1]:${port}`,
`wss://[::1]:${port}`
]);
}
function resolveConfiguredRemoteGatewayKey(cfg) {
let remoteKey;
const remoteUrl = normalizeOptionalString(cfg.gateway?.remote?.url) ?? "";
if (remoteUrl) try {
remoteKey = canonicalizeToolGatewayWsUrl(remoteUrl).key;
} catch {}
return remoteKey;
}
function resolveDefaultGatewayTarget(params) {
if (params.envGatewayUrl) return "remote";
if (params.cfg.gateway?.mode === "remote" && normalizeOptionalString(params.cfg.gateway.remote?.url)) return "remote";
return "local";
}
function validateGatewayUrlOverrideForAgentTools(params) {
const { cfg } = params;
const localAllowed = resolveLocalGatewayUrlKeys(cfg);
const remoteKey = resolveConfiguredRemoteGatewayKey(cfg);
const parsed = canonicalizeToolGatewayWsUrl(params.urlOverride);
if (localAllowed.has(parsed.key)) return {
url: parsed.origin,
target: "local"
};
if (remoteKey && parsed.key === remoteKey) return {
url: parsed.origin,
target: "remote"
};
const port = resolveGatewayPort(cfg);
throw new Error([
"gatewayUrl override rejected.",
`Allowed: ws(s) loopback on port ${port} (127.0.0.1/localhost/[::1])`,
"Or: configure gateway.remote.url and omit gatewayUrl to use the configured remote gateway."
].join(" "));
}
function resolveGatewayOverrideToken(params) {
if (params.explicitToken) return params.explicitToken;
return resolveGatewayCredentialsFromConfig({
cfg: params.cfg,
env: process.env,
modeOverride: params.target,
remoteTokenFallback: params.target === "remote" ? "remote-only" : "remote-env-local",
remotePasswordFallback: params.target === "remote" ? "remote-only" : "remote-env-local"
}).token;
}
/**
* Resolves the gateway URL, token, and timeout for agent tool calls.
*/
function resolveGatewayOptions(opts) {
const cfg = getRuntimeConfig();
const validatedOverride = trimToUndefined(opts?.gatewayUrl) !== void 0 ? validateGatewayUrlOverrideForAgentTools({
cfg,
urlOverride: String(opts?.gatewayUrl)
}) : void 0;
const explicitToken = trimToUndefined(opts?.gatewayToken);
const token = validatedOverride ? resolveGatewayOverrideToken({
cfg,
target: validatedOverride.target,
explicitToken
}) : explicitToken;
const timeoutMs = typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? Math.max(1, Math.floor(opts.timeoutMs)) : 3e4;
const envGatewayUrl = trimToUndefined(process.env.OPENCLAW_GATEWAY_URL);
const target = validatedOverride?.target ?? resolveDefaultGatewayTarget({
cfg,
envGatewayUrl
});
return {
url: validatedOverride?.url,
token,
timeoutMs,
target
};
}
const APPROVAL_RUNTIME_METHODS = /* @__PURE__ */ new Set([
"exec.approval.request",
"exec.approval.resolve",
"exec.approval.waitDecision",
"plugin.approval.request",
"plugin.approval.waitDecision"
]);
const AGENT_RUNTIME_IDENTITY_METHODS = /* @__PURE__ */ new Set([
"exec.approval.request",
"plugin.approval.request",
"wake",
"cron.list",
"cron.get",
"cron.add",
"cron.update",
"cron.remove",
"cron.run",
"cron.runs",
"secrets.store.delete"
]);
const OPTIONAL_LOCAL_AGENT_RUNTIME_IDENTITY_METHODS = /* @__PURE__ */ new Set(["node.invoke", "question.request"]);
function resolveApprovalRuntimeTokenForGatewayTool(params) {
if (!APPROVAL_RUNTIME_METHODS.has(params.method)) return;
if (trimToUndefined(params.opts.gatewayUrl) !== void 0) return;
if (params.target !== "local") return;
return getOperatorApprovalRuntimeToken();
}
function isApprovalReplayNodeSystemRun(method, callParams) {
const invoke = method === "node.invoke" ? asNullableRecord(callParams) : null;
const run = invoke?.command === "system.run" ? asNullableRecord(invoke.params) : null;
const decision = normalizeOptionalString(run?.approvalDecision);
return run?.approved === true || decision === "allow-once" || decision === "allow-always";
}
function attachNodeInvokeTurnSource(method, params) {
if (method !== "node.invoke") return params;
const invoke = asNullableRecord(params);
const caller = getGatewayToolCallerIdentity();
if (!invoke || !caller) return params;
return {
...omitNodeInvokeTurnSource(invoke),
...caller.turnSourceChannel ? { turnSourceChannel: caller.turnSourceChannel } : {},
...caller.turnSourceTo ? { turnSourceTo: caller.turnSourceTo } : {},
...caller.turnSourceAccountId ? { turnSourceAccountId: caller.turnSourceAccountId } : {},
...caller.turnSourceThreadId !== void 0 ? { turnSourceThreadId: caller.turnSourceThreadId } : {}
};
}
function omitNodeInvokeTurnSource(invoke) {
const legacyParams = { ...invoke };
delete legacyParams.turnSourceChannel;
delete legacyParams.turnSourceTo;
delete legacyParams.turnSourceAccountId;
delete legacyParams.turnSourceThreadId;
return legacyParams;
}
function stripNodeInvokeTurnSource(params) {
const invoke = asNullableRecord(params);
return invoke ? omitNodeInvokeTurnSource(invoke) : params;
}
function resolveApprovalRequesterDeviceIdentityForGatewayTool(params) {
const isApprovalRuntimeMethod = APPROVAL_RUNTIME_METHODS.has(params.method);
const isNodeApprovalReplay = isApprovalReplayNodeSystemRun(params.method, params.callParams);
if (!isApprovalRuntimeMethod && !isNodeApprovalReplay) return;
if (isApprovalRuntimeMethod && trimToUndefined(params.opts.gatewayUrl) !== void 0) return;
if (params.approvalRuntimeToken !== void 0) return;
try {
if (isNodeApprovalReplay) {
const identity = loadDeviceIdentityIfPresent();
if (!identity) throw new Error("device identity is not persisted");
return identity;
}
return loadOrCreateDeviceIdentity();
} catch (error) {
if (isNodeApprovalReplay) throw new Error(["approved node gateway calls require a stable device identity.", "Fix the OpenClaw state directory permissions and retry the approval."].join(" "), { cause: error });
throw new Error(["remote approval gateway calls require a stable device identity.", "Fix the OpenClaw state directory permissions or use the local approval-runtime gateway."].join(" "), { cause: error });
}
}
async function resolveAgentRuntimeIdentityTokenForGatewayTool(params) {
const optionalLocalIdentity = OPTIONAL_LOCAL_AGENT_RUNTIME_IDENTITY_METHODS.has(params.method);
if (!params.required && !AGENT_RUNTIME_IDENTITY_METHODS.has(params.method) && !optionalLocalIdentity) return;
const identity = getGatewayToolCallerIdentity();
if (!identity) {
if (params.required) throw new Error("trusted agent runtime identity required for this gateway call");
return;
}
const hasGatewayUrlOverride = trimToUndefined(params.opts.gatewayUrl) !== void 0;
const hasGatewayTokenOverride = trimToUndefined(params.opts.gatewayToken) !== void 0;
if (hasGatewayUrlOverride || hasGatewayTokenOverride || params.target !== "local") {
if (optionalLocalIdentity && !params.required) return;
throw new Error("agent gateway calls require the trusted local gateway context");
}
if (identity.signedAgentRuntimeIdentityToken) return identity.signedAgentRuntimeIdentityToken;
if (optionalLocalIdentity && !params.required && !identity.gatewayContextResolver) return;
if (!identity.operationalRunInstance) {
if (optionalLocalIdentity && !params.required) return;
throw new Error("trusted operational run instance required for this gateway call");
}
try {
const sessionSpawnContext = getGatewaySessionSpawnContext();
const parentExecutionIdentityToken = getGatewaySessionSpawnParentExecutionIdentityToken();
const activeAuthority = getActiveAgentRunDelegatedAuthority(identity.operationalRunInstance);
const executionLineage = readAgentRuntimeExecutionLineage(sessionSpawnContext);
if (executionLineage && !activeAuthority) throw new Error("execution lineage handoff requires active parent authority");
const lineageHandoff = sessionSpawnContext && executionLineage && activeAuthority ? createAgentRuntimeExecutionLineageHandoff({
agentId: identity.agentId,
sessionKey: identity.sessionKey,
operationalRunInstance: identity.operationalRunInstance,
delegatedAuthority: activeAuthority,
...parentExecutionIdentityToken ? { executionIdentity: parentExecutionIdentityToken } : {},
sessionSpawnContext
}) : void 0;
if (executionLineage && !lineageHandoff) throw new Error("execution lineage handoff could not bind the parent admission");
try {
const approvalSignals = params.method === "exec.approval.request" || params.method === "plugin.approval.request" ? [...identity.approvalSignals ?? [], ...params.signal ? [params.signal] : []] : void 0;
const approvalAuthority = activeAuthority && approvalSignals?.length ? claimAgentRunApprovalAuthority(activeAuthority, approvalSignals) : void 0;
return await mintAgentRuntimeIdentityToken({
...identity,
operationalRunInstance: identity.operationalRunInstance,
approvalAuthority,
...lineageHandoff ? { executionIdentityToken: void 0 } : {},
...lineageHandoff ? { executionLineageHandoffId: lineageHandoff.id } : sessionSpawnContext ? {
executionIdentityToken: parentExecutionIdentityToken,
sessionSpawnContext
} : {}
});
} catch (error) {
lineageHandoff?.revoke();
throw error;
}
} catch (error) {
if (optionalLocalIdentity && !params.required) return;
throw error;
}
}
async function resolveMessageActionAgentRuntimeIdentityToken(params) {
const terminalSourceReply = params.sourceReplyFinal === true;
const sourceReplyToolCallId = normalizeOptionalString(params.sourceReplyToolCallId);
if (terminalSourceReply && !sourceReplyToolCallId) throw new Error("terminal source reply requires tool-call correlation");
const identity = getGatewayToolCallerIdentity();
if (!identity) {
if (terminalSourceReply) throw new Error("terminal source reply requires trusted agent runtime identity");
return;
}
const hasGatewayUrlOverride = trimToUndefined(params.opts.gatewayUrl) !== void 0;
const hasGatewayTokenOverride = trimToUndefined(params.opts.gatewayToken) !== void 0;
const usesUntrustedGatewayContext = hasGatewayUrlOverride || hasGatewayTokenOverride || params.target !== "local";
if (usesUntrustedGatewayContext && !terminalSourceReply) return;
const turnCapabilitySessionKey = normalizeOptionalString(params.turnCapabilitySessionKey) ?? identity.sessionKey;
const messageActionContext = resolveMessageActionTurnCapability({
token: params.turnCapability,
agentId: identity.agentId,
runId: params.runId,
sessionKey: turnCapabilitySessionKey,
sessionId: params.sessionId
});
if (!messageActionContext) {
if (terminalSourceReply) throw new Error("terminal source reply requires an active turn capability");
return;
}
if (terminalSourceReply && !normalizeOptionalString(messageActionContext.toolContext?.currentSourceTurnId)) throw new Error("terminal source reply requires source-turn correlation");
if (usesUntrustedGatewayContext) {
if (params.callerOwnsTerminalReceipt !== true) throw new Error("terminal source reply requires the trusted local gateway context");
return;
}
if (!identity.operationalRunInstance) {
if (terminalSourceReply) throw new Error("terminal source reply requires a trusted operational run instance");
return;
}
const resolvedMessageActionContext = terminalSourceReply ? {
...messageActionContext,
turnCapability: params.turnCapability,
sourceReplyFinal: true,
sourceReplyToolCallId
} : {
...messageActionContext,
turnCapability: params.turnCapability,
...params.sourceReplyFinal === false ? { sourceReplyFinal: false } : {},
...sourceReplyToolCallId ? { sourceReplyToolCallId } : {}
};
return await mintAgentRuntimeIdentityToken({
...identity,
sessionKey: turnCapabilitySessionKey,
operationalRunInstance: identity.operationalRunInstance,
messageActionContext: resolvedMessageActionContext
});
}
function isStaleGatewayAgentRuntimeIdentityRejection(error) {
const message = formatErrorMessage(error);
if (message.includes("gateway rejected required agent runtime identity auth field; refusing to retry without it")) return true;
return message.includes("invalid connect params") && message.includes("/auth") && message.includes("unexpected property 'agentRuntimeIdentityToken'");
}
function isStaleGatewayNodeInvokeTurnSourceRejection(error) {
if (!(error instanceof Error) || error.name !== "GatewayClientRequestError") return false;
const requestError = error;
if (requestError.gatewayCode !== ErrorCodes.INVALID_REQUEST) return false;
if (asNullableRecord(requestError.details)?.nodeCommandDispatched !== false) return false;
const message = formatErrorMessage(error);
if (!message.includes("invalid node.invoke params:")) return false;
return [
"turnSourceChannel",
"turnSourceTo",
"turnSourceAccountId",
"turnSourceThreadId"
].some((field) => message.includes(`unexpected property '${field}'`));
}
function staleGatewayAgentRuntimeIdentityError(cause) {
return new Error(["The running Gateway is from an older OpenClaw build and rejected current agent runtime connection metadata.", "Restart the Gateway with `openclaw gateway restart`, then retry."].join(" "), { cause });
}
/**
* Calls a gateway method as the agent-tool backend client with least-privilege scopes.
*/
async function callGatewayTool(method, opts, params, extra) {
const dispatchAuthority = extra?.dispatchAuthority;
if (dispatchAuthority && (dispatchAuthority.version !== 2 || typeof dispatchAuthority.assertCurrent !== "function")) throw new Error("Gateway dispatch authority requires version 2 and a synchronous assertion");
const gateway = resolveGatewayOptions(opts);
const resolveGatewayContext = getGatewayToolCallerIdentity()?.gatewayContextResolver;
const callParams = attachNodeInvokeTurnSource(method, params);
const scopes = Array.isArray(extra?.scopes) ? extra.scopes : resolveLeastPrivilegeOperatorScopesForMethod(method, callParams);
const approvalRuntimeToken = resolveApprovalRuntimeTokenForGatewayTool({
method,
opts,
target: gateway.target
});
const agentRuntimeIdentityToken = await resolveAgentRuntimeIdentityTokenForGatewayTool({
method,
opts,
target: gateway.target,
required: extra?.requireAgentRuntimeIdentity,
signal: extra?.signal
});
const deviceIdentity = resolveApprovalRequesterDeviceIdentityForGatewayTool({
method,
callParams,
opts,
approvalRuntimeToken
});
const callOptions = {
url: gateway.url,
token: gateway.token,
method,
params: callParams,
timeoutMs: gateway.timeoutMs,
signal: extra?.signal,
expectFinal: extra?.expectFinal,
assertDispatchCurrent: extra?.dispatchAuthority?.assertCurrent,
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
clientDisplayName: "agent",
mode: GATEWAY_CLIENT_MODES.BACKEND,
...approvalRuntimeToken ? { approvalRuntimeToken } : {},
...agentRuntimeIdentityToken ? { agentRuntimeIdentityToken } : {},
...deviceIdentity ? { deviceIdentity } : {},
scopes
};
const dispatch = (options) => {
if (resolveGatewayContext && !resolveGatewayContext()) throw new Error("The admitting Gateway is no longer available. Retry from a new agent run.");
return callGateway(options);
};
try {
return await dispatch(callOptions);
} catch (error) {
if (method === "node.invoke" && isStaleGatewayNodeInvokeTurnSourceRejection(error)) return await dispatch({
...callOptions,
params: stripNodeInvokeTurnSource(callOptions.params)
});
if (agentRuntimeIdentityToken && isStaleGatewayAgentRuntimeIdentityRejection(error)) {
if (method === "node.invoke" && extra?.requireAgentRuntimeIdentity !== true) return await dispatch({
...callOptions,
params: stripNodeInvokeTurnSource(callOptions.params),
agentRuntimeIdentityToken: void 0
});
throw staleGatewayAgentRuntimeIdentityError(error);
}
throw error;
}
}
//#endregion
export { runWithGatewaySessionSpawnParentExecutionIdentity as a, resolveMessageActionAgentRuntimeIdentityToken as i, readGatewayCallOptions as n, runWithGatewaySessionSpawnContext as o, resolveGatewayOptions as r, callGatewayTool as t };