@poolzin/pool-bot 2026.2.23 → 2026.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -1,54 +1,66 @@
1
+ import path from "node:path";
1
2
  import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
2
- import { initSubagentRegistry } from "../agents/subagent-registry.js";
3
+ import { getActiveEmbeddedRunCount } from "../agents/pi-embedded-runner/runs.js";
3
4
  import { registerSkillsChangeListener } from "../agents/skills/refresh.js";
5
+ import { initSubagentRegistry } from "../agents/subagent-registry.js";
6
+ import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js";
4
7
  import { listChannelPlugins } from "../channels/plugins/index.js";
5
- import { createDefaultDeps } from "../cli/deps.js";
6
8
  import { formatCliCommand } from "../cli/command-format.js";
9
+ import { createDefaultDeps } from "../cli/deps.js";
10
+ import { isRestartEnabled } from "../config/commands.js";
7
11
  import { CONFIG_PATH, isNixMode, loadConfig, migrateLegacyConfig, readConfigFileSnapshot, writeConfigFile, } from "../config/config.js";
8
- import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
9
- import { logAcceptedEnvOption } from "../infra/env.js";
10
12
  import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
11
13
  import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
14
+ import { ensureControlUiAssetsBuilt, resolveControlUiRootOverrideSync, resolveControlUiRootSync, } from "../infra/control-ui-assets.js";
15
+ import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
16
+ import { logAcceptedEnvOption } from "../infra/env.js";
17
+ import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js";
12
18
  import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
13
19
  import { startHeartbeatRunner } from "../infra/heartbeat-runner.js";
14
20
  import { getMachineDisplayName } from "../infra/machine-name.js";
15
21
  import { ensurePoolbotCliOnPath } from "../infra/path-env.js";
22
+ import { setGatewaySigusr1RestartPolicy, setPreRestartDeferralCheck } from "../infra/restart.js";
16
23
  import { primeRemoteSkillsCache, refreshRemoteBinsForConnectedNodes, setSkillsRemoteRegistry, } from "../infra/skills-remote.js";
17
24
  import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
18
- import { setGatewaySigusr1RestartPolicy } from "../infra/restart.js";
19
25
  import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/diagnostic.js";
20
26
  import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
27
+ import { getGlobalHookRunner, runGlobalGatewayStopSafely } from "../plugins/hook-runner-global.js";
28
+ import { createEmptyPluginRegistry } from "../plugins/registry.js";
29
+ import { getTotalQueueSize } from "../process/command-queue.js";
21
30
  import { runOnboardingWizard } from "../wizard/onboarding.js";
31
+ import { createAuthRateLimiter } from "./auth-rate-limit.js";
32
+ import { startChannelHealthMonitor } from "./channel-health-monitor.js";
22
33
  import { startGatewayConfigReloader } from "./config-reload.js";
23
- import { getHealthCache, getHealthVersion, getPresenceVersion, incrementPresenceVersion, refreshGatewayHealthSnapshot, } from "./server/health-state.js";
24
- import { startGatewayDiscovery } from "./server-discovery-runtime.js";
34
+ import { GATEWAY_EVENT_UPDATE_AVAILABLE, } from "./events.js";
25
35
  import { ExecApprovalManager } from "./exec-approval-manager.js";
26
- import { createExecApprovalHandlers } from "./server-methods/exec-approval.js";
27
- import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js";
36
+ import { NodeRegistry } from "./node-registry.js";
28
37
  import { createChannelManager } from "./server-channels.js";
29
38
  import { createAgentEventHandler } from "./server-chat.js";
30
39
  import { createGatewayCloseHandler } from "./server-close.js";
31
40
  import { buildGatewayCronService } from "./server-cron.js";
41
+ import { startGatewayDiscovery } from "./server-discovery-runtime.js";
32
42
  import { applyGatewayLaneConcurrency } from "./server-lanes.js";
33
43
  import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
34
- import { coreGatewayHandlers } from "./server-methods.js";
35
44
  import { GATEWAY_EVENTS, listGatewayMethods } from "./server-methods-list.js";
45
+ import { coreGatewayHandlers } from "./server-methods.js";
46
+ import { createExecApprovalHandlers } from "./server-methods/exec-approval.js";
47
+ import { safeParseJson } from "./server-methods/nodes.helpers.js";
48
+ import { hasConnectedMobileNode } from "./server-mobile-nodes.js";
36
49
  import { loadGatewayModelCatalog } from "./server-model-catalog.js";
