openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
306 lines (305 loc) • 13.9 kB
JavaScript
import { a as createLazyRuntimeSurface } from "./lazy-runtime-CJrzFzqU.mjs";
import { n as isTruthyEnvValue } from "./env-Bw1K_ti_.mjs";
import { t as createSubsystemLogger } from "./subsystem-BlrgtxyM.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import { A as parseAgentSessionKey } from "./session-key-BC4m_Ly5.mjs";
import "./runtime-env-DMdB1vhM.mjs";
import "./routing-D93XBoA5.mjs";
import { n as registerBrowserCliMetadata } from "./cli-metadata-CWrAnb-_.mjs";
import { $ as getOptionalBrowserStateRuntime, Q as getBrowserStateRuntime, _ as readBrowserDashboardSessionOwners, l as drainBrowserSessionTabStore, p as initializeBrowserSessionTabStore } from "./session-tab-store-CkLpaG2I.mjs";
import { i as SESSION_BROWSER_REQUEST_GATEWAY_METHOD, n as BROWSER_REQUEST_GATEWAY_SCOPE, t as BROWSER_REQUEST_GATEWAY_METHOD } from "./browser-gateway-contract-CxgRVGE3.mjs";
import { n as BROWSER_PROXY_UPLOAD_COMMAND, t as BROWSER_PROXY_COMMAND } from "./browser-node-commands-CIbUPKdY.mjs";
import { t as createBrowserToolDefinition } from "./browser-tool-description-CkCLR4cg.mjs";
import { t as configureSystemProfileImportStateStore } from "./system-profile-import-state-DFy4mHBX.mjs";
import { AsyncLocalStorage } from "node:async_hooks";
//#region extensions/browser/src/browser-dashboard-events.ts
/** One service subscription owns discovery and reconciliation through shutdown. */
function bindBrowserDashboardEvents(events, onWarn) {
const runtime = getBrowserStateRuntime();
const pendingSessions = /* @__PURE__ */ new Set();
const pendingAgentSessions = /* @__PURE__ */ new Set();
let reconciliation;
let accepting = true;
const isCurrentRuntime = () => getOptionalBrowserStateRuntime() === runtime;
const clearPending = () => {
pendingSessions.clear();
pendingAgentSessions.clear();
};
const reconcilePending = () => {
if (reconciliation || pendingSessions.size === 0 || !isCurrentRuntime()) return;
reconciliation = (async () => {
while (pendingSessions.size > 0 && isCurrentRuntime()) {
const sessions = new Set(pendingSessions);
const agentSessions = new Set(pendingAgentSessions);
clearPending();
const dashboards = await readBrowserDashboardSessionOwners();
if (!isCurrentRuntime()) return;
const sessionKeys = /* @__PURE__ */ new Set();
for (const dashboard of dashboards) if (sessions.has(dashboard.sessionKey) || dashboard.agentId && agentSessions.has(JSON.stringify([dashboard.agentId, parseAgentSessionKey(dashboard.sessionKey)?.rest]))) sessionKeys.add(dashboard.sessionKey);
if (sessionKeys.size === 0) continue;
const { reconcileBrowserDashboards } = await import("./browser-dashboard-DHvVnM-l.mjs");
if (!isCurrentRuntime()) return;
await reconcileBrowserDashboards({
sessionKeys: [...sessionKeys],
onWarn
});
}
})().catch((error) => {
clearPending();
onWarn(`Browser dashboard reconciliation failed: ${String(error)}`);
}).finally(() => {
reconciliation = void 0;
if (accepting && isCurrentRuntime()) reconcilePending();
else clearPending();
});
};
runtime.dashboardEvents = events;
const onBoardChanged = (event) => {
if (!accepting || !isCurrentRuntime() || event.reason !== "board") return;
for (const dashboard of runtime.sessionDashboards?.values() ?? []) if (dashboard.definition.sessionKey === event.sessionKey || dashboard.definition.agentId === event.agentId && parseAgentSessionKey(dashboard.definition.sessionKey)?.rest === event.sessionKey) {
dashboard.definitionChanged();
dashboard.assertDefinitionCurrent().catch(() => {});
}
pendingSessions.add(event.sessionKey);
if (event.agentId) pendingAgentSessions.add(JSON.stringify([normalizeAgentId(event.agentId), event.sessionKey]));
reconcilePending();
};
let unsubscribe = events.onSessionsChanged(AsyncLocalStorage.bind(onBoardChanged));
return async () => {
accepting = false;
unsubscribe?.();
unsubscribe = void 0;
if (runtime.dashboardEvents === events) runtime.dashboardEvents = void 0;
await reconciliation;
await Promise.allSettled([...runtime.sessionDashboards?.values() ?? []].map((dashboard) => dashboard.close()));
clearPending();
};
}
//#endregion
//#region extensions/browser/plugin-registration.ts
const EAGER_BROWSER_CONTROL_SERVICE_ENV = "OPENCLAW_EAGER_BROWSER_CONTROL_SERVER";
const logger = createSubsystemLogger("browser");
let hasBrowserNodeHostWork;
let hasBrowserProxyUploadWork;
const loadBrowserRegistrationRuntimeModule = createLazyRuntimeSurface(() => import("./extensions/browser/register.runtime.js"), (runtime) => {
hasBrowserNodeHostWork = runtime.hasBrowserNodeHostWork;
return runtime;
});
const loadBrowserUploadCleanupRuntimeModule = createLazyRuntimeSurface(() => import("./browser-proxy-upload-cleanup.runtime.js"), (runtime) => {
hasBrowserProxyUploadWork = runtime.hasBrowserProxyUploadWork;
return runtime;
});
function deriveChatTypeFromSessionKey(sessionKey) {
const tokens = new Set(sessionKey?.toLowerCase().split(":") ?? []);
if (tokens.has("group")) return "group";
if (tokens.has("channel")) return "channel";
if (tokens.has("direct") || tokens.has("dm")) return "direct";
}
function createLazyBrowserTool(opts, config) {
const { binding, capabilities, metadata } = createBrowserToolDefinition(opts, () => config);
return {
...metadata,
execute: async (toolCallId, args, signal, onUpdate) => {
const { createBrowserTool } = await loadBrowserRegistrationRuntimeModule();
return await createBrowserTool({
...opts,
...binding ? { runToolBinding: binding } : {},
toolCapabilities: capabilities
}).execute(toolCallId, args, signal, onUpdate);
}
};
}
function createBrowserToolOptions(ctx) {
const mediaChannel = ctx.deliveryContext?.channel ?? ctx.messageChannel;
const mediaChatType = deriveChatTypeFromSessionKey(ctx.sessionKey);
return {
...ctx.browser?.sandboxBridgeUrl ? { sandboxBridgeUrl: ctx.browser.sandboxBridgeUrl } : {},
...ctx.browser?.allowHostControl !== void 0 ? { allowHostControl: ctx.browser.allowHostControl } : {},
...ctx.sessionKey ? { agentSessionKey: ctx.sessionKey } : {},
...ctx.agentId ? { agentId: ctx.agentId } : {},
...ctx.agentDir ? { agentDir: ctx.agentDir } : {},
...ctx.workspaceDir ? { workspaceDir: ctx.workspaceDir } : {},
...ctx.activeModel?.provider || ctx.activeModel?.modelId ? { activeModel: {
provider: ctx.activeModel.provider,
model: ctx.activeModel.modelId
} } : {},
...ctx.sessionKey || mediaChannel ? { mediaScope: {
...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {},
...mediaChannel ? { channel: mediaChannel } : {},
...mediaChatType ? { chatType: mediaChatType } : {}
} } : {},
...ctx.toolBindings && Object.hasOwn(ctx.toolBindings, "browser") ? { runToolBinding: ctx.toolBindings.browser } : {}
};
}
/** Browser plugin reload policy. */
const browserPluginReload = {
restartPrefixes: ["browser"],
hotPrefixes: [
"browser.profiles",
"browser.defaultProfile",
"browser.headless",
"browser.executablePath",
"browser.attachOnly",
"browser.cdpUrl",
"browser.noSandbox",
"browser.extraArgs",
"browser.snapshotDefaults",
"browser.tabCleanup",
"browser.allowSystemProfileImport"
]
};
/** Node-host command descriptors exposed by the Browser plugin. */
function createBrowserProxyNodeHostCommand(command) {
return {
command,
cap: "browser",
hasActiveWork: () => loadBrowserRegistrationRuntimeModule.peek() !== void 0 && hasBrowserNodeHostWork?.() !== false || loadBrowserUploadCleanupRuntimeModule.peek() !== void 0 && hasBrowserProxyUploadWork?.() !== false,
isAvailable: ({ config }) => config.browser?.enabled !== false && config.nodeHost?.browserProxy?.enabled !== false,
handle: async (paramsJSON, _io, context) => {
const { runBrowserProxyCommand } = await loadBrowserRegistrationRuntimeModule();
return await runBrowserProxyCommand(paramsJSON, command, context?.signal);
},
...command === "browser.proxy.upload.v1" ? { watchAvailability: () => {
loadBrowserUploadCleanupRuntimeModule().then(({ ensureBrowserProxyUploadCleanup }) => ensureBrowserProxyUploadCleanup()).catch((error) => {
logger.warn(`browser proxy upload cleanup startup failed: ${String(error)}`);
});
} } : {}
};
}
const browserPluginNodeHostCommands = [createBrowserProxyNodeHostCommand(BROWSER_PROXY_COMMAND), createBrowserProxyNodeHostCommand(BROWSER_PROXY_UPLOAD_COMMAND)];
/** Security audit collectors contributed by the Browser plugin. */
const browserSecurityAuditCollectors = [async (ctx) => {
const { collectBrowserSecurityAuditFindings } = await loadBrowserRegistrationRuntimeModule();
return collectBrowserSecurityAuditFindings(ctx);
}];
function createLazyBrowserPluginService(runtime) {
let service = null;
let stopDashboardEvents;
return {
id: "browser-control",
reload: { configPrefixes: [
"browser.enabled",
"browser.evaluateEnabled",
"browser.ssrfPolicy",
"browser.extensionRelay.allowLegacyAuth"
] },
start: async (ctx) => {
await stopDashboardEvents?.();
stopDashboardEvents = ctx.gatewayEvents ? bindBrowserDashboardEvents(ctx.gatewayEvents, (message) => logger.warn(message)) : void 0;
if (!isTruthyEnvValue(process.env[EAGER_BROWSER_CONTROL_SERVICE_ENV])) return;
const { createBrowserPluginService, stopBrowserControlService } = await loadBrowserRegistrationRuntimeModule();
service ??= createBrowserPluginService({ stopOnDemand: stopBrowserControlService });
await service.start(ctx);
},
stop: async (ctx) => {
try {
await stopDashboardEvents?.();
stopDashboardEvents = void 0;
if (!service) {
const loadedRuntime = loadBrowserRegistrationRuntimeModule.peek();
if (!loadedRuntime) return;
const { stopBrowserControlService } = await loadedRuntime;
await stopBrowserControlService();
return;
}
await service.stop?.(ctx);
} finally {
await drainBrowserSessionTabStore(runtime);
}
}
};
}
/** Register Browser tool factories, CLI, gateway methods, services, and audits. */
function registerBrowserPlugin(api) {
const runtime = initializeBrowserSessionTabStore(api.runtime);
api.session.controls.registerControlUiDescriptor({
id: "dashboard",
surface: "widget",
label: "Browser",
description: "An interactive HTTP(S) browser dashboard. Session writers share an isolated session context with the agent's dashboard selector; administrators use the separate managed-profile browser. Author with dashboard widget_put.",
requiredScopes: ["operator.sessions.write"],
schema: {
type: "object",
additionalProperties: false,
required: ["url"],
properties: {
url: {
type: "string",
maxLength: 4096,
description: "HTTP(S) website URL without embedded credentials"
},
profile: {
type: "string",
maxLength: 128,
description: "Administrator-only local managed profile (default openclaw). Session browser uses the configured default profile; omit this property."
}
}
}
});
api.on("session_end", async (event) => {
if (event.reason !== "deleted" || !event.sessionKey || getOptionalBrowserStateRuntime() !== runtime) return;
const dashboards = await readBrowserDashboardSessionOwners();
if (getOptionalBrowserStateRuntime() !== runtime || !dashboards.some((dashboard) => dashboard.sessionKey === event.sessionKey)) return;
const { reconcileBrowserDashboards } = await import("./browser-dashboard-DHvVnM-l.mjs");
if (getOptionalBrowserStateRuntime() !== runtime) return;
await reconcileBrowserDashboards({
sessionKeys: [event.sessionKey],
onWarn: (message) => logger.warn(message)
});
});
configureSystemProfileImportStateStore(api.runtime.state.openKeyedStore({
namespace: "browser.system-profile-import",
maxEntries: 1
}));
api.registerTool((ctx) => {
const config = ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
return createLazyBrowserTool(createBrowserToolOptions(ctx), config);
});
registerBrowserCliMetadata(api);
api.registerGatewayMethod(BROWSER_REQUEST_GATEWAY_METHOD, async (opts) => {
const { handleBrowserGatewayRequest } = await loadBrowserRegistrationRuntimeModule();
return await handleBrowserGatewayRequest(opts);
}, { scope: BROWSER_REQUEST_GATEWAY_SCOPE });
api.registerGatewayMethod(SESSION_BROWSER_REQUEST_GATEWAY_METHOD, async (opts) => {
const { handleSessionBrowserGatewayRequest } = await loadBrowserRegistrationRuntimeModule();
return handleSessionBrowserGatewayRequest(opts);
}, {
scope: "operator.write",
sessionAccess: {
mode: "write",
allowOwnSessionScope: true,
requiredTool: "browser"
}
});
api.registerHttpRoute({
path: "/browser/extension",
auth: "plugin",
match: "exact",
handler: (_req, res) => {
res.writeHead(426, { "Content-Type": "text/plain" });
res.end("Upgrade Required: connect the OpenClaw Chrome extension over WebSocket.");
},
handleUpgrade: async (req, socket, head) => {
await loadBrowserRegistrationRuntimeModule();
const { handleGatewayExtensionUpgrade } = await import("./gateway-relay-route-C6MZ4G1O.mjs");
return await handleGatewayExtensionUpgrade(req, socket, head);
}
});
api.registerHttpRoute({
path: "/browser/screencast",
auth: "plugin",
match: "exact",
handler: (_req, res) => {
res.writeHead(426, { "Content-Type": "text/plain" });
res.end("Upgrade Required: connect the browser screencast over WebSocket.");
},
handleUpgrade: async (req, socket, head) => {
await loadBrowserRegistrationRuntimeModule();
const { handleBrowserScreencastUpgrade } = await import("./upgrade-DEUl4TMl.mjs");
return await handleBrowserScreencastUpgrade(req, socket, head);
}
});
api.registerService(createLazyBrowserPluginService(runtime));
}
//#endregion
export { registerBrowserPlugin as i, browserPluginReload as n, browserSecurityAuditCollectors as r, browserPluginNodeHostCommands as t };