import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { getModelRefStatus, resolveConfiguredModelRef, resolveHooksGmailModel, } from "../agents/model-selection.js"; import type { CliDeps } from "../cli/deps.js"; import type { loadConfig } from "../config/config.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { startGmailWatcher } from "../hooks/gmail-watcher.js"; import { clearInternalHooks, createInternalHookEvent, triggerInternalHook, } from "../hooks/internal-hooks.js"; import { loadInternalHooks } from "../hooks/loader.js"; import type { loadClawdbotPlugins } from "../plugins/loader.js"; import { type PluginServicesHandle, startPluginServices } from "../plugins/services.js"; import { startBrowserControlServerIfEnabled } from "./server-browser.js"; import { scheduleRestartSentinelWake, shouldWakeFromRestartSentinel, } from "./server-restart-sentinel.js"; export async function startGatewaySidecars(params: { cfg: ReturnType; pluginRegistry: ReturnType; defaultWorkspaceDir: string; deps: CliDeps; startChannels: () => Promise; log: { warn: (msg: string) => void }; logHooks: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void; }; logChannels: { info: (msg: string) => void; error: (msg: string) => void }; logBrowser: { error: (msg: string) => void }; }) { // Start clawd browser control server (unless disabled via config). let browserControl: Awaited> = null; try { browserControl = await startBrowserControlServerIfEnabled(); } catch (err) { params.logBrowser.error(`server failed to start: ${String(err)}`); } // Start Gmail watcher if configured (hooks.gmail.account). if (!isTruthyEnvValue(process.env.CLAWDBOT_SKIP_GMAIL_WATCHER)) { try { const gmailResult = await startGmailWatcher(params.cfg); if (gmailResult.started) { params.logHooks.info("gmail watcher started"); } else if ( gmailResult.reason && gmailResult.reason !== "hooks not enabled" && gmailResult.reason !== "no gmail account configured" ) { params.logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`); } } catch (err) { params.logHooks.error(`gmail watcher failed to start: ${String(err)}`); } } // Validate hooks.gmail.model if configured. if (params.cfg.hooks?.gmail?.model) { const hooksModelRef = resolveHooksGmailModel({ cfg: params.cfg, defaultProvider: DEFAULT_PROVIDER, }); if (hooksModelRef) { const { provider: defaultProvider, model: defaultModel } = resolveConfiguredModelRef({ cfg: params.cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const catalog = await loadModelCatalog({ config: params.cfg }); const status = getModelRefStatus({ cfg: params.cfg, catalog, ref: hooksModelRef, defaultProvider, defaultModel, }); if (!status.allowed) { params.logHooks.warn( `hooks.gmail.model "${status.key}" not in agents.defaults.models allowlist (will use primary instead)`, ); } if (!status.inCatalog) { params.logHooks.warn( `hooks.gmail.model "${status.key}" not in the model catalog (may fail at runtime)`, ); } } } // Load internal hook handlers from configuration and directory discovery. try { // Clear any previously registered hooks to ensure fresh loading clearInternalHooks(); const loadedCount = await loadInternalHooks(params.cfg, params.defaultWorkspaceDir); if (loadedCount > 0) { params.logHooks.info( `loaded ${loadedCount} internal hook handler${loadedCount > 1 ? "s" : ""}`, ); } } catch (err) { params.logHooks.error(`failed to load hooks: ${String(err)}`); } // Launch configured channels so gateway replies via the surface the message came from. // Tests can opt out via CLAWDBOT_SKIP_CHANNELS (or legacy CLAWDBOT_SKIP_PROVIDERS). const skipChannels = isTruthyEnvValue(process.env.CLAWDBOT_SKIP_CHANNELS) || isTruthyEnvValue(process.env.CLAWDBOT_SKIP_PROVIDERS); if (!skipChannels) { try { await params.startChannels(); } catch (err) { params.logChannels.error(`channel startup failed: ${String(err)}`); } } else { params.logChannels.info( "skipping channel start (CLAWDBOT_SKIP_CHANNELS=1 or CLAWDBOT_SKIP_PROVIDERS=1)", ); } if (params.cfg.hooks?.internal?.enabled) { setTimeout(() => { const hookEvent = createInternalHookEvent("gateway", "startup", "gateway:startup", { cfg: params.cfg, deps: params.deps, workspaceDir: params.defaultWorkspaceDir, }); void triggerInternalHook(hookEvent); }, 250); } let pluginServices: PluginServicesHandle | null = null; try { pluginServices = await startPluginServices({ registry: params.pluginRegistry, config: params.cfg, workspaceDir: params.defaultWorkspaceDir, }); } catch (err) { params.log.warn(`plugin services failed to start: ${String(err)}`); } if (shouldWakeFromRestartSentinel()) { setTimeout(() => { void scheduleRestartSentinelWake({ deps: params.deps }); }, 750); } return { browserControl, pluginServices }; }