37
- import { NodeRegistry } from "./node-registry.js";
38
50
  import { createNodeSubscriptionManager } from "./server-node-subscriptions.js";
39
- import { safeParseJson } from "./server-methods/nodes.helpers.js";
40
51
  import { loadGatewayPlugins } from "./server-plugins.js";
41
52
  import { createGatewayReloadHandlers } from "./server-reload-handlers.js";
42
53
  import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js";
43
54
  import { createGatewayRuntimeState } from "./server-runtime-state.js";
44
- import { hasConnectedMobileNode } from "./server-mobile-nodes.js";
45
55
  import { resolveSessionKeyForRun } from "./server-session-key.js";
46
- import { startGatewaySidecars } from "./server-startup.js";
47
56
  import { logGatewayStartup } from "./server-startup-log.js";
57
+ import { startGatewaySidecars } from "./server-startup.js";
48
58
  import { startGatewayTailscaleExposure } from "./server-tailscale.js";
49
- import { loadGatewayTlsRuntime } from "./server/tls.js";
50
59
  import { createWizardSessionTracker } from "./server-wizard-sessions.js";
51
60
  import { attachGatewayWsHandlers } from "./server-ws-runtime.js";
61
+ import { getHealthCache, getHealthVersion, getPresenceVersion, incrementPresenceVersion, refreshGatewayHealthSnapshot, } from "./server/health-state.js";
62
+ import { loadGatewayTlsRuntime } from "./server/tls.js";
63
+ import { ensureGatewayStartupAuth } from "./startup-auth.js";
52
64
  export { __resetModelCatalogCacheForTest } from "./server-model-catalog.js";
53
65
  ensurePoolbotCliOnPath();
54
66
  const log = createSubsystemLogger("gateway");
@@ -63,8 +75,10 @@ const logReload = log.child("reload");
63
75
  const logHooks = log.child("hooks");
64
76
  const logPlugins = log.child("plugins");
65
77
  const logWsControl = log.child("ws");
78
+ const gatewayRuntime = runtimeForLogger(log);
66
79
  const canvasRuntime = runtimeForLogger(logCanvas);
67
80
  export async function startGatewayServer(port = 18789, opts = {}) {
81
+ const minimalTestGateway = process.env.VITEST === "1" && process.env.POOLBOT_TEST_MINIMAL_GATEWAY === "1";
68
82
  // Ensure all default port derivations (browser/canvas) see the actual runtime port.
69
83
  process.env.POOLBOT_GATEWAY_PORT = String(port);
70
84
  process.env.CLAWDBOT_GATEWAY_PORT = String(port);
@@ -113,23 +127,43 @@ export async function startGatewayServer(port = 18789, opts = {}) {
113
127
  log.warn(`gateway: failed to persist plugin auto-enable changes: ${String(err)}`);
114
128
  }
115
129
  }
116
- const cfgAtStart = loadConfig();
130
+ let cfgAtStart = loadConfig();
131
+ const authBootstrap = await ensureGatewayStartupAuth({
132
+ cfg: cfgAtStart,
133
+ env: process.env,
134
+ authOverride: opts.auth,
135
+ tailscaleOverride: opts.tailscale,
136
+ persist: true,
137
+ });
138
+ cfgAtStart = authBootstrap.cfg;
139
+ if (authBootstrap.generatedToken) {
140
+ if (authBootstrap.persistedGeneratedToken) {
141
+ log.info("Gateway auth token was missing. Generated a new token and saved it to config (gateway.auth.token).");
142
+ }
143
+ else {
144
+ 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 `poolbot config set gateway.auth.mode token` and `poolbot config set gateway.auth.token <token>`.");
145
+ }
146
+ }
117
147
  const diagnosticsEnabled = isDiagnosticsEnabled(cfgAtStart);
118
148
  if (diagnosticsEnabled) {
119
149
  startDiagnosticHeartbeat();
120
150
  }
121
- setGatewaySigusr1RestartPolicy({ allowExternal: cfgAtStart.commands?.restart === true });
151
+ setGatewaySigusr1RestartPolicy({ allowExternal: isRestartEnabled(cfgAtStart) });
152
+ setPreRestartDeferralCheck(() => getTotalQueueSize() + getTotalPendingReplies() + getActiveEmbeddedRunCount());
122
153
  initSubagentRegistry();
