openclaw/src/gateway/server.impl.ts

1103 lines
39 KiB
TypeScript
Raw Normal View History

import path from "node:path";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { getActiveEmbeddedRunCount } from "../agents/pi-embedded-runner/runs.js";
import { registerSkillsChangeListener } from "../agents/skills/refresh.js";
import { initSubagentRegistry } from "../agents/subagent-registry.js";
import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js";
import type { CanvasHostServer } from "../canvas-host/server.js";
import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js";
2026-01-20 07:42:21 +00:00
import { formatCliCommand } from "../cli/command-format.js";
import { createDefaultDeps } from "../cli/deps.js";
import { isRestartEnabled } from "../config/commands.js";
2026-01-14 01:08:15 +00:00
import {
CONFIG_PATH,
type OpenClawConfig,
2026-01-14 01:08:15 +00:00
isNixMode,
loadConfig,
migrateLegacyConfig,
readConfigFileSnapshot,
writeConfigFile,
} from "../config/config.js";
import { formatConfigIssueLines } from "../config/issue-format.js";
2026-01-20 16:37:34 +00:00
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { resolveMainSessionKey } from "../config/sessions.js";
import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
import {
ensureControlUiAssetsBuilt,
isPackageProvenControlUiRootSync,
resolveControlUiRootOverrideSync,
resolveControlUiRootSync,
} from "../infra/control-ui-assets.js";
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
import { logAcceptedEnvOption } from "../infra/env.js";
import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js";
2026-01-14 01:08:15 +00:00
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js";
2026-01-14 01:08:15 +00:00
import { getMachineDisplayName } from "../infra/machine-name.js";
2026-01-30 03:15:10 +01:00
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
import { setGatewaySigusr1RestartPolicy, setPreRestartDeferralCheck } from "../infra/restart.js";
import {
primeRemoteSkillsCache,
refreshRemoteBinsForConnectedNodes,
2026-01-19 04:50:07 +00:00
setSkillsRemoteRegistry,
} from "../infra/skills-remote.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
2026-01-21 00:29:42 +00:00
import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/diagnostic.js";
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
import { getGlobalHookRunner, runGlobalGatewayStopSafely } from "../plugins/hook-runner-global.js";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
feat(plugin-sdk): Add channelRuntime support for external channel plugins ## Overview This PR enables external channel plugins (loaded via Plugin SDK) to access advanced runtime features like AI response dispatching, which were previously only available to built-in channels. ## Changes ### src/gateway/server-channels.ts - Import PluginRuntime type - Add optional channelRuntime parameter to ChannelManagerOptions - Pass channelRuntime to channel startAccount calls via conditional spread - Ensures backward compatibility (field is optional) ### src/gateway/server.impl.ts - Import createPluginRuntime from plugins/runtime - Create and pass channelRuntime to channel manager ### src/channels/plugins/types.adapters.ts - Import PluginRuntime type - Add comprehensive documentation for channelRuntime field - Document available features, use cases, and examples - Improve type safety (use imported PluginRuntime type vs inline import) ## Benefits External channel plugins can now: - Generate AI-powered responses using dispatchReplyWithBufferedBlockDispatcher - Access routing, text processing, and session management utilities - Use command authorization and group policy resolution - Maintain feature parity with built-in channels ## Backward Compatibility - channelRuntime field is optional in ChannelGatewayContext - Conditional spread ensures it's only passed when explicitly provided - Existing channels without channelRuntime support continue to work unchanged - No breaking changes to channel plugin API ## Testing - Email channel plugin successfully uses channelRuntime for AI responses - All existing built-in channels (slack, discord, telegram, etc.) work unchanged - Gateway loads and runs without errors when channelRuntime is provided
2026-02-24 21:51:41 +08:00
import { createPluginRuntime } from "../plugins/runtime/index.js";
import type { PluginServicesHandle } from "../plugins/services.js";
import { getTotalQueueSize } from "../process/command-queue.js";
import type { RuntimeEnv } from "../runtime.js";
feat(secrets): expand SecretRef coverage across user-supplied credentials (#29580) * feat(secrets): expand secret target coverage and gateway tooling * docs(secrets): align gateway and CLI secret docs * chore(protocol): regenerate swift gateway models for secrets methods * fix(config): restore talk apiKey fallback and stabilize runner test * ci(windows): reduce test worker count for shard stability * ci(windows): raise node heap for test shard stability * test(feishu): make proxy env precedence assertion windows-safe * fix(gateway): resolve auth password SecretInput refs for clients * fix(gateway): resolve remote SecretInput credentials for clients * fix(secrets): skip inactive refs in command snapshot assignments * fix(secrets): scope gateway.remote refs to effective auth surfaces * fix(secrets): ignore memory defaults when enabled agents disable search * fix(secrets): honor Google Chat serviceAccountRef inheritance * fix(secrets): address tsgo errors in command and gateway collectors * fix(secrets): avoid auth-store load in providers-only configure * fix(gateway): defer local password ref resolution by precedence * fix(secrets): gate telegram webhook secret refs by webhook mode * fix(secrets): gate slack signing secret refs to http mode * fix(secrets): skip telegram botToken refs when tokenFile is set * fix(secrets): gate discord pluralkit refs by enabled flag * fix(secrets): gate discord voice tts refs by voice enabled * test(secrets): make runtime fixture modes explicit * fix(cli): resolve local qr password secret refs * fix(cli): fail when gateway leaves command refs unresolved * fix(gateway): fail when local password SecretRef is unresolved * fix(gateway): fail when required remote SecretRefs are unresolved * fix(gateway): resolve local password refs only when password can win * fix(cli): skip local password SecretRef resolution on qr token override * test(gateway): cast SecretRef fixtures to OpenClawConfig * test(secrets): activate mode-gated targets in runtime coverage fixture * fix(cron): support SecretInput webhook tokens safely * fix(bluebubbles): support SecretInput passwords across config paths * fix(msteams): make appPassword SecretInput-safe in onboarding/token paths * fix(bluebubbles): align SecretInput schema helper typing * fix(cli): clarify secrets.resolve version-skew errors * refactor(secrets): return structured inactive paths from secrets.resolve * refactor(gateway): type onboarding secret writes as SecretInput * chore(protocol): regenerate swift models for secrets.resolve * feat(secrets): expand extension credential secretref support * fix(secrets): gate web-search refs by active provider * fix(onboarding): detect SecretRef credentials in extension status * fix(onboarding): allow keeping existing ref in secret prompt * fix(onboarding): resolve gateway password SecretRefs for probe and tui * fix(onboarding): honor secret-input-mode for local gateway auth * fix(acp): resolve gateway SecretInput credentials * fix(secrets): gate gateway.remote refs to remote surfaces * test(secrets): cover pattern matching and inactive array refs * docs(secrets): clarify secrets.resolve and remote active surfaces * fix(bluebubbles): keep existing SecretRef during onboarding * fix(tests): resolve CI type errors in new SecretRef coverage * fix(extensions): replace raw fetch with SSRF-guarded fetch * test(secrets): mark gateway remote targets active in runtime coverage * test(infra): normalize home-prefix expectation across platforms * fix(cli): only resolve local qr password refs in password mode * test(cli): cover local qr token mode with unresolved password ref * docs(cli): clarify local qr password ref resolution behavior * refactor(extensions): reuse sdk SecretInput helpers * fix(wizard): resolve onboarding env-template secrets before plaintext * fix(cli): surface secrets.resolve diagnostics in memory and qr * test(secrets): repair post-rebase runtime and fixtures * fix(gateway): skip remote password ref resolution when token wins * fix(secrets): treat tailscale remote gateway refs as active * fix(gateway): allow remote password fallback when token ref is unresolved * fix(gateway): ignore stale local password refs for none and trusted-proxy * fix(gateway): skip remote secret ref resolution on local call paths * test(cli): cover qr remote tailscale secret ref resolution * fix(secrets): align gateway password active-surface with auth inference * fix(cli): resolve inferred local gateway password refs in qr * fix(gateway): prefer resolvable remote password over token ref pre-resolution * test(gateway): cover none and trusted-proxy stale password refs * docs(secrets): sync qr and gateway active-surface behavior * fix: restore stability blockers from pre-release audit * Secrets: fix collector/runtime precedence contradictions * docs: align secrets and web credential docs * fix(rebase): resolve integration regressions after main rebase * fix(node-host): resolve gateway secret refs for auth * fix(secrets): harden secretinput runtime readers * gateway: skip inactive auth secretref resolution * cli: avoid gateway preflight for inactive secret refs * extensions: allow unresolved refs in onboarding status * tests: fix qr-cli module mock hoist ordering * Security: align audit checks with SecretInput resolution * Gateway: resolve local-mode remote fallback secret refs * Node host: avoid resolving inactive password secret refs * Secrets runtime: mark Slack appToken inactive for HTTP mode * secrets: keep inactive gateway remote refs non-blocking * cli: include agent memory secret targets in runtime resolution * docs(secrets): sync docs with active-surface and web search behavior * fix(secrets): keep telegram top-level token refs active for blank account tokens * fix(daemon): resolve gateway password secret refs for probe auth * fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled * fix(secrets): align token inheritance and exec timeout defaults * docs(secrets): clarify active-surface notes in cli docs * cli: require secrets.resolve gateway capability * gateway: log auth secret surface diagnostics * secrets: remove dead provider resolver module * fix(secrets): restore gateway auth precedence and fallback resolution * fix(tests): align plugin runtime mock typings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-03-02 20:58:20 -06:00
import type { CommandSecretAssignment } from "../secrets/command-config.js";
import {
GATEWAY_AUTH_SURFACE_PATHS,
evaluateGatewayAuthSurfaceStates,
} from "../secrets/runtime-gateway-auth-surfaces.js";
import {
activateSecretsRuntimeSnapshot,
clearSecretsRuntimeSnapshot,
getActiveSecretsRuntimeSnapshot,
prepareSecretsRuntimeSnapshot,
feat(secrets): expand SecretRef coverage across user-supplied credentials (#29580) * feat(secrets): expand secret target coverage and gateway tooling * docs(secrets): align gateway and CLI secret docs * chore(protocol): regenerate swift gateway models for secrets methods * fix(config): restore talk apiKey fallback and stabilize runner test * ci(windows): reduce test worker count for shard stability * ci(windows): raise node heap for test shard stability * test(feishu): make proxy env precedence assertion windows-safe * fix(gateway): resolve auth password SecretInput refs for clients * fix(gateway): resolve remote SecretInput credentials for clients * fix(secrets): skip inactive refs in command snapshot assignments * fix(secrets): scope gateway.remote refs to effective auth surfaces * fix(secrets): ignore memory defaults when enabled agents disable search * fix(secrets): honor Google Chat serviceAccountRef inheritance * fix(secrets): address tsgo errors in command and gateway collectors * fix(secrets): avoid auth-store load in providers-only configure * fix(gateway): defer local password ref resolution by precedence * fix(secrets): gate telegram webhook secret refs by webhook mode * fix(secrets): gate slack signing secret refs to http mode * fix(secrets): skip telegram botToken refs when tokenFile is set * fix(secrets): gate discord pluralkit refs by enabled flag * fix(secrets): gate discord voice tts refs by voice enabled * test(secrets): make runtime fixture modes explicit * fix(cli): resolve local qr password secret refs * fix(cli): fail when gateway leaves command refs unresolved * fix(gateway): fail when local password SecretRef is unresolved * fix(gateway): fail when required remote SecretRefs are unresolved * fix(gateway): resolve local password refs only when password can win * fix(cli): skip local password SecretRef resolution on qr token override * test(gateway): cast SecretRef fixtures to OpenClawConfig * test(secrets): activate mode-gated targets in runtime coverage fixture * fix(cron): support SecretInput webhook tokens safely * fix(bluebubbles): support SecretInput passwords across config paths * fix(msteams): make appPassword SecretInput-safe in onboarding/token paths * fix(bluebubbles): align SecretInput schema helper typing * fix(cli): clarify secrets.resolve version-skew errors * refactor(secrets): return structured inactive paths from secrets.resolve * refactor(gateway): type onboarding secret writes as SecretInput * chore(protocol): regenerate swift models for secrets.resolve * feat(secrets): expand extension credential secretref support * fix(secrets): gate web-search refs by active provider * fix(onboarding): detect SecretRef credentials in extension status * fix(onboarding): allow keeping existing ref in secret prompt * fix(onboarding): resolve gateway password SecretRefs for probe and tui * fix(onboarding): honor secret-input-mode for local gateway auth * fix(acp): resolve gateway SecretInput credentials * fix(secrets): gate gateway.remote refs to remote surfaces * test(secrets): cover pattern matching and inactive array refs * docs(secrets): clarify secrets.resolve and remote active surfaces * fix(bluebubbles): keep existing SecretRef during onboarding * fix(tests): resolve CI type errors in new SecretRef coverage * fix(extensions): replace raw fetch with SSRF-guarded fetch * test(secrets): mark gateway remote targets active in runtime coverage * test(infra): normalize home-prefix expectation across platforms * fix(cli): only resolve local qr password refs in password mode * test(cli): cover local qr token mode with unresolved password ref * docs(cli): clarify local qr password ref resolution behavior * refactor(extensions): reuse sdk SecretInput helpers * fix(wizard): resolve onboarding env-template secrets before plaintext * fix(cli): surface secrets.resolve diagnostics in memory and qr * test(secrets): repair post-rebase runtime and fixtures * fix(gateway): skip remote password ref resolution when token wins * fix(secrets): treat tailscale remote gateway refs as active * fix(gateway): allow remote password fallback when token ref is unresolved * fix(gateway): ignore stale local password refs for none and trusted-proxy * fix(gateway): skip remote secret ref resolution on local call paths * test(cli): cover qr remote tailscale secret ref resolution * fix(secrets): align gateway password active-surface with auth inference * fix(cli): resolve inferred local gateway password refs in qr * fix(gateway): prefer resolvable remote password over token ref pre-resolution * test(gateway): cover none and trusted-proxy stale password refs * docs(secrets): sync qr and gateway active-surface behavior * fix: restore stability blockers from pre-release audit * Secrets: fix collector/runtime precedence contradictions * docs: align secrets and web credential docs * fix(rebase): resolve integration regressions after main rebase * fix(node-host): resolve gateway secret refs for auth * fix(secrets): harden secretinput runtime readers * gateway: skip inactive auth secretref resolution * cli: avoid gateway preflight for inactive secret refs * extensions: allow unresolved refs in onboarding status * tests: fix qr-cli module mock hoist ordering * Security: align audit checks with SecretInput resolution * Gateway: resolve local-mode remote fallback secret refs * Node host: avoid resolving inactive password secret refs * Secrets runtime: mark Slack appToken inactive for HTTP mode * secrets: keep inactive gateway remote refs non-blocking * cli: include agent memory secret targets in runtime resolution * docs(secrets): sync docs with active-surface and web search behavior * fix(secrets): keep telegram top-level token refs active for blank account tokens * fix(daemon): resolve gateway password secret refs for probe auth * fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled * fix(secrets): align token inheritance and exec timeout defaults * docs(secrets): clarify active-surface notes in cli docs * cli: require secrets.resolve gateway capability * gateway: log auth secret surface diagnostics * secrets: remove dead provider resolver module * fix(secrets): restore gateway auth precedence and fallback resolution * fix(tests): align plugin runtime mock typings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-03-02 20:58:20 -06:00
resolveCommandSecretsFromActiveRuntimeSnapshot,
} from "../secrets/runtime.js";
import { onSessionTranscriptUpdate } from "../sessions/transcript-events.js";
2026-01-14 01:08:15 +00:00
import { runOnboardingWizard } from "../wizard/onboarding.js";
import { createAuthRateLimiter, type AuthRateLimiter } from "./auth-rate-limit.js";
import { startChannelHealthMonitor } from "./channel-health-monitor.js";
import { startGatewayConfigReloader } from "./config-reload.js";
import type { ControlUiRootState } from "./control-ui.js";
import {
GATEWAY_EVENT_UPDATE_AVAILABLE,
type GatewayUpdateAvailableEventPayload,
} from "./events.js";
2026-01-19 02:31:18 +00:00
import { ExecApprovalManager } from "./exec-approval-manager.js";
import { NodeRegistry } from "./node-registry.js";
import type { startBrowserControlServerIfEnabled } from "./server-browser.js";
2026-01-14 01:08:15 +00:00
import { createChannelManager } from "./server-channels.js";
import { createAgentEventHandler, createSessionEventSubscriberRegistry } from "./server-chat.js";
import { createGatewayCloseHandler } from "./server-close.js";
import { buildGatewayCronService } from "./server-cron.js";
import { startGatewayDiscovery } from "./server-discovery-runtime.js";
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
2026-01-15 02:42:41 +00:00
import { GATEWAY_EVENTS, listGatewayMethods } from "./server-methods-list.js";
import { coreGatewayHandlers } from "./server-methods.js";
import { createExecApprovalHandlers } from "./server-methods/exec-approval.js";
import { safeParseJson } from "./server-methods/nodes.helpers.js";
import { createSecretsHandlers } from "./server-methods/secrets.js";
import { hasConnectedMobileNode } from "./server-mobile-nodes.js";
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
2026-01-19 04:50:07 +00:00
import { createNodeSubscriptionManager } from "./server-node-subscriptions.js";
feature(context): extend plugin system to support custom context management (#22201) * feat(context-engine): add ContextEngine interface and registry Introduce the pluggable ContextEngine abstraction that allows external plugins to register custom context management strategies. - ContextEngine interface with lifecycle methods: bootstrap, ingest, ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn, onSubagentEnded, dispose - Module-level singleton registry with registerContextEngine() and resolveContextEngine() (config-driven slot selection) - LegacyContextEngine: pass-through implementation wrapping existing compaction behavior for 100% backward compatibility - ensureContextEnginesInitialized() guard for safe one-time registration - 19 tests covering contract, registry, resolution, and legacy parity * feat(plugins): add context-engine slot and registerContextEngine API Wire the ContextEngine abstraction into the plugin system so external plugins can register context engines via the standard plugin API. - Add 'context-engine' to PluginKind union type - Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy') - Wire registerContextEngine() through OpenClawPluginApi - Export ContextEngine types from plugin-sdk for external consumers - Restore proper slot-based resolution in registry * feat(context-engine): wire ContextEngine into agent run lifecycle Integrate the ContextEngine abstraction into the core agent run path: - Resolve context engine once per run (reused across retries) - Bootstrap: hydrate canonical store from session file on first run - Assemble: route context assembly through pluggable engine - Auto-compaction guard: disable built-in auto-compaction when the engine declares ownsCompaction (prevents double-compaction) - AfterTurn: post-turn lifecycle hook for ingest + background compaction decisions - Overflow compaction: route through contextEngine.compact() - Dispose: clean up engine resources in finally block - Notify context engine on subagent lifecycle events Legacy engine: all lifecycle methods are pass-through/no-op, preserving 100% backward compatibility for users without a context engine plugin. * feat(plugins): add scoped subagent methods and gateway request scope Expose runtime.subagent.{run, waitForRun, getSession, deleteSession} so external plugins can spawn sub-agent sessions without raw gateway dispatch access. Uses AsyncLocalStorage request-scope bridge to dispatch internally via handleGatewayRequest with a synthetic operator client. Methods are only available during gateway request handling. - Symbol.for-backed global singleton for cross-module-reload safety - Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp) - Set gateway request scope for all handlers, not just plugin handlers - 3 staleness tests for fallback context hardening * feat(context-engine): route /compact and sessions.get through context engine Wire the /compact command and sessions.get handler through the pluggable ContextEngine interface. - Thread tokenBudget and force parameters to context engine compact - Route /compact through contextEngine.compact() when registered - Wire sessions.get as runtime alias for plugin subagent dispatch - Add .pebbles/ to .gitignore * style: format with oxfmt 0.33.0 Fix duplicate import (ControlUiRootState in server.impl.ts) and import ordering across all changed files. * fix: update extension test mocks for context-engine types Add missing subagent property to bluebubbles PluginRuntime mock. Add missing registerContextEngine to lobster OpenClawPluginApi mock. * fix(subagents): keep deferred delete cleanup retryable * style: format run attempt for CI * fix(rebase): remove duplicate embedded-run imports * test: add missing gateway context mock export * fix: pass resolved auth profile into afterTurn compaction Ensure the embedded runner forwards resolved auth profile context into legacy context-engine compaction params on the normal afterTurn path, matching overflow compaction behavior. This allows downstream LCM summarization to use the intended provider auth/profile consistently. Also fix strict TS typing in external-link token dedupe and align an attempt unit test reasoningLevel value with the current ReasoningLevel enum. Regeneration-Prompt: | We were debugging context-engine compaction where downstream summary calls were missing the right auth/profile context in normal afterTurn flow, while overflow compaction already propagated it. Preserve current behavior and keep changes additive: thread the resolved authProfileId through run -> attempt -> legacy compaction param builder without broad refactors. Add tests that prove the auth profile is included in afterTurn legacy params and that overflow compaction still passes it through run attempts. Keep existing APIs stable, and only adjust small type issues needed for strict compilation. * fix: remove duplicate imports from rebase * feat: add context-engine system prompt additions * fix(rebase): dedupe attempt import declarations * test: fix fetch mock typing in ollama autodiscovery * fix(test): add registerContextEngine to diffs extension mock APIs * test(windows): use path.delimiter in ios-team-id fixture PATH * test(cron): add model formatting and precedence edge case tests Covers: - Provider/model string splitting (whitespace, nested paths, empty segments) - Provider normalization (casing, aliases like bedrock→amazon-bedrock) - Anthropic model alias normalization (opus-4.5→claude-opus-4-5) - Precedence: job payload > session override > config default - Sequential runs with different providers (CI flake regression pattern) - forceNew session preserving stored model overrides - Whitespace/empty model string edge cases - Config model as string vs object format * test(cron): fix model formatting test config types * test(phone-control): add registerContextEngine to mock API * fix: re-export ChannelKind from config-reload-plan * fix: add subagent mock to plugin-runtime-mock test util * docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
import { loadGatewayPlugins, setFallbackGatewayContext } from "./server-plugins.js";
import { createGatewayReloadHandlers } from "./server-reload-handlers.js";
import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js";
import { createGatewayRuntimeState } from "./server-runtime-state.js";
import { resolveSessionKeyForRun } from "./server-session-key.js";
import { logGatewayStartup } from "./server-startup-log.js";
import { startGatewaySidecars } from "./server-startup.js";
import { startGatewayTailscaleExposure } from "./server-tailscale.js";
import { createWizardSessionTracker } from "./server-wizard-sessions.js";
import { attachGatewayWsHandlers } from "./server-ws-runtime.js";
import {
getHealthCache,
getHealthVersion,
getPresenceVersion,
incrementPresenceVersion,
refreshGatewayHealthSnapshot,
} from "./server/health-state.js";
import { resolveHookClientIpConfig } from "./server/hooks.js";
import { createReadinessChecker } from "./server/readiness.js";
import { loadGatewayTlsRuntime } from "./server/tls.js";
import { resolveSessionKeyForTranscriptFile } from "./session-transcript-key.js";
import {
ensureGatewayStartupAuth,
mergeGatewayAuthConfig,
mergeGatewayTailscaleConfig,
} from "./startup-auth.js";
import { maybeSeedControlUiAllowedOriginsAtStartup } from "./startup-control-ui-origins.js";
export { __resetModelCatalogCacheForTest } from "./server-model-catalog.js";
2026-01-14 01:08:15 +00:00
2026-01-30 03:15:10 +01:00
ensureOpenClawCliOnPath();
2026-01-14 01:08:15 +00:00
const MAX_MEDIA_TTL_HOURS = 24 * 7;
function resolveMediaCleanupTtlMs(ttlHoursRaw: number): number {
const ttlHours = Math.min(Math.max(ttlHoursRaw, 1), MAX_MEDIA_TTL_HOURS);
const ttlMs = ttlHours * 60 * 60_000;
if (!Number.isFinite(ttlMs) || !Number.isSafeInteger(ttlMs)) {
throw new Error(`Invalid media.ttlHours: ${String(ttlHoursRaw)}`);
}
return ttlMs;
}
2026-01-14 01:08:15 +00:00
const log = createSubsystemLogger("gateway");
const logCanvas = log.child("canvas");
const logDiscovery = log.child("discovery");
const logTailscale = log.child("tailscale");
const logChannels = log.child("channels");
const logBrowser = log.child("browser");
const logHealth = log.child("health");
const logCron = log.child("cron");
const logReload = log.child("reload");
const logHooks = log.child("hooks");
const logPlugins = log.child("plugins");
2026-01-14 01:08:15 +00:00
const logWsControl = log.child("ws");
const logSecrets = log.child("secrets");
const gatewayRuntime = runtimeForLogger(log);
2026-01-14 01:08:15 +00:00
const canvasRuntime = runtimeForLogger(logCanvas);
type AuthRateLimitConfig = Parameters<typeof createAuthRateLimiter>[0];
function createGatewayAuthRateLimiters(rateLimitConfig: AuthRateLimitConfig | undefined): {
rateLimiter?: AuthRateLimiter;
browserRateLimiter: AuthRateLimiter;
} {
const rateLimiter = rateLimitConfig ? createAuthRateLimiter(rateLimitConfig) : undefined;
// Browser-origin WS auth attempts always use loopback-non-exempt throttling.
const browserRateLimiter = createAuthRateLimiter({
...rateLimitConfig,
exemptLoopback: false,
});
return { rateLimiter, browserRateLimiter };
}
feat(secrets): expand SecretRef coverage across user-supplied credentials (#29580) * feat(secrets): expand secret target coverage and gateway tooling * docs(secrets): align gateway and CLI secret docs * chore(protocol): regenerate swift gateway models for secrets methods * fix(config): restore talk apiKey fallback and stabilize runner test * ci(windows): reduce test worker count for shard stability * ci(windows): raise node heap for test shard stability * test(feishu): make proxy env precedence assertion windows-safe * fix(gateway): resolve auth password SecretInput refs for clients * fix(gateway): resolve remote SecretInput credentials for clients * fix(secrets): skip inactive refs in command snapshot assignments * fix(secrets): scope gateway.remote refs to effective auth surfaces * fix(secrets): ignore memory defaults when enabled agents disable search * fix(secrets): honor Google Chat serviceAccountRef inheritance * fix(secrets): address tsgo errors in command and gateway collectors * fix(secrets): avoid auth-store load in providers-only configure * fix(gateway): defer local password ref resolution by precedence * fix(secrets): gate telegram webhook secret refs by webhook mode * fix(secrets): gate slack signing secret refs to http mode * fix(secrets): skip telegram botToken refs when tokenFile is set * fix(secrets): gate discord pluralkit refs by enabled flag * fix(secrets): gate discord voice tts refs by voice enabled * test(secrets): make runtime fixture modes explicit * fix(cli): resolve local qr password secret refs * fix(cli): fail when gateway leaves command refs unresolved * fix(gateway): fail when local password SecretRef is unresolved * fix(gateway): fail when required remote SecretRefs are unresolved * fix(gateway): resolve local password refs only when password can win * fix(cli): skip local password SecretRef resolution on qr token override * test(gateway): cast SecretRef fixtures to OpenClawConfig * test(secrets): activate mode-gated targets in runtime coverage fixture * fix(cron): support SecretInput webhook tokens safely * fix(bluebubbles): support SecretInput passwords across config paths * fix(msteams): make appPassword SecretInput-safe in onboarding/token paths * fix(bluebubbles): align SecretInput schema helper typing * fix(cli): clarify secrets.resolve version-skew errors * refactor(secrets): return structured inactive paths from secrets.resolve * refactor(gateway): type onboarding secret writes as SecretInput * chore(protocol): regenerate swift models for secrets.resolve * feat(secrets): expand extension credential secretref support * fix(secrets): gate web-search refs by active provider * fix(onboarding): detect SecretRef credentials in extension status * fix(onboarding): allow keeping existing ref in secret prompt * fix(onboarding): resolve gateway password SecretRefs for probe and tui * fix(onboarding): honor secret-input-mode for local gateway auth * fix(acp): resolve gateway SecretInput credentials * fix(secrets): gate gateway.remote refs to remote surfaces * test(secrets): cover pattern matching and inactive array refs * docs(secrets): clarify secrets.resolve and remote active surfaces * fix(bluebubbles): keep existing SecretRef during onboarding * fix(tests): resolve CI type errors in new SecretRef coverage * fix(extensions): replace raw fetch with SSRF-guarded fetch * test(secrets): mark gateway remote targets active in runtime coverage * test(infra): normalize home-prefix expectation across platforms * fix(cli): only resolve local qr password refs in password mode * test(cli): cover local qr token mode with unresolved password ref * docs(cli): clarify local qr password ref resolution behavior * refactor(extensions): reuse sdk SecretInput helpers * fix(wizard): resolve onboarding env-template secrets before plaintext * fix(cli): surface secrets.resolve diagnostics in memory and qr * test(secrets): repair post-rebase runtime and fixtures * fix(gateway): skip remote password ref resolution when token wins * fix(secrets): treat tailscale remote gateway refs as active * fix(gateway): allow remote password fallback when token ref is unresolved * fix(gateway): ignore stale local password refs for none and trusted-proxy * fix(gateway): skip remote secret ref resolution on local call paths * test(cli): cover qr remote tailscale secret ref resolution * fix(secrets): align gateway password active-surface with auth inference * fix(cli): resolve inferred local gateway password refs in qr * fix(gateway): prefer resolvable remote password over token ref pre-resolution * test(gateway): cover none and trusted-proxy stale password refs * docs(secrets): sync qr and gateway active-surface behavior * fix: restore stability blockers from pre-release audit * Secrets: fix collector/runtime precedence contradictions * docs: align secrets and web credential docs * fix(rebase): resolve integration regressions after main rebase * fix(node-host): resolve gateway secret refs for auth * fix(secrets): harden secretinput runtime readers * gateway: skip inactive auth secretref resolution * cli: avoid gateway preflight for inactive secret refs * extensions: allow unresolved refs in onboarding status * tests: fix qr-cli module mock hoist ordering * Security: align audit checks with SecretInput resolution * Gateway: resolve local-mode remote fallback secret refs * Node host: avoid resolving inactive password secret refs * Secrets runtime: mark Slack appToken inactive for HTTP mode * secrets: keep inactive gateway remote refs non-blocking * cli: include agent memory secret targets in runtime resolution * docs(secrets): sync docs with active-surface and web search behavior * fix(secrets): keep telegram top-level token refs active for blank account tokens * fix(daemon): resolve gateway password secret refs for probe auth * fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled * fix(secrets): align token inheritance and exec timeout defaults * docs(secrets): clarify active-surface notes in cli docs * cli: require secrets.resolve gateway capability * gateway: log auth secret surface diagnostics * secrets: remove dead provider resolver module * fix(secrets): restore gateway auth precedence and fallback resolution * fix(tests): align plugin runtime mock typings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-03-02 20:58:20 -06:00
function logGatewayAuthSurfaceDiagnostics(prepared: {
sourceConfig: OpenClawConfig;
warnings: Array<{ code: string; path: string; message: string }>;
}): void {
const states = evaluateGatewayAuthSurfaceStates({
config: prepared.sourceConfig,
defaults: prepared.sourceConfig.secrets?.defaults,
env: process.env,
});
const inactiveWarnings = new Map<string, string>();
for (const warning of prepared.warnings) {
if (warning.code !== "SECRETS_REF_IGNORED_INACTIVE_SURFACE") {
continue;
}
inactiveWarnings.set(warning.path, warning.message);
}
for (const path of GATEWAY_AUTH_SURFACE_PATHS) {
const state = states[path];
if (!state.hasSecretRef) {
continue;
}
const stateLabel = state.active ? "active" : "inactive";
const inactiveDetails =
!state.active && inactiveWarnings.get(path) ? inactiveWarnings.get(path) : undefined;
const details = inactiveDetails ?? state.reason;
logSecrets.info(`[SECRETS_GATEWAY_AUTH_SURFACE] ${path} is ${stateLabel}. ${details}`);
}
}
function applyGatewayAuthOverridesForStartupPreflight(
config: OpenClawConfig,
overrides: Pick<GatewayServerOptions, "auth" | "tailscale">,
): OpenClawConfig {
if (!overrides.auth && !overrides.tailscale) {
return config;
}
return {
...config,
gateway: {
...config.gateway,
auth: mergeGatewayAuthConfig(config.gateway?.auth, overrides.auth),
tailscale: mergeGatewayTailscaleConfig(config.gateway?.tailscale, overrides.tailscale),
},
};
}
2026-01-14 01:08:15 +00:00
export type GatewayServer = {
close: (opts?: { reason?: string; restartExpectedMs?: number | null }) => Promise<void>;
2026-01-14 01:08:15 +00:00
};
export type GatewayServerOptions = {
/**
* Bind address policy for the Gateway WebSocket/HTTP server.
* - loopback: 127.0.0.1
* - lan: 0.0.0.0
* - tailnet: bind only to the Tailscale IPv4 address (100.64.0.0/10)
2026-01-21 20:35:39 +00:00
* - auto: prefer loopback, else LAN
2026-01-14 01:08:15 +00:00
*/
2026-01-19 04:50:07 +00:00
bind?: import("../config/config.js").GatewayBindMode;
2026-01-14 01:08:15 +00:00
/**
* Advanced override for the bind host, bypassing bind resolution.
* Prefer `bind` unless you really need a specific address.
*/
host?: string;
/**
* If false, do not serve the browser Control UI.
* Default: config `gateway.controlUi.enabled` (or true when absent).
*/
controlUiEnabled?: boolean;
/**
* If false, do not serve `POST /v1/chat/completions`.
* Default: config `gateway.http.endpoints.chatCompletions.enabled` (or false when absent).
*/
openAiChatCompletionsEnabled?: boolean;
/**
* If false, do not serve `POST /v1/responses` (OpenResponses API).
* Default: config `gateway.http.endpoints.responses.enabled` (or false when absent).
*/
openResponsesEnabled?: boolean;
2026-01-14 01:08:15 +00:00
/**
* Override gateway auth configuration (merges with config).
*/
auth?: import("../config/config.js").GatewayAuthConfig;
/**
* Override gateway Tailscale exposure configuration (merges with config).
*/
tailscale?: import("../config/config.js").GatewayTailscaleConfig;
/**
* Test-only: allow canvas host startup even when NODE_ENV/VITEST would disable it.
*/
allowCanvasHostInTests?: boolean;
/**
* Test-only: override the onboarding wizard runner.
*/
wizardRunner?: (
opts: import("../commands/onboard-types.js").OnboardOptions,
runtime: import("../runtime.js").RuntimeEnv,
prompter: import("../wizard/prompts.js").WizardPrompter,
) => Promise<void>;
};
export async function startGatewayServer(
port = 18789,
opts: GatewayServerOptions = {},
): Promise<GatewayServer> {
const minimalTestGateway =
process.env.VITEST === "1" && process.env.OPENCLAW_TEST_MINIMAL_GATEWAY === "1";
2026-01-19 04:50:07 +00:00
// Ensure all default port derivations (browser/canvas) see the actual runtime port.
2026-01-30 03:15:10 +01:00
process.env.OPENCLAW_GATEWAY_PORT = String(port);
logAcceptedEnvOption({
2026-01-30 03:15:10 +01:00
key: "OPENCLAW_RAW_STREAM",
description: "raw stream logging enabled",
});
logAcceptedEnvOption({
2026-01-30 03:15:10 +01:00
key: "OPENCLAW_RAW_STREAM_PATH",
description: "raw stream log path override",
});
2026-01-14 01:08:15 +00:00
let configSnapshot = await readConfigFileSnapshot();
2026-01-14 01:08:15 +00:00
if (configSnapshot.legacyIssues.length > 0) {
if (isNixMode) {
throw new Error(
"Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and restart.",
);
}
const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed);
2026-01-14 01:08:15 +00:00
if (!migrated) {
log.warn(
"gateway: legacy config entries detected but no auto-migration changes were produced; continuing with validation.",
2026-01-14 01:08:15 +00:00
);
} else {
await writeConfigFile(migrated);
if (changes.length > 0) {
log.info(
`gateway: migrated legacy config entries:\n${changes
.map((entry) => `- ${entry}`)
.join("\n")}`,
);
}
2026-01-14 01:08:15 +00:00
}
}
configSnapshot = await readConfigFileSnapshot();
if (configSnapshot.exists && !configSnapshot.valid) {
const issues =
configSnapshot.issues.length > 0
? formatConfigIssueLines(configSnapshot.issues, "", { normalizeRoot: true }).join("\n")
: "Unknown validation issue.";
throw new Error(
2026-01-30 03:15:10 +01:00
`Invalid config at ${configSnapshot.path}.\n${issues}\nRun "${formatCliCommand("openclaw doctor")}" to repair, then retry.`,
);
}
2026-01-20 16:37:34 +00:00
const autoEnable = applyPluginAutoEnable({ config: configSnapshot.config, env: process.env });
if (autoEnable.changes.length > 0) {
try {
await writeConfigFile(autoEnable.config);
log.info(
`gateway: auto-enabled plugins:\n${autoEnable.changes
.map((entry) => `- ${entry}`)
.join("\n")}`,
);
} catch (err) {
log.warn(`gateway: failed to persist plugin auto-enable changes: ${String(err)}`);
}
}
let secretsDegraded = false;
const emitSecretsStateEvent = (
code: "SECRETS_RELOADER_DEGRADED" | "SECRETS_RELOADER_RECOVERED",
message: string,
cfg: OpenClawConfig,
) => {
enqueueSystemEvent(`[${code}] ${message}`, {
sessionKey: resolveMainSessionKey(cfg),
contextKey: code,
});
};
let secretsActivationTail: Promise<void> = Promise.resolve();
const runWithSecretsActivationLock = async <T>(operation: () => Promise<T>): Promise<T> => {
const run = secretsActivationTail.then(operation, operation);
secretsActivationTail = run.then(
() => undefined,
() => undefined,
);
return await run;
};
const activateRuntimeSecrets = async (
config: OpenClawConfig,
params: { reason: "startup" | "reload" | "restart-check"; activate: boolean },
) =>
await runWithSecretsActivationLock(async () => {
try {
const prepared = await prepareSecretsRuntimeSnapshot({ config });
if (params.activate) {
activateSecretsRuntimeSnapshot(prepared);
feat(secrets): expand SecretRef coverage across user-supplied credentials (#29580) * feat(secrets): expand secret target coverage and gateway tooling * docs(secrets): align gateway and CLI secret docs * chore(protocol): regenerate swift gateway models for secrets methods * fix(config): restore talk apiKey fallback and stabilize runner test * ci(windows): reduce test worker count for shard stability * ci(windows): raise node heap for test shard stability * test(feishu): make proxy env precedence assertion windows-safe * fix(gateway): resolve auth password SecretInput refs for clients * fix(gateway): resolve remote SecretInput credentials for clients * fix(secrets): skip inactive refs in command snapshot assignments * fix(secrets): scope gateway.remote refs to effective auth surfaces * fix(secrets): ignore memory defaults when enabled agents disable search * fix(secrets): honor Google Chat serviceAccountRef inheritance * fix(secrets): address tsgo errors in command and gateway collectors * fix(secrets): avoid auth-store load in providers-only configure * fix(gateway): defer local password ref resolution by precedence * fix(secrets): gate telegram webhook secret refs by webhook mode * fix(secrets): gate slack signing secret refs to http mode * fix(secrets): skip telegram botToken refs when tokenFile is set * fix(secrets): gate discord pluralkit refs by enabled flag * fix(secrets): gate discord voice tts refs by voice enabled * test(secrets): make runtime fixture modes explicit * fix(cli): resolve local qr password secret refs * fix(cli): fail when gateway leaves command refs unresolved * fix(gateway): fail when local password SecretRef is unresolved * fix(gateway): fail when required remote SecretRefs are unresolved * fix(gateway): resolve local password refs only when password can win * fix(cli): skip local password SecretRef resolution on qr token override * test(gateway): cast SecretRef fixtures to OpenClawConfig * test(secrets): activate mode-gated targets in runtime coverage fixture * fix(cron): support SecretInput webhook tokens safely * fix(bluebubbles): support SecretInput passwords across config paths * fix(msteams): make appPassword SecretInput-safe in onboarding/token paths * fix(bluebubbles): align SecretInput schema helper typing * fix(cli): clarify secrets.resolve version-skew errors * refactor(secrets): return structured inactive paths from secrets.resolve * refactor(gateway): type onboarding secret writes as SecretInput * chore(protocol): regenerate swift models for secrets.resolve * feat(secrets): expand extension credential secretref support * fix(secrets): gate web-search refs by active provider * fix(onboarding): detect SecretRef credentials in extension status * fix(onboarding): allow keeping existing ref in secret prompt * fix(onboarding): resolve gateway password SecretRefs for probe and tui * fix(onboarding): honor secret-input-mode for local gateway auth * fix(acp): resolve gateway SecretInput credentials * fix(secrets): gate gateway.remote refs to remote surfaces * test(secrets): cover pattern matching and inactive array refs * docs(secrets): clarify secrets.resolve and remote active surfaces * fix(bluebubbles): keep existing SecretRef during onboarding * fix(tests): resolve CI type errors in new SecretRef coverage * fix(extensions): replace raw fetch with SSRF-guarded fetch * test(secrets): mark gateway remote targets active in runtime coverage * test(infra): normalize home-prefix expectation across platforms * fix(cli): only resolve local qr password refs in password mode * test(cli): cover local qr token mode with unresolved password ref * docs(cli): clarify local qr password ref resolution behavior * refactor(extensions): reuse sdk SecretInput helpers * fix(wizard): resolve onboarding env-template secrets before plaintext * fix(cli): surface secrets.resolve diagnostics in memory and qr * test(secrets): repair post-rebase runtime and fixtures * fix(gateway): skip remote password ref resolution when token wins * fix(secrets): treat tailscale remote gateway refs as active * fix(gateway): allow remote password fallback when token ref is unresolved * fix(gateway): ignore stale local password refs for none and trusted-proxy * fix(gateway): skip remote secret ref resolution on local call paths * test(cli): cover qr remote tailscale secret ref resolution * fix(secrets): align gateway password active-surface with auth inference * fix(cli): resolve inferred local gateway password refs in qr * fix(gateway): prefer resolvable remote password over token ref pre-resolution * test(gateway): cover none and trusted-proxy stale password refs * docs(secrets): sync qr and gateway active-surface behavior * fix: restore stability blockers from pre-release audit * Secrets: fix collector/runtime precedence contradictions * docs: align secrets and web credential docs * fix(rebase): resolve integration regressions after main rebase * fix(node-host): resolve gateway secret refs for auth * fix(secrets): harden secretinput runtime readers * gateway: skip inactive auth secretref resolution * cli: avoid gateway preflight for inactive secret refs * extensions: allow unresolved refs in onboarding status * tests: fix qr-cli module mock hoist ordering * Security: align audit checks with SecretInput resolution * Gateway: resolve local-mode remote fallback secret refs * Node host: avoid resolving inactive password secret refs * Secrets runtime: mark Slack appToken inactive for HTTP mode * secrets: keep inactive gateway remote refs non-blocking * cli: include agent memory secret targets in runtime resolution * docs(secrets): sync docs with active-surface and web search behavior * fix(secrets): keep telegram top-level token refs active for blank account tokens * fix(daemon): resolve gateway password secret refs for probe auth * fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled * fix(secrets): align token inheritance and exec timeout defaults * docs(secrets): clarify active-surface notes in cli docs * cli: require secrets.resolve gateway capability * gateway: log auth secret surface diagnostics * secrets: remove dead provider resolver module * fix(secrets): restore gateway auth precedence and fallback resolution * fix(tests): align plugin runtime mock typings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-03-02 20:58:20 -06:00
logGatewayAuthSurfaceDiagnostics(prepared);
}
for (const warning of prepared.warnings) {
logSecrets.warn(`[${warning.code}] ${warning.message}`);
}
if (secretsDegraded) {
const recoveredMessage =
"Secret resolution recovered; runtime remained on last-known-good during the outage.";
logSecrets.info(`[SECRETS_RELOADER_RECOVERED] ${recoveredMessage}`);
emitSecretsStateEvent("SECRETS_RELOADER_RECOVERED", recoveredMessage, prepared.config);
}
secretsDegraded = false;
return prepared;
} catch (err) {
const details = String(err);
if (!secretsDegraded) {
logSecrets.error(`[SECRETS_RELOADER_DEGRADED] ${details}`);
if (params.reason !== "startup") {
emitSecretsStateEvent(
"SECRETS_RELOADER_DEGRADED",
`Secret resolution failed; runtime remains on last-known-good snapshot. ${details}`,
config,
);
}
} else {
logSecrets.warn(`[SECRETS_RELOADER_DEGRADED] ${details}`);
}
secretsDegraded = true;
if (params.reason === "startup") {
throw new Error(`Startup failed: required secrets are unavailable. ${details}`, {
cause: err,
});
}
throw err;
}
});
// Fail fast before startup if required refs are unresolved.
let cfgAtStart: OpenClawConfig;
{
const freshSnapshot = await readConfigFileSnapshot();
if (!freshSnapshot.valid) {
const issues =
freshSnapshot.issues.length > 0
? formatConfigIssueLines(freshSnapshot.issues, "", { normalizeRoot: true }).join("\n")
: "Unknown validation issue.";
throw new Error(`Invalid config at ${freshSnapshot.path}.\n${issues}`);
}
const startupPreflightConfig = applyGatewayAuthOverridesForStartupPreflight(
freshSnapshot.config,
{
auth: opts.auth,
tailscale: opts.tailscale,
},
);
await activateRuntimeSecrets(startupPreflightConfig, {
reason: "startup",
activate: false,
});
}
cfgAtStart = loadConfig();
const authBootstrap = await ensureGatewayStartupAuth({
cfg: cfgAtStart,
env: process.env,
authOverride: opts.auth,
tailscaleOverride: opts.tailscale,
persist: true,
});
cfgAtStart = authBootstrap.cfg;
if (authBootstrap.generatedToken) {
if (authBootstrap.persistedGeneratedToken) {
log.info(
"Gateway auth token was missing. Generated a new token and saved it to config (gateway.auth.token).",
);
} else {
log.warn(
"Gateway auth token was missing. Generated a runtime token for this startup without changing config; restart will generate a different token. Persist one with `openclaw config set gateway.auth.mode token` and `openclaw config set gateway.auth.token <token>`.",
);
}
}
cfgAtStart = (
await activateRuntimeSecrets(cfgAtStart, {
reason: "startup",
activate: true,
})
).config;
2026-01-21 00:29:42 +00:00
const diagnosticsEnabled = isDiagnosticsEnabled(cfgAtStart);
if (diagnosticsEnabled) {
startDiagnosticHeartbeat();
2026-01-21 00:29:42 +00:00
}
setGatewaySigusr1RestartPolicy({ allowExternal: isRestartEnabled(cfgAtStart) });
setPreRestartDeferralCheck(
() => getTotalQueueSize() + getTotalPendingReplies() + getActiveEmbeddedRunCount(),
);
// Unconditional startup migration: seed gateway.controlUi.allowedOrigins for existing
// non-loopback installs that upgraded to v2026.2.26+ without required origins.
cfgAtStart = await maybeSeedControlUiAllowedOriginsAtStartup({
config: cfgAtStart,
writeConfig: writeConfigFile,
log,
});
2026-01-14 01:08:15 +00:00
initSubagentRegistry();
const defaultAgentId = resolveDefaultAgentId(cfgAtStart);
const defaultWorkspaceDir = resolveAgentWorkspaceDir(cfgAtStart, defaultAgentId);
2026-01-15 02:42:41 +00:00
const baseMethods = listGatewayMethods();
const emptyPluginRegistry = createEmptyPluginRegistry();
const { pluginRegistry, gatewayMethods: baseGatewayMethods } = minimalTestGateway
? { pluginRegistry: emptyPluginRegistry, gatewayMethods: baseMethods }
: loadGatewayPlugins({
cfg: cfgAtStart,
workspaceDir: defaultWorkspaceDir,
log,
coreGatewayHandlers,
baseMethods,
});
2026-01-15 02:42:41 +00:00
const channelLogs = Object.fromEntries(
listChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]),
) as Record<ChannelId, ReturnType<typeof createSubsystemLogger>>;
const channelRuntimeEnvs = Object.fromEntries(
Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]),
) as Record<ChannelId, RuntimeEnv>;
const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
const gatewayMethods = Array.from(new Set([...baseGatewayMethods, ...channelMethods]));
2026-01-14 01:08:15 +00:00
let pluginServices: PluginServicesHandle | null = null;
const runtimeConfig = await resolveGatewayRuntimeConfig({
cfg: cfgAtStart,
port,
bind: opts.bind,
host: opts.host,
controlUiEnabled: opts.controlUiEnabled,
openAiChatCompletionsEnabled: opts.openAiChatCompletionsEnabled,
openResponsesEnabled: opts.openResponsesEnabled,
auth: opts.auth,
tailscale: opts.tailscale,
2026-01-14 01:08:15 +00:00
});
const {
bindHost,
controlUiEnabled,
openAiChatCompletionsEnabled,
openAiChatCompletionsConfig,
openResponsesEnabled,
openResponsesConfig,
strictTransportSecurityHeader,
controlUiBasePath,
controlUiRoot: controlUiRootOverride,
resolvedAuth,
tailscaleConfig,
tailscaleMode,
} = runtimeConfig;
let hooksConfig = runtimeConfig.hooksConfig;
let hookClientIpConfig = resolveHookClientIpConfig(cfgAtStart);
const canvasHostEnabled = runtimeConfig.canvasHostEnabled;
2026-01-14 01:08:15 +00:00
// Create auth rate limiters used by connect/auth flows.
const rateLimitConfig = cfgAtStart.gateway?.auth?.rateLimit;
const { rateLimiter: authRateLimiter, browserRateLimiter: browserAuthRateLimiter } =
createGatewayAuthRateLimiters(rateLimitConfig);
let controlUiRootState: ControlUiRootState | undefined;
if (controlUiRootOverride) {
const resolvedOverride = resolveControlUiRootOverrideSync(controlUiRootOverride);
const resolvedOverridePath = path.resolve(controlUiRootOverride);
controlUiRootState = resolvedOverride
? { kind: "resolved", path: resolvedOverride }
: { kind: "invalid", path: resolvedOverridePath };
if (!resolvedOverride) {
log.warn(`gateway: controlUi.root not found at ${resolvedOverridePath}`);
}
} else if (controlUiEnabled) {
let resolvedRoot = resolveControlUiRootSync({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
if (!resolvedRoot) {
const ensureResult = await ensureControlUiAssetsBuilt(gatewayRuntime);
if (!ensureResult.ok && ensureResult.message) {
log.warn(`gateway: ${ensureResult.message}`);
}
resolvedRoot = resolveControlUiRootSync({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
}
controlUiRootState = resolvedRoot
? {
kind: isPackageProvenControlUiRootSync(resolvedRoot, {
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
})
? "bundled"
: "resolved",
path: resolvedRoot,
}
: { kind: "missing" };
}
2026-01-14 01:08:15 +00:00
const wizardRunner = opts.wizardRunner ?? runOnboardingWizard;
const { wizardSessions, findRunningWizard, purgeWizardSession } = createWizardSessionTracker();
2026-01-14 01:08:15 +00:00
const deps = createDefaultDeps();
let canvasHostServer: CanvasHostServer | null = null;
2026-01-19 02:46:07 +00:00
const gatewayTls = await loadGatewayTlsRuntime(cfgAtStart.gateway?.tls, log.child("tls"));
if (cfgAtStart.gateway?.tls?.enabled && !gatewayTls.enabled) {
throw new Error(gatewayTls.error ?? "gateway tls: failed to enable");
}
const serverStartedAt = Date.now();
const channelManager = createChannelManager({
loadConfig,
channelLogs,
channelRuntimeEnvs,
channelRuntime: createPluginRuntime().channel,
});
const getReadiness = createReadinessChecker({
channelManager,
startedAt: serverStartedAt,
});
const {
canvasHost,
httpServer,
httpServers,
httpBindHosts,
wss,
clients,
broadcast,
broadcastToConnIds,
agentRunSeq,
dedupe,
chatRunState,
chatRunBuffers,
chatDeltaSentAt,
addChatRun,
removeChatRun,
chatAbortControllers,
toolEventRecipients,
} = await createGatewayRuntimeState({
cfg: cfgAtStart,
2026-01-14 01:08:15 +00:00
bindHost,
port,
controlUiEnabled,
controlUiBasePath,
controlUiRoot: controlUiRootState,
2026-01-14 01:08:15 +00:00
openAiChatCompletionsEnabled,
openAiChatCompletionsConfig,
openResponsesEnabled,
openResponsesConfig,
strictTransportSecurityHeader,
2026-01-14 01:08:15 +00:00
resolvedAuth,
rateLimiter: authRateLimiter,
2026-01-19 02:46:07 +00:00
gatewayTls,
hooksConfig: () => hooksConfig,
getHookClientIpConfig: () => hookClientIpConfig,
pluginRegistry,
deps,
canvasRuntime,
canvasHostEnabled,
allowCanvasHostInTests: opts.allowCanvasHostInTests,
logCanvas,
log,
logHooks,
logPlugins,
getReadiness,
2026-01-14 01:08:15 +00:00
});
let bonjourStop: (() => Promise<void>) | null = null;
2026-01-19 04:50:07 +00:00
const nodeRegistry = new NodeRegistry();
const nodePresenceTimers = new Map<string, ReturnType<typeof setInterval>>();
const nodeSubscriptions = createNodeSubscriptionManager();
const sessionEventSubscribers = createSessionEventSubscriberRegistry();
2026-01-19 04:50:07 +00:00
const nodeSendEvent = (opts: { nodeId: string; event: string; payloadJSON?: string | null }) => {
const payload = safeParseJson(opts.payloadJSON ?? null);
nodeRegistry.sendEvent(opts.nodeId, opts.event, payload);
};
const nodeSendToSession = (sessionKey: string, event: string, payload: unknown) =>
nodeSubscriptions.sendToSession(sessionKey, event, payload, nodeSendEvent);
const nodeSendToAllSubscribed = (event: string, payload: unknown) =>
nodeSubscriptions.sendToAllSubscribed(event, payload, nodeSendEvent);
const nodeSubscribe = nodeSubscriptions.subscribe;
const nodeUnsubscribe = nodeSubscriptions.unsubscribe;
const nodeUnsubscribeAll = nodeSubscriptions.unsubscribeAll;
const broadcastVoiceWakeChanged = (triggers: string[]) => {
broadcast("voicewake.changed", { triggers }, { dropIfSlow: true });
};
const hasMobileNodeConnected = () => hasConnectedMobileNode(nodeRegistry);
applyGatewayLaneConcurrency(cfgAtStart);
2026-01-14 01:08:15 +00:00
let cronState = buildGatewayCronService({
cfg: cfgAtStart,
deps,
broadcast,
2026-01-14 01:08:15 +00:00
});
let { cron, storePath: cronStorePath } = cronState;
2026-01-14 01:08:15 +00:00
const { getRuntimeSnapshot, startChannels, startChannel, stopChannel, markChannelLoggedOut } =
channelManager;
2026-01-14 01:08:15 +00:00
if (!minimalTestGateway) {
const machineDisplayName = await getMachineDisplayName();
const discovery = await startGatewayDiscovery({
machineDisplayName,
port,
gatewayTls: gatewayTls.enabled
? { enabled: true, fingerprintSha256: gatewayTls.fingerprintSha256 }
: undefined,
wideAreaDiscoveryEnabled: cfgAtStart.discovery?.wideArea?.enabled === true,
wideAreaDiscoveryDomain: cfgAtStart.discovery?.wideArea?.domain,
tailscaleMode,
mdnsMode: cfgAtStart.discovery?.mdns?.mode,
logDiscovery,
});
bonjourStop = discovery.bonjourStop;
}
if (!minimalTestGateway) {
setSkillsRemoteRegistry(nodeRegistry);
void primeRemoteSkillsCache();
}
// Debounce skills-triggered node probes to avoid feedback loops and rapid-fire invokes.
// Skills changes can happen in bursts (e.g., file watcher events), and each probe
// takes time to complete. A 30-second delay ensures we batch changes together.
let skillsRefreshTimer: ReturnType<typeof setTimeout> | null = null;
const skillsRefreshDelayMs = 30_000;
const skillsChangeUnsub = minimalTestGateway
? () => {}
: registerSkillsChangeListener((event) => {
if (event.reason === "remote-node") {
return;
}
if (skillsRefreshTimer) {
clearTimeout(skillsRefreshTimer);
}
skillsRefreshTimer = setTimeout(() => {
skillsRefreshTimer = null;
const latest = loadConfig();
void refreshRemoteBinsForConnectedNodes(latest);
}, skillsRefreshDelayMs);
});
2026-01-14 01:08:15 +00:00
const noopInterval = () => setInterval(() => {}, 1 << 30);
let tickInterval = noopInterval();
let healthInterval = noopInterval();
let dedupeCleanup = noopInterval();
let mediaCleanup: ReturnType<typeof setInterval> | null = null;
if (!minimalTestGateway) {
({ tickInterval, healthInterval, dedupeCleanup, mediaCleanup } = startGatewayMaintenanceTimers({
2026-01-14 01:08:15 +00:00
broadcast,
nodeSendToAllSubscribed,
getPresenceVersion,
getHealthVersion,
refreshGatewayHealthSnapshot,
logHealth,
dedupe,
chatAbortControllers,
2026-01-14 01:08:15 +00:00
chatRunState,
chatRunBuffers,
chatDeltaSentAt,
removeChatRun,
agentRunSeq,
nodeSendToSession,
...(typeof cfgAtStart.media?.ttlHours === "number"
? { mediaCleanupTtlMs: resolveMediaCleanupTtlMs(cfgAtStart.media.ttlHours) }
: {}),
}));
}
2026-01-14 01:08:15 +00:00
const agentUnsub = minimalTestGateway
? null
: onAgentEvent(
createAgentEventHandler({
broadcast,
broadcastToConnIds,
nodeSendToSession,
agentRunSeq,
chatRunState,
resolveSessionKeyForRun,
clearAgentRunContext,
toolEventRecipients,
sessionEventSubscribers,
}),
);
2026-01-14 01:08:15 +00:00
const heartbeatUnsub = minimalTestGateway
? null
: onHeartbeatEvent((evt) => {
broadcast("heartbeat", evt, { dropIfSlow: true });
});
const transcriptUnsub = minimalTestGateway
? null
: onSessionTranscriptUpdate((update) => {
const sessionKey =
update.sessionKey ?? resolveSessionKeyForTranscriptFile(update.sessionFile);
if (!sessionKey || update.message === undefined) {
return;
}
const connIds = sessionEventSubscribers.getAll();
if (connIds.size === 0) {
return;
}
broadcastToConnIds(
"session.message",
{
sessionKey,
message: update.message,
},
connIds,
{ dropIfSlow: true },
);
});
let heartbeatRunner: HeartbeatRunner = minimalTestGateway
? {
stop: () => {},
updateConfig: () => {},
}
: startHeartbeatRunner({ cfg: cfgAtStart });
2026-01-14 01:08:15 +00:00
const healthCheckMinutes = cfgAtStart.gateway?.channelHealthCheckMinutes;
const healthCheckDisabled = healthCheckMinutes === 0;
let channelHealthMonitor = healthCheckDisabled
? null
: startChannelHealthMonitor({
channelManager,
checkIntervalMs: (healthCheckMinutes ?? 5) * 60_000,
});
if (!minimalTestGateway) {
void cron.start().catch((err) => logCron.error(`failed to start: ${String(err)}`));
}
2026-01-14 01:08:15 +00:00
// Recover pending outbound deliveries from previous crash/restart.
if (!minimalTestGateway) {
void (async () => {
const { recoverPendingDeliveries } = await import("../infra/outbound/delivery-queue.js");
const { deliverOutboundPayloads } = await import("../infra/outbound/deliver.js");
const logRecovery = log.child("delivery-recovery");
await recoverPendingDeliveries({
deliver: deliverOutboundPayloads,
log: logRecovery,
cfg: cfgAtStart,
});
})().catch((err) => log.error(`Delivery recovery failed: ${String(err)}`));
}
2026-01-19 02:31:18 +00:00
const execApprovalManager = new ExecApprovalManager();
const execApprovalForwarder = createExecApprovalForwarder();
const execApprovalHandlers = createExecApprovalHandlers(execApprovalManager, {
forwarder: execApprovalForwarder,
});
const secretsHandlers = createSecretsHandlers({
reloadSecrets: async () => {
const active = getActiveSecretsRuntimeSnapshot();
if (!active) {
throw new Error("Secrets runtime snapshot is not active.");
}
const prepared = await activateRuntimeSecrets(active.sourceConfig, {
reason: "reload",
activate: true,
});
return { warningCount: prepared.warnings.length };
},
feat(secrets): expand SecretRef coverage across user-supplied credentials (#29580) * feat(secrets): expand secret target coverage and gateway tooling * docs(secrets): align gateway and CLI secret docs * chore(protocol): regenerate swift gateway models for secrets methods * fix(config): restore talk apiKey fallback and stabilize runner test * ci(windows): reduce test worker count for shard stability * ci(windows): raise node heap for test shard stability * test(feishu): make proxy env precedence assertion windows-safe * fix(gateway): resolve auth password SecretInput refs for clients * fix(gateway): resolve remote SecretInput credentials for clients * fix(secrets): skip inactive refs in command snapshot assignments * fix(secrets): scope gateway.remote refs to effective auth surfaces * fix(secrets): ignore memory defaults when enabled agents disable search * fix(secrets): honor Google Chat serviceAccountRef inheritance * fix(secrets): address tsgo errors in command and gateway collectors * fix(secrets): avoid auth-store load in providers-only configure * fix(gateway): defer local password ref resolution by precedence * fix(secrets): gate telegram webhook secret refs by webhook mode * fix(secrets): gate slack signing secret refs to http mode * fix(secrets): skip telegram botToken refs when tokenFile is set * fix(secrets): gate discord pluralkit refs by enabled flag * fix(secrets): gate discord voice tts refs by voice enabled * test(secrets): make runtime fixture modes explicit * fix(cli): resolve local qr password secret refs * fix(cli): fail when gateway leaves command refs unresolved * fix(gateway): fail when local password SecretRef is unresolved * fix(gateway): fail when required remote SecretRefs are unresolved * fix(gateway): resolve local password refs only when password can win * fix(cli): skip local password SecretRef resolution on qr token override * test(gateway): cast SecretRef fixtures to OpenClawConfig * test(secrets): activate mode-gated targets in runtime coverage fixture * fix(cron): support SecretInput webhook tokens safely * fix(bluebubbles): support SecretInput passwords across config paths * fix(msteams): make appPassword SecretInput-safe in onboarding/token paths * fix(bluebubbles): align SecretInput schema helper typing * fix(cli): clarify secrets.resolve version-skew errors * refactor(secrets): return structured inactive paths from secrets.resolve * refactor(gateway): type onboarding secret writes as SecretInput * chore(protocol): regenerate swift models for secrets.resolve * feat(secrets): expand extension credential secretref support * fix(secrets): gate web-search refs by active provider * fix(onboarding): detect SecretRef credentials in extension status * fix(onboarding): allow keeping existing ref in secret prompt * fix(onboarding): resolve gateway password SecretRefs for probe and tui * fix(onboarding): honor secret-input-mode for local gateway auth * fix(acp): resolve gateway SecretInput credentials * fix(secrets): gate gateway.remote refs to remote surfaces * test(secrets): cover pattern matching and inactive array refs * docs(secrets): clarify secrets.resolve and remote active surfaces * fix(bluebubbles): keep existing SecretRef during onboarding * fix(tests): resolve CI type errors in new SecretRef coverage * fix(extensions): replace raw fetch with SSRF-guarded fetch * test(secrets): mark gateway remote targets active in runtime coverage * test(infra): normalize home-prefix expectation across platforms * fix(cli): only resolve local qr password refs in password mode * test(cli): cover local qr token mode with unresolved password ref * docs(cli): clarify local qr password ref resolution behavior * refactor(extensions): reuse sdk SecretInput helpers * fix(wizard): resolve onboarding env-template secrets before plaintext * fix(cli): surface secrets.resolve diagnostics in memory and qr * test(secrets): repair post-rebase runtime and fixtures * fix(gateway): skip remote password ref resolution when token wins * fix(secrets): treat tailscale remote gateway refs as active * fix(gateway): allow remote password fallback when token ref is unresolved * fix(gateway): ignore stale local password refs for none and trusted-proxy * fix(gateway): skip remote secret ref resolution on local call paths * test(cli): cover qr remote tailscale secret ref resolution * fix(secrets): align gateway password active-surface with auth inference * fix(cli): resolve inferred local gateway password refs in qr * fix(gateway): prefer resolvable remote password over token ref pre-resolution * test(gateway): cover none and trusted-proxy stale password refs * docs(secrets): sync qr and gateway active-surface behavior * fix: restore stability blockers from pre-release audit * Secrets: fix collector/runtime precedence contradictions * docs: align secrets and web credential docs * fix(rebase): resolve integration regressions after main rebase * fix(node-host): resolve gateway secret refs for auth * fix(secrets): harden secretinput runtime readers * gateway: skip inactive auth secretref resolution * cli: avoid gateway preflight for inactive secret refs * extensions: allow unresolved refs in onboarding status * tests: fix qr-cli module mock hoist ordering * Security: align audit checks with SecretInput resolution * Gateway: resolve local-mode remote fallback secret refs * Node host: avoid resolving inactive password secret refs * Secrets runtime: mark Slack appToken inactive for HTTP mode * secrets: keep inactive gateway remote refs non-blocking * cli: include agent memory secret targets in runtime resolution * docs(secrets): sync docs with active-surface and web search behavior * fix(secrets): keep telegram top-level token refs active for blank account tokens * fix(daemon): resolve gateway password secret refs for probe auth * fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled * fix(secrets): align token inheritance and exec timeout defaults * docs(secrets): clarify active-surface notes in cli docs * cli: require secrets.resolve gateway capability * gateway: log auth secret surface diagnostics * secrets: remove dead provider resolver module * fix(secrets): restore gateway auth precedence and fallback resolution * fix(tests): align plugin runtime mock typings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-03-02 20:58:20 -06:00
resolveSecrets: async ({ commandName, targetIds }) => {
const { assignments, diagnostics, inactiveRefPaths } =
resolveCommandSecretsFromActiveRuntimeSnapshot({
commandName,
targetIds: new Set(targetIds),
});
if (assignments.length === 0) {
return { assignments: [] as CommandSecretAssignment[], diagnostics, inactiveRefPaths };
}
return { assignments, diagnostics, inactiveRefPaths };
},
});
2026-01-19 02:31:18 +00:00
2026-01-19 06:22:01 +00:00
const canvasHostServerPort = (canvasHostServer as CanvasHostServer | null)?.port;
feature(context): extend plugin system to support custom context management (#22201) * feat(context-engine): add ContextEngine interface and registry Introduce the pluggable ContextEngine abstraction that allows external plugins to register custom context management strategies. - ContextEngine interface with lifecycle methods: bootstrap, ingest, ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn, onSubagentEnded, dispose - Module-level singleton registry with registerContextEngine() and resolveContextEngine() (config-driven slot selection) - LegacyContextEngine: pass-through implementation wrapping existing compaction behavior for 100% backward compatibility - ensureContextEnginesInitialized() guard for safe one-time registration - 19 tests covering contract, registry, resolution, and legacy parity * feat(plugins): add context-engine slot and registerContextEngine API Wire the ContextEngine abstraction into the plugin system so external plugins can register context engines via the standard plugin API. - Add 'context-engine' to PluginKind union type - Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy') - Wire registerContextEngine() through OpenClawPluginApi - Export ContextEngine types from plugin-sdk for external consumers - Restore proper slot-based resolution in registry * feat(context-engine): wire ContextEngine into agent run lifecycle Integrate the ContextEngine abstraction into the core agent run path: - Resolve context engine once per run (reused across retries) - Bootstrap: hydrate canonical store from session file on first run - Assemble: route context assembly through pluggable engine - Auto-compaction guard: disable built-in auto-compaction when the engine declares ownsCompaction (prevents double-compaction) - AfterTurn: post-turn lifecycle hook for ingest + background compaction decisions - Overflow compaction: route through contextEngine.compact() - Dispose: clean up engine resources in finally block - Notify context engine on subagent lifecycle events Legacy engine: all lifecycle methods are pass-through/no-op, preserving 100% backward compatibility for users without a context engine plugin. * feat(plugins): add scoped subagent methods and gateway request scope Expose runtime.subagent.{run, waitForRun, getSession, deleteSession} so external plugins can spawn sub-agent sessions without raw gateway dispatch access. Uses AsyncLocalStorage request-scope bridge to dispatch internally via handleGatewayRequest with a synthetic operator client. Methods are only available during gateway request handling. - Symbol.for-backed global singleton for cross-module-reload safety - Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp) - Set gateway request scope for all handlers, not just plugin handlers - 3 staleness tests for fallback context hardening * feat(context-engine): route /compact and sessions.get through context engine Wire the /compact command and sessions.get handler through the pluggable ContextEngine interface. - Thread tokenBudget and force parameters to context engine compact - Route /compact through contextEngine.compact() when registered - Wire sessions.get as runtime alias for plugin subagent dispatch - Add .pebbles/ to .gitignore * style: format with oxfmt 0.33.0 Fix duplicate import (ControlUiRootState in server.impl.ts) and import ordering across all changed files. * fix: update extension test mocks for context-engine types Add missing subagent property to bluebubbles PluginRuntime mock. Add missing registerContextEngine to lobster OpenClawPluginApi mock. * fix(subagents): keep deferred delete cleanup retryable * style: format run attempt for CI * fix(rebase): remove duplicate embedded-run imports * test: add missing gateway context mock export * fix: pass resolved auth profile into afterTurn compaction Ensure the embedded runner forwards resolved auth profile context into legacy context-engine compaction params on the normal afterTurn path, matching overflow compaction behavior. This allows downstream LCM summarization to use the intended provider auth/profile consistently. Also fix strict TS typing in external-link token dedupe and align an attempt unit test reasoningLevel value with the current ReasoningLevel enum. Regeneration-Prompt: | We were debugging context-engine compaction where downstream summary calls were missing the right auth/profile context in normal afterTurn flow, while overflow compaction already propagated it. Preserve current behavior and keep changes additive: thread the resolved authProfileId through run -> attempt -> legacy compaction param builder without broad refactors. Add tests that prove the auth profile is included in afterTurn legacy params and that overflow compaction still passes it through run attempts. Keep existing APIs stable, and only adjust small type issues needed for strict compilation. * fix: remove duplicate imports from rebase * feat: add context-engine system prompt additions * fix(rebase): dedupe attempt import declarations * test: fix fetch mock typing in ollama autodiscovery * fix(test): add registerContextEngine to diffs extension mock APIs * test(windows): use path.delimiter in ios-team-id fixture PATH * test(cron): add model formatting and precedence edge case tests Covers: - Provider/model string splitting (whitespace, nested paths, empty segments) - Provider normalization (casing, aliases like bedrock→amazon-bedrock) - Anthropic model alias normalization (opus-4.5→claude-opus-4-5) - Precedence: job payload > session override > config default - Sequential runs with different providers (CI flake regression pattern) - forceNew session preserving stored model overrides - Whitespace/empty model string edge cases - Config model as string vs object format * test(cron): fix model formatting test config types * test(phone-control): add registerContextEngine to mock API * fix: re-export ChannelKind from config-reload-plan * fix: add subagent mock to plugin-runtime-mock test util * docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
const gatewayRequestContext: import("./server-methods/types.js").GatewayRequestContext = {
deps,
cron,
cronStorePath,
execApprovalManager,
loadGatewayModelCatalog,
getHealthCache,
refreshHealthSnapshot: refreshGatewayHealthSnapshot,
logHealth,
logGateway: log,
incrementPresenceVersion,
getHealthVersion,
broadcast,
broadcastToConnIds,
nodeSendToSession,
nodeSendToAllSubscribed,
nodeSubscribe,
nodeUnsubscribe,
nodeUnsubscribeAll,
hasConnectedMobileNode: hasMobileNodeConnected,
hasExecApprovalClients: () => {
for (const gatewayClient of clients) {
const scopes = Array.isArray(gatewayClient.connect.scopes)
? gatewayClient.connect.scopes
: [];
if (scopes.includes("operator.admin") || scopes.includes("operator.approvals")) {
return true;
}
}
return false;
},
nodeRegistry,
agentRunSeq,
chatAbortControllers,
chatAbortedRuns: chatRunState.abortedRuns,
chatRunBuffers: chatRunState.buffers,
chatDeltaSentAt: chatRunState.deltaSentAt,
addChatRun,
removeChatRun,
subscribeSessionEvents: sessionEventSubscribers.subscribe,
unsubscribeSessionEvents: sessionEventSubscribers.unsubscribe,
unsubscribeAllSessionEvents: sessionEventSubscribers.unsubscribe,
getSessionEventSubscriberConnIds: sessionEventSubscribers.getAll,
feature(context): extend plugin system to support custom context management (#22201) * feat(context-engine): add ContextEngine interface and registry Introduce the pluggable ContextEngine abstraction that allows external plugins to register custom context management strategies. - ContextEngine interface with lifecycle methods: bootstrap, ingest, ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn, onSubagentEnded, dispose - Module-level singleton registry with registerContextEngine() and resolveContextEngine() (config-driven slot selection) - LegacyContextEngine: pass-through implementation wrapping existing compaction behavior for 100% backward compatibility - ensureContextEnginesInitialized() guard for safe one-time registration - 19 tests covering contract, registry, resolution, and legacy parity * feat(plugins): add context-engine slot and registerContextEngine API Wire the ContextEngine abstraction into the plugin system so external plugins can register context engines via the standard plugin API. - Add 'context-engine' to PluginKind union type - Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy') - Wire registerContextEngine() through OpenClawPluginApi - Export ContextEngine types from plugin-sdk for external consumers - Restore proper slot-based resolution in registry * feat(context-engine): wire ContextEngine into agent run lifecycle Integrate the ContextEngine abstraction into the core agent run path: - Resolve context engine once per run (reused across retries) - Bootstrap: hydrate canonical store from session file on first run - Assemble: route context assembly through pluggable engine - Auto-compaction guard: disable built-in auto-compaction when the engine declares ownsCompaction (prevents double-compaction) - AfterTurn: post-turn lifecycle hook for ingest + background compaction decisions - Overflow compaction: route through contextEngine.compact() - Dispose: clean up engine resources in finally block - Notify context engine on subagent lifecycle events Legacy engine: all lifecycle methods are pass-through/no-op, preserving 100% backward compatibility for users without a context engine plugin. * feat(plugins): add scoped subagent methods and gateway request scope Expose runtime.subagent.{run, waitForRun, getSession, deleteSession} so external plugins can spawn sub-agent sessions without raw gateway dispatch access. Uses AsyncLocalStorage request-scope bridge to dispatch internally via handleGatewayRequest with a synthetic operator client. Methods are only available during gateway request handling. - Symbol.for-backed global singleton for cross-module-reload safety - Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp) - Set gateway request scope for all handlers, not just plugin handlers - 3 staleness tests for fallback context hardening * feat(context-engine): route /compact and sessions.get through context engine Wire the /compact command and sessions.get handler through the pluggable ContextEngine interface. - Thread tokenBudget and force parameters to context engine compact - Route /compact through contextEngine.compact() when registered - Wire sessions.get as runtime alias for plugin subagent dispatch - Add .pebbles/ to .gitignore * style: format with oxfmt 0.33.0 Fix duplicate import (ControlUiRootState in server.impl.ts) and import ordering across all changed files. * fix: update extension test mocks for context-engine types Add missing subagent property to bluebubbles PluginRuntime mock. Add missing registerContextEngine to lobster OpenClawPluginApi mock. * fix(subagents): keep deferred delete cleanup retryable * style: format run attempt for CI * fix(rebase): remove duplicate embedded-run imports * test: add missing gateway context mock export * fix: pass resolved auth profile into afterTurn compaction Ensure the embedded runner forwards resolved auth profile context into legacy context-engine compaction params on the normal afterTurn path, matching overflow compaction behavior. This allows downstream LCM summarization to use the intended provider auth/profile consistently. Also fix strict TS typing in external-link token dedupe and align an attempt unit test reasoningLevel value with the current ReasoningLevel enum. Regeneration-Prompt: | We were debugging context-engine compaction where downstream summary calls were missing the right auth/profile context in normal afterTurn flow, while overflow compaction already propagated it. Preserve current behavior and keep changes additive: thread the resolved authProfileId through run -> attempt -> legacy compaction param builder without broad refactors. Add tests that prove the auth profile is included in afterTurn legacy params and that overflow compaction still passes it through run attempts. Keep existing APIs stable, and only adjust small type issues needed for strict compilation. * fix: remove duplicate imports from rebase * feat: add context-engine system prompt additions * fix(rebase): dedupe attempt import declarations * test: fix fetch mock typing in ollama autodiscovery * fix(test): add registerContextEngine to diffs extension mock APIs * test(windows): use path.delimiter in ios-team-id fixture PATH * test(cron): add model formatting and precedence edge case tests Covers: - Provider/model string splitting (whitespace, nested paths, empty segments) - Provider normalization (casing, aliases like bedrock→amazon-bedrock) - Anthropic model alias normalization (opus-4.5→claude-opus-4-5) - Precedence: job payload > session override > config default - Sequential runs with different providers (CI flake regression pattern) - forceNew session preserving stored model overrides - Whitespace/empty model string edge cases - Config model as string vs object format * test(cron): fix model formatting test config types * test(phone-control): add registerContextEngine to mock API * fix: re-export ChannelKind from config-reload-plan * fix: add subagent mock to plugin-runtime-mock test util * docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
registerToolEventRecipient: toolEventRecipients.add,
dedupe,
wizardSessions,
findRunningWizard,
purgeWizardSession,
getRuntimeSnapshot,
startChannel,
stopChannel,
markChannelLoggedOut,
wizardRunner,
broadcastVoiceWakeChanged,
};
// Store the gateway context as a fallback for plugin subagent dispatch
// in non-WS paths (Telegram polling, WhatsApp, etc.) where no per-request
// scope is set via AsyncLocalStorage.
setFallbackGatewayContext(gatewayRequestContext);
attachGatewayWsHandlers({
2026-01-14 01:08:15 +00:00
wss,
clients,
port,
2026-01-19 04:50:07 +00:00
gatewayHost: bindHost ?? undefined,
2026-01-14 01:08:15 +00:00
canvasHostEnabled: Boolean(canvasHost),
2026-01-19 06:22:01 +00:00
canvasHostServerPort,
2026-01-14 01:08:15 +00:00
resolvedAuth,
rateLimiter: authRateLimiter,
browserRateLimiter: browserAuthRateLimiter,
2026-01-14 01:08:15 +00:00
gatewayMethods,
events: GATEWAY_EVENTS,
2026-01-14 01:08:15 +00:00
logGateway: log,
logHealth,
logWsControl,
2026-01-19 02:31:18 +00:00
extraHandlers: {
...pluginRegistry.gatewayHandlers,
...execApprovalHandlers,
...secretsHandlers,
2026-01-19 02:31:18 +00:00
},
2026-01-14 01:08:15 +00:00
broadcast,
feature(context): extend plugin system to support custom context management (#22201) * feat(context-engine): add ContextEngine interface and registry Introduce the pluggable ContextEngine abstraction that allows external plugins to register custom context management strategies. - ContextEngine interface with lifecycle methods: bootstrap, ingest, ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn, onSubagentEnded, dispose - Module-level singleton registry with registerContextEngine() and resolveContextEngine() (config-driven slot selection) - LegacyContextEngine: pass-through implementation wrapping existing compaction behavior for 100% backward compatibility - ensureContextEnginesInitialized() guard for safe one-time registration - 19 tests covering contract, registry, resolution, and legacy parity * feat(plugins): add context-engine slot and registerContextEngine API Wire the ContextEngine abstraction into the plugin system so external plugins can register context engines via the standard plugin API. - Add 'context-engine' to PluginKind union type - Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy') - Wire registerContextEngine() through OpenClawPluginApi - Export ContextEngine types from plugin-sdk for external consumers - Restore proper slot-based resolution in registry * feat(context-engine): wire ContextEngine into agent run lifecycle Integrate the ContextEngine abstraction into the core agent run path: - Resolve context engine once per run (reused across retries) - Bootstrap: hydrate canonical store from session file on first run - Assemble: route context assembly through pluggable engine - Auto-compaction guard: disable built-in auto-compaction when the engine declares ownsCompaction (prevents double-compaction) - AfterTurn: post-turn lifecycle hook for ingest + background compaction decisions - Overflow compaction: route through contextEngine.compact() - Dispose: clean up engine resources in finally block - Notify context engine on subagent lifecycle events Legacy engine: all lifecycle methods are pass-through/no-op, preserving 100% backward compatibility for users without a context engine plugin. * feat(plugins): add scoped subagent methods and gateway request scope Expose runtime.subagent.{run, waitForRun, getSession, deleteSession} so external plugins can spawn sub-agent sessions without raw gateway dispatch access. Uses AsyncLocalStorage request-scope bridge to dispatch internally via handleGatewayRequest with a synthetic operator client. Methods are only available during gateway request handling. - Symbol.for-backed global singleton for cross-module-reload safety - Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp) - Set gateway request scope for all handlers, not just plugin handlers - 3 staleness tests for fallback context hardening * feat(context-engine): route /compact and sessions.get through context engine Wire the /compact command and sessions.get handler through the pluggable ContextEngine interface. - Thread tokenBudget and force parameters to context engine compact - Route /compact through contextEngine.compact() when registered - Wire sessions.get as runtime alias for plugin subagent dispatch - Add .pebbles/ to .gitignore * style: format with oxfmt 0.33.0 Fix duplicate import (ControlUiRootState in server.impl.ts) and import ordering across all changed files. * fix: update extension test mocks for context-engine types Add missing subagent property to bluebubbles PluginRuntime mock. Add missing registerContextEngine to lobster OpenClawPluginApi mock. * fix(subagents): keep deferred delete cleanup retryable * style: format run attempt for CI * fix(rebase): remove duplicate embedded-run imports * test: add missing gateway context mock export * fix: pass resolved auth profile into afterTurn compaction Ensure the embedded runner forwards resolved auth profile context into legacy context-engine compaction params on the normal afterTurn path, matching overflow compaction behavior. This allows downstream LCM summarization to use the intended provider auth/profile consistently. Also fix strict TS typing in external-link token dedupe and align an attempt unit test reasoningLevel value with the current ReasoningLevel enum. Regeneration-Prompt: | We were debugging context-engine compaction where downstream summary calls were missing the right auth/profile context in normal afterTurn flow, while overflow compaction already propagated it. Preserve current behavior and keep changes additive: thread the resolved authProfileId through run -> attempt -> legacy compaction param builder without broad refactors. Add tests that prove the auth profile is included in afterTurn legacy params and that overflow compaction still passes it through run attempts. Keep existing APIs stable, and only adjust small type issues needed for strict compilation. * fix: remove duplicate imports from rebase * feat: add context-engine system prompt additions * fix(rebase): dedupe attempt import declarations * test: fix fetch mock typing in ollama autodiscovery * fix(test): add registerContextEngine to diffs extension mock APIs * test(windows): use path.delimiter in ios-team-id fixture PATH * test(cron): add model formatting and precedence edge case tests Covers: - Provider/model string splitting (whitespace, nested paths, empty segments) - Provider normalization (casing, aliases like bedrock→amazon-bedrock) - Anthropic model alias normalization (opus-4.5→claude-opus-4-5) - Precedence: job payload > session override > config default - Sequential runs with different providers (CI flake regression pattern) - forceNew session preserving stored model overrides - Whitespace/empty model string edge cases - Config model as string vs object format * test(cron): fix model formatting test config types * test(phone-control): add registerContextEngine to mock API * fix: re-export ChannelKind from config-reload-plan * fix: add subagent mock to plugin-runtime-mock test util * docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
context: gatewayRequestContext,
2026-01-14 01:08:15 +00:00
});
logGatewayStartup({
cfg: cfgAtStart,
bindHost,
bindHosts: httpBindHosts,
port,
2026-01-19 02:46:07 +00:00
tlsEnabled: gatewayTls.enabled,
log,
isNixMode,
});
const stopGatewayUpdateCheck = minimalTestGateway
? () => {}
: scheduleGatewayUpdateCheck({
cfg: cfgAtStart,
log,
isNixMode,
onUpdateAvailableChange: (updateAvailable) => {
const payload: GatewayUpdateAvailableEventPayload = { updateAvailable };
broadcast(GATEWAY_EVENT_UPDATE_AVAILABLE, payload, { dropIfSlow: true });
},
});
const tailscaleCleanup = minimalTestGateway
? null
: await startGatewayTailscaleExposure({
tailscaleMode,
resetOnExit: tailscaleConfig.resetOnExit,
port,
controlUiBasePath,
logTailscale,
});
2026-01-14 01:08:15 +00:00
let browserControl: Awaited<ReturnType<typeof startBrowserControlServerIfEnabled>> = null;
if (!minimalTestGateway) {
({ browserControl, pluginServices } = await startGatewaySidecars({
cfg: cfgAtStart,
pluginRegistry,
defaultWorkspaceDir,
deps,
startChannels,
log,
logHooks,
logChannels,
logBrowser,
}));
}
2026-01-14 01:08:15 +00:00
// Run gateway_start plugin hook (fire-and-forget)
if (!minimalTestGateway) {
const hookRunner = getGlobalHookRunner();
if (hookRunner?.hasHooks("gateway_start")) {
void hookRunner.runGatewayStart({ port }, { port }).catch((err) => {
log.warn(`gateway_start hook failed: ${String(err)}`);
});
}
}
const configReloader = minimalTestGateway
? { stop: async () => {} }
: (() => {
const { applyHotReload, requestGatewayRestart } = createGatewayReloadHandlers({
deps,
broadcast,
getState: () => ({
hooksConfig,
hookClientIpConfig,
heartbeatRunner,
cronState,
browserControl,
channelHealthMonitor,
}),
setState: (nextState) => {
hooksConfig = nextState.hooksConfig;
hookClientIpConfig = nextState.hookClientIpConfig;
heartbeatRunner = nextState.heartbeatRunner;
cronState = nextState.cronState;
cron = cronState.cron;
cronStorePath = cronState.storePath;
browserControl = nextState.browserControl;
channelHealthMonitor = nextState.channelHealthMonitor;
},
startChannel,
stopChannel,
logHooks,
logBrowser,
logChannels,
logCron,
logReload,
createHealthMonitor: (checkIntervalMs: number) =>
startChannelHealthMonitor({ channelManager, checkIntervalMs }),
});
return startGatewayConfigReloader({
initialConfig: cfgAtStart,
readSnapshot: readConfigFileSnapshot,
onHotReload: async (plan, nextConfig) => {
const previousSnapshot = getActiveSecretsRuntimeSnapshot();
const prepared = await activateRuntimeSecrets(nextConfig, {
reason: "reload",
activate: true,
});
try {
await applyHotReload(plan, prepared.config);
} catch (err) {
if (previousSnapshot) {
activateSecretsRuntimeSnapshot(previousSnapshot);
} else {
clearSecretsRuntimeSnapshot();
}
throw err;
}
},
onRestart: async (plan, nextConfig) => {
await activateRuntimeSecrets(nextConfig, { reason: "restart-check", activate: false });
requestGatewayRestart(plan, nextConfig);
},
log: {
info: (msg) => logReload.info(msg),
warn: (msg) => logReload.warn(msg),
error: (msg) => logReload.error(msg),
},
watchPath: CONFIG_PATH,
});
})();
2026-01-14 01:08:15 +00:00
const close = createGatewayCloseHandler({
bonjourStop,
tailscaleCleanup,
canvasHost,
canvasHostServer,
stopChannel,
pluginServices,
cron,
heartbeatRunner,
updateCheckStop: stopGatewayUpdateCheck,
nodePresenceTimers,
broadcast,
tickInterval,
healthInterval,
dedupeCleanup,
mediaCleanup,
agentUnsub,
heartbeatUnsub,
transcriptUnsub,
chatRunState,
clients,
configReloader,
browserControl,
wss,
httpServer,
httpServers,
});
2026-01-21 00:29:42 +00:00
return {
close: async (opts) => {
// Run gateway_stop plugin hook before shutdown
await runGlobalGatewayStopSafely({
event: { reason: opts?.reason ?? "gateway stopping" },
ctx: { port },
onError: (err) => log.warn(`gateway_stop hook failed: ${String(err)}`),
});
2026-01-21 00:29:42 +00:00
if (diagnosticsEnabled) {
stopDiagnosticHeartbeat();
}
if (skillsRefreshTimer) {
clearTimeout(skillsRefreshTimer);
skillsRefreshTimer = null;
}
skillsChangeUnsub();
authRateLimiter?.dispose();
browserAuthRateLimiter.dispose();
channelHealthMonitor?.stop();
clearSecretsRuntimeSnapshot();
2026-01-21 00:29:42 +00:00
await close(opts);
},
};
2026-01-14 01:08:15 +00:00
}