UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

217 lines (216 loc) • 10.5 kB
import { t as createSubsystemLogger } from "./subsystem-I-uqdx4f.mjs"; import { t as normalizeControlUiBasePath } from "./control-ui-shared-DqFhbHR8.mjs"; import { r as getPluginRegistryForContext } from "./gateway-request-scope-BSmCVVpP.mjs"; import { o as getRuntimeConfigSnapshot } from "./runtime-snapshot-DYptB0Cf.mjs"; import { a as READ_SCOPE } from "./operator-scopes-DsrPoRc4.mjs"; import { a as BOARD_REPORT_WIDGET_KIND, n as BOARD_WEBSITE_WIDGET_KIND } from "./board-website-CZkEDDYp.mjs"; import { r as authorizeOperatorScopesForRequiredScope } from "./method-scopes-BrMPjfvW.mjs"; import "./control-ui-contract-B_YMMx3J.mjs"; import { n as resolvePluginRoutePathContext } from "./path-context-D7IHOCQE.mjs"; import { t as controlUiPluginAssetPrefix } from "./control-ui-plugin-assets-contract-BEmf5f0r.mjs"; import { n as findRegisteredPluginHttpRoute, t as findMatchingPluginHttpRoutes } from "./route-match-2DPrkRqp.mjs"; //#region src/gateway/control-ui-plugin-frame-contract.ts /** Lifetime shared by server-minted plugin-tab grants and parent-side renewal. */ const CONTROL_UI_PLUGIN_AUTH_GRANT_TTL_MS = 3e5; /** Reserved query key for the sandbox cookie capability probe. */ const CONTROL_UI_PLUGIN_AUTH_PROBE_QUERY = "__openclaw_plugin_frame_auth_probe"; /** Exact parent origin that may receive the successful probe message. */ const CONTROL_UI_PLUGIN_AUTH_PROBE_ORIGIN_QUERY = "__openclaw_plugin_frame_auth_origin"; /** Message emitted only by a successful sandbox cookie capability probe. */ const CONTROL_UI_PLUGIN_AUTH_PROBE_MESSAGE = "openclaw-plugin-frame-auth-probe"; /** Extracts the same-origin route pathname from a tab descriptor URL. */ function resolveControlUiPluginTabPathname(path) { const baseUrl = "http://openclaw.invalid"; const tabUrl = URL.parse(path, baseUrl); return tabUrl?.origin === baseUrl ? tabUrl.pathname : void 0; } //#endregion //#region src/gateway/control-ui-plugin-policy.ts const CUSTOM_PLUGIN_UI_DISABLED_MESSAGE = "Custom plugin UI is disabled. Enable Custom plugin UI in Settings > Labs."; function isControlUiPluginAllowed(plugin) { return plugin.origin === "bundled" || getRuntimeConfigSnapshot()?.gateway?.controlUi?.experimental?.customPlugins === true; } //#endregion //#region src/gateway/control-ui-plugin-tabs.ts const log = createSubsystemLogger("gateway/control-ui"); const CORE_CONTROL_UI_WIDGET_KINDS = [ { pluginId: "session", kind: "session:progress", label: "Session progress" }, { pluginId: "session", kind: BOARD_REPORT_WIDGET_KIND, label: "Report" }, { pluginId: "session", kind: BOARD_WEBSITE_WIDGET_KIND, label: "Website" } ]; function findControlUiTabGatewayRoute(registry, tab) { if (!tab.path) return; const routePath = resolveControlUiPluginTabPathname(tab.path); if (!routePath) return; const route = findMatchingPluginHttpRoutes(registry, resolvePluginRoutePathContext(routePath)).find((candidate) => candidate.auth === "gateway"); if (!route) return; return route.pluginId === tab.pluginId ? route : null; } function visibleDescriptors(entries, scopes) { return entries.filter(({ descriptor }) => (descriptor.requiredScopes ?? []).every((scope) => authorizeOperatorScopesForRequiredScope(scope, scopes).allowed)); } /** Full descriptors and hello projections share the same scope admission. */ function listControlUiPluginDescriptors(scopes) { return visibleDescriptors(getPluginRegistryForContext()?.controlUiDescriptors ?? [], scopes).map(({ pluginId, pluginName, descriptor }) => ({ pluginId, pluginName, id: descriptor.id, surface: descriptor.surface, linkReader: descriptor.linkReader, label: descriptor.label, description: descriptor.description, placement: descriptor.placement, schema: descriptor.schema, requiredScopes: descriptor.requiredScopes, icon: descriptor.icon, path: descriptor.path, group: descriptor.group, order: descriptor.order })).toSorted((left, right) => left.pluginId.localeCompare(right.pluginId) || left.id.localeCompare(right.id)); } /** Pure projection of tab descriptors visible to the presented scopes. */ function projectControlUiPluginTabs(entries, scopes) { const tabs = []; for (const entry of visibleDescriptors(entries, scopes)) { const descriptor = entry.descriptor; if (descriptor.surface !== "tab") continue; tabs.push({ pluginId: entry.pluginId, id: descriptor.id, label: descriptor.label, description: descriptor.description, icon: descriptor.icon, path: descriptor.path, placement: descriptor.placement, ...descriptor.slug ? { slug: descriptor.slug } : {}, group: descriptor.group, order: descriptor.order }); } return tabs.toSorted((left, right) => (left.order ?? 0) - (right.order ?? 0) || left.label.localeCompare(right.label) || left.id.localeCompare(right.id)); } /** Lists active plugins' tab descriptors visible to the presented scopes. */ function listControlUiPluginTabs(scopes, opts = {}) { const registry = getPluginRegistryForContext(); const basePath = normalizeControlUiBasePath(getRuntimeConfigSnapshot()?.gateway?.controlUi?.basePath); return projectControlUiPluginTabs(registry?.controlUiDescriptors ?? [], scopes).flatMap((tab) => { const route = registry ? findControlUiTabGatewayRoute(registry, tab) : void 0; if (route === null) return []; if (registry && tab.slug) { const pathname = `${basePath}/${tab.slug}`; const shadow = findRegisteredPluginHttpRoute(registry, pathname); if (shadow) { const message = `Control UI tab slug ${pathname} is shadowed by plugin HTTP route ${shadow.pluginId}:${shadow.path}; using the generic tab URL for ${tab.pluginId}:${tab.id}`; if (!registry.diagnostics.some((diagnostic) => diagnostic.message === message)) { registry.diagnostics.push({ level: "warn", pluginId: tab.pluginId, message }); log.warn(message); } delete tab.slug; } } return route && opts.requireGatewayAuthGrant !== false ? [{ ...tab, requiresGatewayAuth: true }] : [tab]; }); } /** Lists active plugins' trusted widget kinds visible to the presented scopes. */ function listControlUiPluginWidgetKinds(scopes) { const registry = getPluginRegistryForContext(); const entries = registry?.controlUiDescriptors ?? []; const disabled = new Set(registry?.plugins.filter((plugin) => plugin.controlUi && !isControlUiPluginAllowed(plugin)).map((plugin) => plugin.id)); const coreEntries = authorizeOperatorScopesForRequiredScope("operator.read", scopes).allowed ? CORE_CONTROL_UI_WIDGET_KINDS : []; const pluginEntries = visibleDescriptors(entries, scopes).flatMap((entry) => { const descriptor = entry.descriptor; if (descriptor.surface !== "widget" || disabled.has(entry.pluginId)) return []; return [{ pluginId: entry.pluginId, kind: `${entry.pluginId}:${descriptor.id}`, label: descriptor.label }]; }); return [...coreEntries, ...pluginEntries].toSorted((left, right) => left.label.localeCompare(right.label) || left.kind.localeCompare(right.kind)); } /** Grants read access to active native assets and visible same-plugin Gateway tabs. */ function listControlUiPluginTabAuthGrants(callerScopes) { const registry = getPluginRegistryForContext(); if (!registry || !authorizeOperatorScopesForRequiredScope("operator.read", callerScopes).allowed) return []; const grants = /* @__PURE__ */ new Map(); const basePath = getRuntimeConfigSnapshot()?.gateway?.controlUi?.basePath; for (const plugin of registry.plugins) { if (!plugin.enabled || plugin.status !== "loaded" || !plugin.controlUi || !isControlUiPluginAllowed(plugin)) continue; const assetPath = controlUiPluginAssetPrefix(plugin.id, basePath); grants.set(`${plugin.id}\n${assetPath}`, { pluginId: plugin.id, path: assetPath, match: "prefix", scopes: [READ_SCOPE] }); } for (const tab of projectControlUiPluginTabs(registry.controlUiDescriptors ?? [], callerScopes)) { if (!tab.path) continue; const route = findControlUiTabGatewayRoute(registry, tab); if (!route) continue; const key = `${tab.pluginId}\n${route.path}`; const existing = grants.get(key); if (existing) { if (existing.match === "exact" && route.match === "prefix") grants.set(key, { ...existing, match: "prefix" }); continue; } grants.set(key, { pluginId: tab.pluginId, path: route.path, match: route.match, scopes: [READ_SCOPE] }); } return [...grants.values()]; } /** Reader selection shares the request's actual dispatch snapshot and scope admission. */ function listControlUiLinkReaders(scopes, methods) { const registry = getPluginRegistryForContext(); if (!registry || !methods || !authorizeOperatorScopesForRequiredScope("operator.read", scopes).allowed) return []; if (methods.pluginRegistry && methods.pluginRegistry !== registry) return []; const loaded = new Set(registry.plugins.filter((plugin) => plugin.enabled && plugin.status === "loaded").map((plugin) => plugin.id)); const descriptors = new Map(methods.descriptors().map((method) => [method.name, method])); const readable = (name, pluginId) => { const method = descriptors.get(name); return method?.owner.kind === "plugin" && method.owner.pluginId === pluginId && method.scope === "operator.read" && method.advertise !== false && !method.controlPlaneWrite; }; return visibleDescriptors(registry.controlUiDescriptors, scopes).flatMap((entry) => { const descriptor = entry.descriptor; const metadata = descriptor.linkReader; if (descriptor.surface !== "link-reader" || !metadata || !loaded.has(entry.pluginId) || !readable(metadata.detailMethod, entry.pluginId) || metadata.previewMethod && !readable(metadata.previewMethod, entry.pluginId) || metadata.imageMethod && !readable(metadata.imageMethod, entry.pluginId)) return []; return [{ pluginId: entry.pluginId, id: descriptor.id, label: descriptor.label, icon: descriptor.icon, linkReader: { ...metadata, hosts: [...metadata.hosts] } }]; }).toSorted((left, right) => left.pluginId.localeCompare(right.pluginId) || left.id.localeCompare(right.id)); } //#endregion export { listControlUiPluginWidgetKinds as a, CONTROL_UI_PLUGIN_AUTH_GRANT_TTL_MS as c, CONTROL_UI_PLUGIN_AUTH_PROBE_QUERY as d, listControlUiPluginTabs as i, CONTROL_UI_PLUGIN_AUTH_PROBE_MESSAGE as l, listControlUiPluginDescriptors as n, CUSTOM_PLUGIN_UI_DISABLED_MESSAGE as o, listControlUiPluginTabAuthGrants as r, isControlUiPluginAllowed as s, listControlUiLinkReaders as t, CONTROL_UI_PLUGIN_AUTH_PROBE_ORIGIN_QUERY as u };