123
154
  const defaultAgentId = resolveDefaultAgentId(cfgAtStart);
124
155
  const defaultWorkspaceDir = resolveAgentWorkspaceDir(cfgAtStart, defaultAgentId);
125
156
  const baseMethods = listGatewayMethods();
126
- const { pluginRegistry, gatewayMethods: baseGatewayMethods } = loadGatewayPlugins({
127
- cfg: cfgAtStart,
128
- workspaceDir: defaultWorkspaceDir,
129
- log,
130
- coreGatewayHandlers,
131
- baseMethods,
132
- });
157
+ const emptyPluginRegistry = createEmptyPluginRegistry();
158
+ const { pluginRegistry, gatewayMethods: baseGatewayMethods } = minimalTestGateway
159
+ ? { pluginRegistry: emptyPluginRegistry, gatewayMethods: baseMethods }
160
+ : loadGatewayPlugins({
161
+ cfg: cfgAtStart,
162
+ workspaceDir: defaultWorkspaceDir,
163
+ log,
164
+ coreGatewayHandlers,
165
+ baseMethods,
166
+ });
133
167
  const channelLogs = Object.fromEntries(listChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]));
134
168
  const channelRuntimeEnvs = Object.fromEntries(Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]));
135
169
  const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
@@ -146,9 +180,46 @@ export async function startGatewayServer(port = 18789, opts = {}) {
146
180
  auth: opts.auth,
147
181
  tailscale: opts.tailscale,
148
182
  });
149
- const { bindHost, controlUiEnabled, openAiChatCompletionsEnabled, openResponsesEnabled, openResponsesConfig, controlUiBasePath, resolvedAuth, tailscaleConfig, tailscaleMode, } = runtimeConfig;
183
+ const { bindHost, controlUiEnabled, openAiChatCompletionsEnabled, openResponsesEnabled, openResponsesConfig, controlUiBasePath, controlUiRoot: controlUiRootOverride, resolvedAuth, tailscaleConfig, tailscaleMode, } = runtimeConfig;
150
184
  let hooksConfig = runtimeConfig.hooksConfig;
151
185
  const canvasHostEnabled = runtimeConfig.canvasHostEnabled;
186
+ // Create auth rate limiter only when explicitly configured.
187
+ const rateLimitConfig = cfgAtStart.gateway?.auth?.rateLimit;
188
+ const authRateLimiter = rateLimitConfig
189
+ ? createAuthRateLimiter(rateLimitConfig)
190
+ : undefined;
191
+ let controlUiRootState;
192
+ if (controlUiRootOverride) {
193
+ const resolvedOverride = resolveControlUiRootOverrideSync(controlUiRootOverride);
194
+ const resolvedOverridePath = path.resolve(controlUiRootOverride);
195
+ controlUiRootState = resolvedOverride
196
+ ? { kind: "resolved", path: resolvedOverride }
197
+ : { kind: "invalid", path: resolvedOverridePath };
198
+ if (!resolvedOverride) {
199
+ log.warn(`gateway: controlUi.root not found at ${resolvedOverridePath}`);
200
+ }
201
+ }
202
+ else if (controlUiEnabled) {
203
+ let resolvedRoot = resolveControlUiRootSync({
204
+ moduleUrl: import.meta.url,
205
+ argv1: process.argv[1],
206
+ cwd: process.cwd(),
207
+ });
208
+ if (!resolvedRoot) {
209
+ const ensureResult = await ensureControlUiAssetsBuilt(gatewayRuntime);
210
+ if (!ensureResult.ok && ensureResult.message) {
211
+ log.warn(`gateway: ${ensureResult.message}`);
212
+ }
213
+ resolvedRoot = resolveControlUiRootSync({
214
+ moduleUrl: import.meta.url,
215
+ argv1: process.argv[1],
216
+ cwd: process.cwd(),
217
+ });
218
+ }
219
+ controlUiRootState = resolvedRoot
220
+ ? { kind: "resolved", path: resolvedRoot }
221
+ : { kind: "missing" };
222
+ }
152
223
  const wizardRunner = opts.wizardRunner ?? runOnboardingWizard;
153
224
  const { wizardSessions, findRunningWizard, purgeWizardSession } = createWizardSessionTracker();
154
225
  const deps = createDefaultDeps();
