diff --git a/CHANGELOG.md b/CHANGELOG.md index ce244328210..96fce899305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai - Gateway/startup: read embedded-run activity from a lightweight shared state module so restart deferral no longer imports the embedded runner during Gateway boot. Thanks @vincentkoc. - Gateway/startup: defer MCP loopback server imports until Gateway shutdown so normal boot no longer loads the loopback HTTP/tool schema stack just to register close handlers. Thanks @vincentkoc. - Gateway/startup: resolve channel runtime helpers asynchronously only when an enabled/configured channel starts, so no-channel Gateway boot skips auto-reply, media, pairing, and outbound channel helper imports. Thanks @vincentkoc. +- Gateway/startup: lazy-load HTTP auth, canvas auth, and plugin route scope helpers from their request paths so Gateway bind no longer pays those utility graphs during boot. Thanks @vincentkoc. - CLI/Gateway: use a parse-only config snapshot for plain `gateway status` reads and reuse same-path service config context so status no longer spends tens of seconds in full config validation before printing. Thanks @vincentkoc. - Lobster/Gateway: memoize repeated Ajv schema compilation before loading the embedded Lobster runtime so scheduled workflows and `llm.invoke` loops stop growing gateway heap on content-identical schemas. Fixes #71148. Thanks @cmi525, @vsolaz, and @vincentkoc. - Codex harness: normalize cached input tokens before session/context accounting so prompt cache reads are not double-counted in `/status`, `session_status`, or persisted `sessionEntry.totalTokens`. Fixes #69298. Thanks @richardmqq. diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index b7f61637eb0..47214d3cedb 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -8,7 +8,12 @@ import { import { createServer as createHttpsServer } from "node:https"; import type { TlsOptions } from "node:tls"; import type { WebSocketServer } from "ws"; -import { A2UI_PATH, CANVAS_WS_PATH, handleA2uiHttpRequest } from "../canvas-host/a2ui.js"; +import { + A2UI_PATH, + CANVAS_HOST_PATH, + CANVAS_WS_PATH, + handleA2uiHttpRequest, +} from "../canvas-host/a2ui.js"; import type { CanvasHostHandler } from "../canvas-host/server.js"; import { resolveBundledChannelGatewayAuthBypassPaths } from "../channels/plugins/gateway-auth-bypass.js"; import { loadConfig } from "../config/config.js"; @@ -56,17 +61,10 @@ import { resolveHookChannel, resolveHookDeliver, } from "./hooks.js"; -import { - type AuthorizedGatewayHttpRequest, - authorizeGatewayHttpRequestOrReply, - getBearerToken, - resolveHttpBrowserOriginPolicy, -} from "./http-auth-utils.js"; +import type { AuthorizedGatewayHttpRequest } from "./http-auth-utils.js"; import { sendGatewayAuthFailure, setDefaultSecurityHeaders } from "./http-common.js"; import { resolveRequestClientIp } from "./net.js"; import { DEDUPE_MAX, DEDUPE_TTL_MS } from "./server-constants.js"; -import { authorizeCanvasRequest, isCanvasPath } from "./server/http-auth.js"; -import { resolvePluginRouteRuntimeOperatorScopes } from "./server/plugin-route-runtime-scopes.js"; import { isProtectedPluginRoutePathFromContext, resolvePluginRoutePathContext, @@ -109,6 +107,11 @@ let toolsInvokeHttpModulePromise: Promise | undefined; +let canvasAuthModulePromise: Promise | undefined; +let httpAuthUtilsModulePromise: Promise | undefined; +let pluginRouteRuntimeScopesModulePromise: + | Promise + | undefined; function getIdentityAvatarModule() { identityAvatarModulePromise ??= import("../agents/identity-avatar.js"); @@ -165,6 +168,21 @@ function getVoiceClawRealtimeUpgradeModule() { return voiceClawRealtimeUpgradeModulePromise; } +function getCanvasAuthModule() { + canvasAuthModulePromise ??= import("./server/http-auth.js"); + return canvasAuthModulePromise; +} + +function getHttpAuthUtilsModule() { + httpAuthUtilsModulePromise ??= import("./http-auth-utils.js"); + return httpAuthUtilsModulePromise; +} + +function getPluginRouteRuntimeScopesModule() { + pluginRouteRuntimeScopesModulePromise ??= import("./server/plugin-route-runtime-scopes.js"); + return pluginRouteRuntimeScopesModulePromise; +} + type HookDispatchers = { dispatchWakeHook: (value: { text: string; mode: "now" | "next-heartbeat" }) => void; dispatchAgentHook: (value: HookAgentDispatchPayload) => string; @@ -283,6 +301,16 @@ function isA2uiPath(pathname: string): boolean { return pathname === A2UI_PATH || pathname.startsWith(`${A2UI_PATH}/`); } +function isCanvasPath(pathname: string): boolean { + return ( + pathname === A2UI_PATH || + pathname.startsWith(`${A2UI_PATH}/`) || + pathname === CANVAS_HOST_PATH || + pathname.startsWith(`${CANVAS_HOST_PATH}/`) || + pathname === CANVAS_WS_PATH + ); +} + function shouldEnforceDefaultPluginGatewayAuth(pathContext: PluginRoutePathContext): boolean { return ( pathContext.malformedEncoding || @@ -304,6 +332,7 @@ async function canRevealReadinessDetails(params: { return false; } + const { getBearerToken, resolveHttpBrowserOriginPolicy } = await getHttpAuthUtilsModule(); const bearerToken = getBearerToken(params.req); const authResult = await authorizeHttpGatewayConnect({ auth: params.resolvedAuth, @@ -474,6 +503,7 @@ function buildPluginRequestStages(params: { if ((await params.getGatewayAuthBypassPaths()).has(params.requestPath)) { return false; } + const { authorizeGatewayHttpRequestOrReply } = await getHttpAuthUtilsModule(); const requestAuth = await authorizeGatewayHttpRequestOrReply({ req: params.req, res: params.res, @@ -487,6 +517,8 @@ function buildPluginRequestStages(params: { } pluginGatewayAuthSatisfied = true; pluginGatewayRequestAuth = requestAuth; + const { resolvePluginRouteRuntimeOperatorScopes } = + await getPluginRouteRuntimeScopesModule(); pluginRequestOperatorScopes = resolvePluginRouteRuntimeOperatorScopes( params.req, requestAuth, @@ -1092,6 +1124,7 @@ export function createGatewayHttpServer(opts: { if (!isCanvasPath(scopedRequestPath)) { return false; } + const { authorizeCanvasRequest } = await getCanvasAuthModule(); const ok = await authorizeCanvasRequest({ req, auth: resolvedAuth, @@ -1258,6 +1291,7 @@ export function attachGatewayUpgradeHandler(opts: { const url = new URL(req.url ?? "/", "http://localhost"); if (canvasHost) { if (url.pathname === CANVAS_WS_PATH) { + const { authorizeCanvasRequest } = await getCanvasAuthModule(); const ok = await authorizeCanvasRequest({ req, auth: resolvedAuth,