UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

299 lines (298 loc) • 14.3 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { r as createLazyRuntimeModule } from "./lazy-runtime-CJrzFzqU.mjs"; import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs"; import { r as resolveAgentConfig } from "./agent-scope-config-CDYpzA0r.mjs"; import { M as parseAgentSessionKey } from "./session-key-C09ow4h0.mjs"; import { u as withPluginRuntimeRegistryScope } from "./gateway-request-scope-BSmCVVpP.mjs"; import { l as getActivePluginRegistry } from "./runtime-NsVZTERq.mjs"; import { l as resolveExecModePolicy } from "./exec-approvals-core-s7tTQC_a.mjs"; import { r as getRuntimeConfig } from "./io.runtime-BsUXSsD4.mjs"; import "./config-DLycWFvB.mjs"; import { t as logDebug } from "./logger-DskmrSVa.mjs"; import { _ as parseComputerUseCapabilityDescriptor } from "./computer-use-contract-g23cJXOi.mjs"; import { a as DEFAULT_SECURITY } from "./exec-approvals-migration-gate-4cEEb67t.mjs"; import { a as createExecApprovalPolicySnapshot, g as requiresExecApproval } from "./exec-approvals-authorization.kernel-C_vJhPCn.mjs"; import { r as loadExecApprovals } from "./exec-approvals-store-DGp2moY9.mjs"; import { i as recordAllowlistMatchesUse } from "./exec-approvals-B1rZTeax.mjs"; import { t as applyExecPolicyLayer } from "./exec-policy-DDPEdabr.mjs"; //#region src/node-host/exec-policy.ts /** Evaluates node-host exec policy from security, approval, and allowlist context. */ /** One config owner for system.run and plugin-hosted execution. */ function resolveNodeExecConfigPolicy(params) { const agentExec = params.agentId ? resolveAgentConfig(params.cfg, params.agentId)?.tools?.exec : void 0; const globalExec = params.cfg.tools?.exec; const layered = applyExecPolicyLayer(applyExecPolicyLayer({ security: DEFAULT_SECURITY, ask: "off" }, globalExec), agentExec); return { agentExec, globalExec, ...resolveExecModePolicy(layered) }; } /** Normalizes raw approval decisions from node-host payloads. */ function resolveExecApprovalDecision(value) { if (value === "allow-once" || value === "allow-always") return value; return null; } /** Combines exec security, allowlist analysis, and approval state into an allow/deny decision. */ function evaluateSystemRunPolicy(params) { const shellWrapperBlocked = params.security === "allowlist" && params.shellWrapperInvocation && params.isWindows && params.cmdInvocation; const analysisOk = shellWrapperBlocked ? false : params.analysisOk; const allowlistSatisfied = shellWrapperBlocked ? false : params.allowlistSatisfied; const approvedByAsk = params.approvalDecision !== null || params.approved === true; const requiresAsk = params.security !== "deny" && requiresExecApproval({ ask: params.ask, security: params.security, analysisOk, allowlistSatisfied, durableApprovalSatisfied: params.durableApprovalSatisfied }); const context = { analysisOk, allowlistSatisfied, shellWrapperBlocked, approvalDecision: params.approvalDecision, approvedByAsk }; if (params.security === "deny") return { allowed: false, eventReason: "security=deny", errorMessage: "SYSTEM_RUN_DISABLED: security=deny", ...context }; if (requiresAsk && !approvedByAsk) return { allowed: false, eventReason: "approval-required", errorMessage: "SYSTEM_RUN_DENIED: approval required", ...context }; if (params.security === "allowlist" && (!analysisOk || !allowlistSatisfied) && !approvedByAsk && !params.durableApprovalSatisfied) return { allowed: false, eventReason: "allowlist-miss", errorMessage: shellWrapperBlocked ? "SYSTEM_RUN_DENIED: allowlist miss (Windows shell wrappers like cmd.exe /c require approval; approve once/always or run with --ask on-miss|always)" : "SYSTEM_RUN_DENIED: allowlist miss", ...context }; return { allowed: true, ...context }; } //#endregion //#region src/node-host/plugin-exec-policy.ts /** Local policy stays on the executor; Gateway approval never overrides a local deny. */ function preparePluginExecAuthorization(params) { params.assertActive(); const agentId = parseAgentSessionKey(params.sessionKey)?.agentId; const resolvePolicy = () => resolveNodeExecConfigPolicy({ cfg: getRuntimeConfig(), agentId }); const policy = resolvePolicy(); const approvals = loadExecApprovals(); const policySnapshot = createExecApprovalPolicySnapshot({ file: approvals, agentId }); const assertCurrent = () => { params.assertActive(); const current = resolvePolicy(); if (current.security === "deny" || current.security !== policy.security || current.ask !== policy.ask || current.autoReview !== policy.autoReview || params.source === "session-full" && (current.security !== "full" || current.ask !== "off")) throw new Error("SYSTEM_RUN_DENIED: node-local exec policy does not authorize this launch"); recordAllowlistMatchesUse({ approvals, agentId, command: params.command, matches: [], authorization: { source: params.source === "human-approved" ? "explicit-approval" : "current-policy", security: current.security, ask: current.ask, allowlistSatisfied: false, policySnapshot } }); params.assertActive(); }; assertCurrent(); return assertCurrent; } //#endregion //#region src/node-host/plugin-node-host.ts /** Plugin node-host bridge for loading plugin registry commands and dispatching node capabilities. */ const loadPluginRegistryLoaderModule = createLazyRuntimeModule(() => import("./plugins/loader.js")); let nodeHostPluginRegistry; function resolveNodeHostPluginRegistry() { return nodeHostPluginRegistry ?? getActivePluginRegistry() ?? void 0; } /** Ensure plugin registry data is loaded before node-host command dispatch. */ async function ensureNodeHostPluginRegistry(params) { const registry = (await loadPluginRegistryLoaderModule()).loadPluginRegistryHandle({ config: params.config, activationSourceConfig: params.config, env: params.env, onlyPluginIds: params.onlyPluginIds, logger: params.logger }); await withPluginRuntimeRegistryScope(registry, async () => { const prepare = new Set(registry.nodeHostCommands.filter((entry) => !params.commandAllowlist || params.commandAllowlist.has(entry.command.command)).map((entry) => entry.command.prepare)); await Promise.all([...prepare].map(async (callback) => callback?.({ config: params.config, env: params.env ?? process.env }))); }); nodeHostPluginRegistry = registry; } /** List registered node-host capabilities and command ids in deterministic order. */ function listRegisteredNodeHostCapsAndCommands(context, options = {}) { const registry = resolveNodeHostPluginRegistry(); return withPluginRuntimeRegistryScope(registry, () => { const caps = /* @__PURE__ */ new Set(); const commands = /* @__PURE__ */ new Set(); let computerUse; const nodePluginTools = /* @__PURE__ */ new Map(); for (const entry of registry?.nodeHostCommands ?? []) { if (options.commandAllowlist && !options.commandAllowlist.has(entry.command.command)) continue; if (entry.command.duplex === true && options.includeDuplex === false) continue; if (entry.command.isAvailable?.(context) === false) continue; if (entry.command.cap) caps.add(entry.command.cap); commands.add(entry.command.command); if (!options.commandAllowlist && entry.command.computerUse) computerUse = parseComputerUseCapabilityDescriptor(entry.command.computerUse(context)); const agentTool = options.commandAllowlist ? null : buildNodePluginToolDescriptor(entry); if (agentTool) nodePluginTools.set(`${agentTool.pluginId}\0${agentTool.name}`, agentTool); } return { caps: [...caps].toSorted((left, right) => left.localeCompare(right)), commands: [...commands].toSorted((left, right) => left.localeCompare(right)), ...computerUse ? { computerUse } : {}, nodePluginTools: [...nodePluginTools.values()].toSorted((left, right) => left.pluginId.localeCompare(right.pluginId) || left.name.localeCompare(right.name)) }; }); } /** Watch plugin-owned availability inputs that can change during this process. */ function watchRegisteredNodeHostCommandAvailability(context, onChange, commandAllowlist) { const registry = resolveNodeHostPluginRegistry(); let cleanups = []; let stopped = false; let stopping; withPluginRuntimeRegistryScope(registry, () => { for (const entry of registry?.nodeHostCommands ?? []) { if (commandAllowlist && !commandAllowlist.has(entry.command.command)) continue; const cleanup = entry.command.watchAvailability?.(context, () => { if (!stopped) withPluginRuntimeRegistryScope(registry, onChange); }); if (cleanup) cleanups.push(cleanup); } }); return () => { stopped = true; return stopping ??= Promise.resolve().then(async () => { const results = await Promise.allSettled(cleanups.map(async (cleanup) => withPluginRuntimeRegistryScope(registry, () => cleanup()))); cleanups = cleanups.filter((_cleanup, index) => results[index]?.status === "rejected"); const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []); if (failures.length === 1) throw failures[0]; if (failures.length > 1) throw new AggregateError(failures, "node-host watcher cleanup failed"); }).catch((error) => { stopping = void 0; throw error; }); }; } /** Release plugin command state before a reconnected Gateway can invoke it again. */ async function notifyRegisteredNodeHostCommandDisconnect() { const registry = resolveNodeHostPluginRegistry(); const callbacks = new Set((registry?.nodeHostCommands ?? []).map((entry) => entry.command.onDisconnect).filter((callback) => callback !== void 0)); await withPluginRuntimeRegistryScope(registry, async () => { const failures = (await Promise.allSettled([...callbacks].map(async (callback) => await callback()))).flatMap((result) => result.status === "rejected" ? [result.reason] : []); if (failures.length === 1) { const failure = failures[0]; throw failure instanceof Error ? failure : new Error("node-host plugin disconnect cleanup failed", { cause: failure }); } if (failures.length > 1) throw new AggregateError(failures, "node-host plugin disconnect cleanup failed"); }); } /** Retained command work remains owned even when its capability is unavailable. */ function hasRegisteredNodeHostCommandActiveWork() { const registry = resolveNodeHostPluginRegistry(); return withPluginRuntimeRegistryScope(registry, () => { for (const entry of registry?.nodeHostCommands ?? []) try { if (entry.command.hasActiveWork?.() !== false) { if (!entry.command.hasActiveWork) logDebug(`node-host: ${entry.pluginId}/${entry.command.command} has no idle hook; auto-update deferred`); return true; } } catch (error) { logDebug(`node-host: plugin work state unavailable: ${String(error)}`); return true; } return false; }); } function buildNodePluginToolDescriptor(entry) { const agentTool = entry.command.agentTool; if (!agentTool) return null; const name = normalizeOptionalString(agentTool.name) ?? ""; const description = normalizeOptionalString(agentTool.description) ?? ""; if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name) || !description) return null; const mcpServer = normalizeOptionalString(agentTool.mcp?.server) ?? ""; const mcpTool = normalizeOptionalString(agentTool.mcp?.tool) ?? ""; return { pluginId: entry.pluginId, name, description, parameters: asOptionalRecord(agentTool.parameters) ?? { type: "object", properties: {}, additionalProperties: true }, command: entry.command.command, ...mcpServer && mcpTool ? { mcp: { server: mcpServer, tool: mcpTool } } : {} }; } /** Invoke a registered node-host plugin command, or return null for unknown commands. */ async function invokeRegisteredNodeHostCommand(command, paramsJSON, io, context) { const registry = resolveNodeHostPluginRegistry(); const match = (registry?.nodeHostCommands ?? []).find((entry) => entry.command.command === command); if (!match) return null; let active = true; const registeredCommand = match.command; const pluginRecord = registry?.plugins.find((record) => record.id === match.pluginId); const assertActive = () => { if (!active || match.command !== registeredCommand || io?.signal.aborted || context?.signal?.aborted || resolveNodeHostPluginRegistry() !== registry || !registry?.nodeHostCommands.includes(match) || !pluginRecord || !registry.plugins.includes(pluginRecord) || !pluginRecord.enabled || pluginRecord.status !== "loaded") throw new Error("node plugin invocation authority is closed"); }; const invokeContext = context ? { ...context, prepareExecAuthorization: (source) => preparePluginExecAuthorization({ source, command, sessionKey: context.sessionKey, assertActive }) } : void 0; try { return await withPluginRuntimeRegistryScope(registry, async () => { if (match.command.duplex === true || match.command.duplex === "optional") { if (match.command.duplex === true && !io) throw new Error(`node command requires duplex transport: ${command}`); return invokeContext ? await match.command.handle(paramsJSON, io, invokeContext) : await match.command.handle(paramsJSON, io); } return invokeContext ? await match.command.handle(paramsJSON, void 0, invokeContext) : await match.command.handle(paramsJSON); }); } finally { active = false; } } function isRegisteredNodeHostCommandDuplex(command) { const duplex = (resolveNodeHostPluginRegistry()?.nodeHostCommands ?? []).find((entry) => entry.command.command === command)?.command.duplex; return duplex === true || duplex === "optional"; } function resetNodeHostPluginRegistry() { nodeHostPluginRegistry = void 0; } if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.nodeHostPluginTestApi")] = { getNodeHostPluginRegistry: () => nodeHostPluginRegistry, resetNodeHostPluginRegistry }; //#endregion export { listRegisteredNodeHostCapsAndCommands as a, evaluateSystemRunPolicy as c, isRegisteredNodeHostCommandDuplex as i, resolveExecApprovalDecision as l, hasRegisteredNodeHostCommandActiveWork as n, notifyRegisteredNodeHostCommandDisconnect as o, invokeRegisteredNodeHostCommand as r, watchRegisteredNodeHostCommandAvailability as s, ensureNodeHostPluginRegistry as t, resolveNodeExecConfigPolicy as u };