@@ -157,16 +228,18 @@ export async function startGatewayServer(port = 18789, opts = {}) {
157
228
  if (cfgAtStart.gateway?.tls?.enabled && !gatewayTls.enabled) {
158
229
  throw new Error(gatewayTls.error ?? "gateway tls: failed to enable");
159
230
  }
160
- const { canvasHost, httpServer, httpServers, httpBindHosts, wss, clients, broadcast, agentRunSeq, dedupe, chatRunState, chatRunBuffers, chatDeltaSentAt, addChatRun, removeChatRun, chatAbortControllers, broadcastToConnIds, toolEventRecipients, } = await createGatewayRuntimeState({
231
+ const { canvasHost, httpServer, httpServers, httpBindHosts, wss, clients, broadcast, broadcastToConnIds, agentRunSeq, dedupe, chatRunState, chatRunBuffers, chatDeltaSentAt, addChatRun, removeChatRun, chatAbortControllers, toolEventRecipients, } = await createGatewayRuntimeState({
161
232
  cfg: cfgAtStart,
162
233
  bindHost,
163
234
  port,
164
235
  controlUiEnabled,
165
236
  controlUiBasePath,
237
+ controlUiRoot: controlUiRootState,
166
238
  openAiChatCompletionsEnabled,
167
239
  openResponsesEnabled,
168
240
  openResponsesConfig,
169
241
  resolvedAuth,
242
+ rateLimiter: authRateLimiter,
170
243
  gatewayTls,
171
244
  hooksConfig: () => hooksConfig,
172
245
  pluginRegistry,
@@ -209,68 +282,115 @@ export async function startGatewayServer(port = 18789, opts = {}) {
209
282
  channelRuntimeEnvs,
210
283
  });
211
284
  const { getRuntimeSnapshot, startChannels, startChannel, stopChannel, markChannelLoggedOut } = channelManager;
212
- const machineDisplayName = await getMachineDisplayName();
213
- const discovery = await startGatewayDiscovery({
214
- machineDisplayName,
215
- port,
216
- gatewayTls: gatewayTls.enabled
217
- ? { enabled: true, fingerprintSha256: gatewayTls.fingerprintSha256 }
218
- : undefined,
219
- wideAreaDiscoveryEnabled: cfgAtStart.discovery?.wideArea?.enabled === true,
220
- tailscaleMode,
221
- mdnsMode: cfgAtStart.discovery?.mdns?.mode,
222
- logDiscovery,
223
- });
224
- bonjourStop = discovery.bonjourStop;
225
- setSkillsRemoteRegistry(nodeRegistry);
226
- void primeRemoteSkillsCache();
285
+ if (!minimalTestGateway) {
286
+ const machineDisplayName = await getMachineDisplayName();
287
+ const discovery = await startGatewayDiscovery({
288
+ machineDisplayName,
289
+ port,
290
+ gatewayTls: gatewayTls.enabled
291
+ ? { enabled: true, fingerprintSha256: gatewayTls.fingerprintSha256 }
292
+ : undefined,
293
+ wideAreaDiscoveryEnabled: cfgAtStart.discovery?.wideArea?.enabled === true,
294
+ wideAreaDiscoveryDomain: cfgAtStart.discovery?.wideArea?.domain,
295
+ tailscaleMode,
296
+ mdnsMode: cfgAtStart.discovery?.mdns?.mode,
297
+ logDiscovery,
298
+ });
299
+ bonjourStop = discovery.bonjourStop;
300
+ }
301
+ if (!minimalTestGateway) {
302
+ setSkillsRemoteRegistry(nodeRegistry);
303
+ void primeRemoteSkillsCache();
304
+ }
227
305
  // Debounce skills-triggered node probes to avoid feedback loops and rapid-fire invokes.
228
306
  // Skills changes can happen in bursts (e.g., file watcher events), and each probe
229
307
  // takes time to complete. A 30-second delay ensures we batch changes together.
230
308
  let skillsRefreshTimer = null;
231
309
  const skillsRefreshDelayMs = 30_000;
232
- const skillsChangeUnsub = registerSkillsChangeListener((event) => {
233
- if (event.reason === "remote-node")
234
- return;
235
- if (skillsRefreshTimer)
236
- clearTimeout(skillsRefreshTimer);
237
- skillsRefreshTimer = setTimeout(() => {
238
- skillsRefreshTimer = null;
239
- const latest = loadConfig();
240
- void refreshRemoteBinsForConnectedNodes(latest);
241
- }, skillsRefreshDelayMs);
242
- });
243
- const { tickInterval, healthInterval, dedupeCleanup } = startGatewayMaintenanceTimers({
244
- broadcast,
245
- nodeSendToAllSubscribed,
246
- getPresenceVersion,
247
- getHealthVersion,
248
- refreshGatewayHealthSnapshot,
249
- logHealth,
250
- dedupe,
251
- chatAbortControllers,
252
- chatRunState,
253
- chatRunBuffers,
254
- chatDeltaSentAt,
255
- removeChatRun,
256
- agentRunSeq,
257
- nodeSendToSession,
258
- });
259
- const agentUnsub = onAgentEvent(createAgentEventHandler({
260
- broadcast,
261
- broadcastToConnIds,
262
- nodeSendToSession,
263
- agentRunSeq,
264
- chatRunState,
265
- resolveSessionKeyForRun,
266
- clearAgentRunContext,
267
- toolEventRecipients,
268
- }));
269
- const heartbeatUnsub = onHeartbeatEvent((evt) => {
270
- broadcast("heartbeat", evt, { dropIfSlow: true });
271
- });
272
- let heartbeatRunner = startHeartbeatRunner({ cfg: cfgAtStart });
273
- void cron.start().catch((err) => logCron.error(`failed to start: ${String(err)}`));
310
+ const skillsChangeUnsub = minimalTestGateway
311
+ ? () => { }
312
+ : registerSkillsChangeListener((event) => {
313
+ if (event.reason === "remote-node") {
314
+ return;
315
+ }
316
+ if (skillsRefreshTimer) {
317
+ clearTimeout(skillsRefreshTimer);
318
+ }
319
+ skillsRefreshTimer = setTimeout(() => {
320
+ skillsRefreshTimer = null;
321
+ const latest = loadConfig();
322
+ void refreshRemoteBinsForConnectedNodes(latest);
323
+ }, skillsRefreshDelayMs);
324
+ });
325
+ const noopInterval = () => setInterval(() => { }, 1 << 30);
326
+ let tickInterval = noopInterval();
327
+ let healthInterval = noopInterval();
328
+ let dedupeCleanup = noopInterval();
329
+ if (!minimalTestGateway) {
330
+ ({ tickInterval, healthInterval, dedupeCleanup } = startGatewayMaintenanceTimers({
331
+ broadcast,
332
+ nodeSendToAllSubscribed,
333
+ getPresenceVersion,
334
+ getHealthVersion,
335
+ refreshGatewayHealthSnapshot,
336
+ logHealth,
337
+ dedupe,
338
+ chatAbortControllers,
339
+ chatRunState,
340
+ chatRunBuffers,
341
+ chatDeltaSentAt,
342
+ removeChatRun,
343
+ agentRunSeq,
344
+ nodeSendToSession,
345
+ }));
346
+ }
347
+ const agentUnsub = minimalTestGateway
348
+ ? null
349
+ : onAgentEvent(createAgentEventHandler({
350
+ broadcast,
351
+ broadcastToConnIds,
352
+ nodeSendToSession,
353
+ agentRunSeq,
354
+ chatRunState,
355
+ resolveSessionKeyForRun,
356
+ clearAgentRunContext,
357
+ toolEventRecipients,
358
+ }));
359
+ const heartbeatUnsub = minimalTestGateway
360
+ ? null
361
+ : onHeartbeatEvent((evt) => {
362
+ broadcast("heartbeat", evt, { dropIfSlow: true });
363
+ });
364
+ let heartbeatRunner = minimalTestGateway
365
+ ? {
366
+ stop: () => { },
367
+ updateConfig: () => { },
368
+ }
369
+ : startHeartbeatRunner({ cfg: cfgAtStart });
370
+ const healthCheckMinutes = cfgAtStart.gateway?.channelHealthCheckMinutes;
371
+ const healthCheckDisabled = healthCheckMinutes === 0;
372
+ const channelHealthMonitor = healthCheckDisabled
373
+ ? null
374
+ : startChannelHealthMonitor({
375
+ channelManager,
376
+ checkIntervalMs: (healthCheckMinutes ?? 5) * 60_000,
377
+ });
378
+ if (!minimalTestGateway) {
379
+ void cron.start().catch((err) => logCron.error(`failed to start: ${String(err)}`));
380
+ }
381
+ // Recover pending outbound deliveries from previous crash/restart.
382
+ if (!minimalTestGateway) {
383
+ void (async () => {
384
+ const { recoverPendingDeliveries } = await import("../infra/outbound/delivery-queue.js");
385
+ const { deliverOutboundPayloads } = await import("../infra/outbound/deliver.js");
386
+ const logRecovery = log.child("delivery-recovery");
387
+ await recoverPendingDeliveries({
388
+ deliver: deliverOutboundPayloads,
389
+ log: logRecovery,
390
+ cfg: cfgAtStart,
391
+ });
392
+ })().catch((err) => log.error(`Delivery recovery failed: ${String(err)}`));
393
+ }
274
394
  const execApprovalManager = new ExecApprovalManager();
275
395
  const execApprovalForwarder = createExecApprovalForwarder();
276
396
  const execApprovalHandlers = createExecApprovalHandlers(execApprovalManager, {
@@ -285,6 +405,7 @@ export async function startGatewayServer(port = 18789, opts = {}) {
285
405
  canvasHostEnabled: Boolean(canvasHost),
286
406
  canvasHostServerPort,
287
407
  resolvedAuth,
408
+ rateLimiter: authRateLimiter,
288
409
  gatewayMethods,
289
410
  events: GATEWAY_EVENTS,
290
411
  logGateway: log,
@@ -299,6 +420,7 @@ export async function startGatewayServer(port = 18789, opts = {}) {
299
420
  deps,
300
421
  cron,
301
422
  cronStorePath,
423
+ execApprovalManager,
302
424
  loadGatewayModelCatalog,
303
425
  getHealthCache,
304
426
  refreshHealthSnapshot: refreshGatewayHealthSnapshot,
@@ -344,63 +466,90 @@ export async function startGatewayServer(port = 18789, opts = {}) {
344
466
  log,
345
467
  isNixMode,
346
468
  });
347
- scheduleGatewayUpdateCheck({ cfg: cfgAtStart, log, isNixMode });
348
- const tailscaleCleanup = await startGatewayTailscaleExposure({
349
- tailscaleMode,
350
- resetOnExit: tailscaleConfig.resetOnExit,
351
- port,
352
- controlUiBasePath,
353
- logTailscale,
354
- });
469
+ if (!minimalTestGateway) {
470
+ scheduleGatewayUpdateCheck({
471
+ cfg: cfgAtStart,
472
+ log,
473
+ isNixMode,
474
+ onUpdateAvailableChange: (updateAvailable) => {
475
+ const payload = { updateAvailable };
476
+ broadcast(GATEWAY_EVENT_UPDATE_AVAILABLE, payload, { dropIfSlow: true });
477
+ },
478
+ });
479
+ }
480
+ const tailscaleCleanup = minimalTestGateway
481
+ ? null
482
+ : await startGatewayTailscaleExposure({
483
+ tailscaleMode,
484
+ resetOnExit: tailscaleConfig.resetOnExit,
485
+ port,
486
+ controlUiBasePath,
487
+ logTailscale,
488
+ });
355
489
  let browserControl = null;
356
- ({ browserControl, pluginServices } = await startGatewaySidecars({
357
- cfg: cfgAtStart,
358
- pluginRegistry,
359
- defaultWorkspaceDir,
360
- deps,
361
- startChannels,
362
- log,
363
- logHooks,
364
- logChannels,
365
- logBrowser,
366
- }));
367
- const { applyHotReload, requestGatewayRestart } = createGatewayReloadHandlers({
368
- deps,
369
- broadcast,
370
- getState: () => ({
371
- hooksConfig,
372
- heartbeatRunner,
373
- cronState,
374
- browserControl,
375
- }),
376
- setState: (nextState) => {
377
- hooksConfig = nextState.hooksConfig;
378
- heartbeatRunner = nextState.heartbeatRunner;
379
- cronState = nextState.cronState;
380
- cron = cronState.cron;
381
- cronStorePath = cronState.storePath;
382
- browserControl = nextState.browserControl;
383
- },
384
- startChannel,
385
- stopChannel,
386
- logHooks,
387
- logBrowser,
388
- logChannels,
389
- logCron,
390
- logReload,
391
- });
392
- const configReloader = startGatewayConfigReloader({
393
- initialConfig: cfgAtStart,
394
- readSnapshot: readConfigFileSnapshot,
395
- onHotReload: applyHotReload,
396
- onRestart: requestGatewayRestart,
397
- log: {
398
- info: (msg) => logReload.info(msg),
399
- warn: (msg) => logReload.warn(msg),
400
- error: (msg) => logReload.error(msg),
401
- },
402
- watchPath: CONFIG_PATH,
403
- });
490
+ if (!minimalTestGateway) {
491
+ ({ browserControl, pluginServices } = await startGatewaySidecars({
492
+ cfg: cfgAtStart,
493
+ pluginRegistry,
494
+ defaultWorkspaceDir,
495
+ deps,
496
+ startChannels,
497
+ log,
498
+ logHooks,
499
+ logChannels,
500
+ logBrowser,
501
+ }));
502
+ }
503
+ // Run gateway_start plugin hook (fire-and-forget)
504
+ if (!minimalTestGateway) {
505
+ const hookRunner = getGlobalHookRunner();
506
+ if (hookRunner?.hasHooks("gateway_start")) {
507
+ void hookRunner.runGatewayStart({ port }, { port }).catch((err) => {
508
+ log.warn(`gateway_start hook failed: ${String(err)}`);
509
+ });
510
+ }
511
+ }
512
+ const configReloader = minimalTestGateway
513
+ ? { stop: async () => { } }
514
+ : (() => {
515
+ const { applyHotReload, requestGatewayRestart } = createGatewayReloadHandlers({
516
+ deps,
517
+ broadcast,
518
+ getState: () => ({
519
+ hooksConfig,
520
+ heartbeatRunner,
521
+ cronState,
522
+ browserControl,
523
+ }),
524
+ setState: (nextState) => {
525
+ hooksConfig = nextState.hooksConfig;
526
+ heartbeatRunner = nextState.heartbeatRunner;
527
+ cronState = nextState.cronState;
528
+ cron = cronState.cron;
529
+ cronStorePath = cronState.storePath;
530
+ browserControl = nextState.browserControl;
531
+ },
532
+ startChannel,
533
+ stopChannel,
534
+ logHooks,
535
+ logBrowser,
536
+ logChannels,
537
+ logCron,
538
+ logReload,
539
+ });
540
+ return startGatewayConfigReloader({
541
+ initialConfig: cfgAtStart,
542
+ readSnapshot: readConfigFileSnapshot,
543
+ onHotReload: applyHotReload,
544
+ onRestart: requestGatewayRestart,
545
+ log: {
546
+ info: (msg) => logReload.info(msg),
547
+ warn: (msg) => logReload.warn(msg),
548
+ error: (msg) => logReload.error(msg),
549
+ },
550
+ watchPath: CONFIG_PATH,
551
+ });
552
+ })();
404
553
  const close = createGatewayCloseHandler({
405
554
  bonjourStop,
406
555
  tailscaleCleanup,
@@ -427,6 +576,12 @@ export async function startGatewayServer(port = 18789, opts = {}) {
427
576
  });
428
577
  return {
429
578
  close: async (opts) => {
579
+ // Run gateway_stop plugin hook before shutdown
580
+ await runGlobalGatewayStopSafely({
581
+ event: { reason: opts?.reason ?? "gateway stopping" },
582
+ ctx: { port },
583
+ onError: (err) => log.warn(`gateway_stop hook failed: ${String(err)}`),
584
+ });
430
585
  if (diagnosticsEnabled) {
431
586
  stopDiagnosticHeartbeat();
432
587
  }
@@ -435,6 +590,8 @@ export async function startGatewayServer(port = 18789, opts = {}) {
435
590
  skillsRefreshTimer = null;
436
591
  }
437
592
  skillsChangeUnsub();
593
+ authRateLimiter?.dispose();
594
+ channelHealthMonitor?.stop();
438
595
  await close(opts);
439
596
  },
440
597
  };
@@ -0,0 +1,11 @@
1
+ export function createToolSummaryPreviewTranscriptLines(sessionId) {
2
+ return [
3
+ JSON.stringify({ type: "session", version: 1, id: sessionId }),
4
+ JSON.stringify({ message: { role: "user", content: "Hello" } }),
5
+ JSON.stringify({ message: { role: "assistant", content: "Hi" } }),
6
+ JSON.stringify({
7
+ message: { role: "assistant", content: [{ type: "toolcall", name: "weather" }] },
8
+ }),
9
+ JSON.stringify({ message: { role: "assistant", content: "Forecast ready" } }),
10
+ ];
11
+ }