@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,19 +1,24 @@
1
1
  import { createServer as createHttpServer, } from "node:http";
2
2
  import { createServer as createHttpsServer } from "node:https";
3
+ import { resolveAgentAvatar } from "../agents/identity-avatar.js";
3
4
  import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH, handleA2uiHttpRequest, } from "../canvas-host/a2ui.js";
4
5
  import { loadConfig } from "../config/config.js";
5
- import { resolveAgentAvatar } from "../agents/identity-avatar.js";
6
+ import { safeEqualSecret } from "../security/secret-equal.js";
6
7
  import { handleSlackHttpRequest } from "../slack/http/index.js";
7
- import { authorizeGatewayConnect, isLocalDirectRequest } from "./auth.js";
8
+ import { authorizeGatewayConnect, isLocalDirectRequest, } from "./auth.js";
9
+ import { CANVAS_CAPABILITY_TTL_MS, normalizeCanvasScopedUrl } from "./canvas-capability.js";
8
10
  import { handleControlUiAvatarRequest, handleControlUiHttpRequest, } from "./control-ui.js";
9
- import { extractHookToken, getHookAgentPolicyError, getHookChannelError, isHookAgentAllowed, normalizeAgentPayload, normalizeHookHeaders, normalizeWakePayload, readJsonBody, resolveHookTargetAgentId, resolveHookChannel, resolveHookDeliver, } from "./hooks.js";
10
11
  import { applyHookMappings } from "./hooks-mapping.js";
11
- import { sendUnauthorized } from "./http-common.js";
12
- import { getBearerToken, getHeader } from "./http-utils.js";
13
- import { resolveGatewayClientIp } from "./net.js";
12
+ import { extractHookToken, getHookAgentPolicyError, getHookChannelError, isHookAgentAllowed, normalizeAgentPayload, normalizeHookHeaders, normalizeWakePayload, readJsonBody, resolveHookSessionKey, resolveHookTargetAgentId, resolveHookChannel, resolveHookDeliver, } from "./hooks.js";
13
+ import { sendGatewayAuthFailure, setDefaultSecurityHeaders } from "./http-common.js";
14
+ import { getBearerToken } from "./http-utils.js";
14
15
  import { handleOpenAiHttpRequest } from "./openai-http.js";
15
16
  import { handleOpenResponsesHttpRequest } from "./openresponses-http.js";
17
+ import { GATEWAY_CLIENT_MODES, normalizeGatewayClientMode } from "./protocol/client-info.js";
16
18
  import { handleToolsInvokeHttpRequest } from "./tools-invoke-http.js";
19
+ const HOOK_AUTH_FAILURE_LIMIT = 20;
20
+ const HOOK_AUTH_FAILURE_WINDOW_MS = 60_000;
21
+ const HOOK_AUTH_FAILURE_TRACK_MAX = 2048;
17
22
  function sendJson(res, status, body) {
18
23
  res.statusCode = status;
19
24
  res.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -26,19 +31,41 @@ function isCanvasPath(pathname) {
26
31
  pathname.startsWith(`${CANVAS_HOST_PATH}/`) ||
27
32
  pathname === CANVAS_WS_PATH);
28
33
  }
29
- function hasAuthorizedWsClientForIp(clients, clientIp) {
34
+ function isNodeWsClient(client) {
35
+ if (client.connect.role === "node") {
36
+ return true;
37
+ }
38
+ return normalizeGatewayClientMode(client.connect.client.mode) === GATEWAY_CLIENT_MODES.NODE;
39
+ }
40
+ function hasAuthorizedNodeWsClientForCanvasCapability(clients, capability) {
41
+ const nowMs = Date.now();
30
42
  for (const client of clients) {
31
- if (client.clientIp && client.clientIp === clientIp) {
43
+ if (!isNodeWsClient(client)) {
44
+ continue;
45
+ }
46
+ if (!client.canvasCapability || !client.canvasCapabilityExpiresAtMs) {
47
+ continue;
48
+ }
49
+ if (client.canvasCapabilityExpiresAtMs <= nowMs) {
50
+ continue;
51
+ }
52
+ if (safeEqualSecret(client.canvasCapability, capability)) {
53
+ // Sliding expiration while the connected node keeps using canvas.
54
+ client.canvasCapabilityExpiresAtMs = nowMs + CANVAS_CAPABILITY_TTL_MS;
32
55
  return true;
33
56
  }
34
57
  }
35
58
  return false;
36
59
  }
37
60
  async function authorizeCanvasRequest(params) {
38
- const { req, auth, trustedProxies, clients } = params;
61
+ const { req, auth, trustedProxies, clients, canvasCapability, malformedScopedPath, rateLimiter } = params;
62
+ if (malformedScopedPath) {
63
+ return { ok: false, reason: "unauthorized" };
64
+ }
39
65
  if (isLocalDirectRequest(req, trustedProxies)) {
40
- return true;
66
+ return { ok: true };
41
67
  }
68
+ let lastAuthFailure = null;
42
69
  const token = getBearerToken(req);
43
70
  if (token) {
44
71
  const authResult = await authorizeGatewayConnect({
@@ -46,47 +73,124 @@ async function authorizeCanvasRequest(params) {
46
73
  connectAuth: { token, password: token },
47
74
  req,
48
75
  trustedProxies,
76
+ rateLimiter,
49
77
  });
50
78
  if (authResult.ok) {
51
- return true;
79
+ return authResult;
52
80
  }
81
+ lastAuthFailure = authResult;
53
82
  }
54
- const clientIp = resolveGatewayClientIp({
55
- remoteAddr: req.socket?.remoteAddress ?? "",
56
- forwardedFor: getHeader(req, "x-forwarded-for"),
57
- realIp: getHeader(req, "x-real-ip"),
58
- trustedProxies,
59
- });
60
- if (!clientIp) {
61
- return false;
83
+ if (canvasCapability && hasAuthorizedNodeWsClientForCanvasCapability(clients, canvasCapability)) {
84
+ return { ok: true };
85
+ }
86
+ return lastAuthFailure ?? { ok: false, reason: "unauthorized" };
87
+ }
88
+ function writeUpgradeAuthFailure(socket, auth) {
89
+ if (auth.rateLimited) {
90
+ const retryAfterSeconds = auth.retryAfterMs && auth.retryAfterMs > 0 ? Math.ceil(auth.retryAfterMs / 1000) : undefined;
91
+ socket.write([
92
+ "HTTP/1.1 429 Too Many Requests",
93
+ retryAfterSeconds ? `Retry-After: ${retryAfterSeconds}` : undefined,
94
+ "Content-Type: application/json; charset=utf-8",
95
+ "Connection: close",
96
+ "",
97
+ JSON.stringify({
98
+ error: {
99
+ message: "Too many failed authentication attempts. Please try again later.",
100
+ type: "rate_limited",
101
+ },
102
+ }),
103
+ ]
104
+ .filter(Boolean)
105
+ .join("\r\n"));
106
+ return;
62
107
  }
63
- return hasAuthorizedWsClientForIp(clients, clientIp);
108
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
64
109
  }
65
110
  export function createHooksRequestHandler(opts) {
66
111
  const { getHooksConfig, bindHost, port, logHooks, dispatchAgentHook, dispatchWakeHook } = opts;
112
+ const hookAuthFailures = new Map();
113
+ const resolveHookClientKey = (req) => {
114
+ return req.socket?.remoteAddress?.trim() || "unknown";
115
+ };
116
+ const recordHookAuthFailure = (clientKey, nowMs) => {
117
+ if (!hookAuthFailures.has(clientKey) && hookAuthFailures.size >= HOOK_AUTH_FAILURE_TRACK_MAX) {
118
+ // Prune expired entries instead of clearing all state.
119
+ for (const [key, entry] of hookAuthFailures) {
120
+ if (nowMs - entry.windowStartedAtMs >= HOOK_AUTH_FAILURE_WINDOW_MS) {
121
+ hookAuthFailures.delete(key);
122
+ }
123
+ }
124
+ // If still at capacity after pruning, drop the oldest half.
125
+ if (hookAuthFailures.size >= HOOK_AUTH_FAILURE_TRACK_MAX) {
126
+ let toRemove = Math.floor(hookAuthFailures.size / 2);
127
+ for (const key of hookAuthFailures.keys()) {
128
+ if (toRemove <= 0) {
129
+ break;
130
+ }
131
+ hookAuthFailures.delete(key);
132
+ toRemove--;
133
+ }
134
+ }
135
+ }
136
+ const current = hookAuthFailures.get(clientKey);
137
+ const expired = !current || nowMs - current.windowStartedAtMs >= HOOK_AUTH_FAILURE_WINDOW_MS;
138
+ const next = expired
139
+ ? { count: 1, windowStartedAtMs: nowMs }
140
+ : { count: current.count + 1, windowStartedAtMs: current.windowStartedAtMs };
141
+ // Delete-before-set refreshes Map insertion order so recently-active
142
+ // clients are not evicted before dormant ones during oldest-half eviction.
143
+ if (hookAuthFailures.has(clientKey)) {
144
+ hookAuthFailures.delete(clientKey);
145
+ }
146
+ hookAuthFailures.set(clientKey, next);
147
+ if (next.count <= HOOK_AUTH_FAILURE_LIMIT) {
148
+ return { throttled: false };
149
+ }
150
+ const retryAfterMs = Math.max(1, next.windowStartedAtMs + HOOK_AUTH_FAILURE_WINDOW_MS - nowMs);
151
+ return {
152
+ throttled: true,
153
+ retryAfterSeconds: Math.ceil(retryAfterMs / 1000),
154
+ };
155
+ };
156
+ const clearHookAuthFailure = (clientKey) => {
157
+ hookAuthFailures.delete(clientKey);
158
+ };
67
159
  return async (req, res) => {
68
160
  const hooksConfig = getHooksConfig();
69
- if (!hooksConfig)
161
+ if (!hooksConfig) {
70
162
  return false;
163
+ }
71
164
  const url = new URL(req.url ?? "/", `http://${bindHost}:${port}`);
72
165
  const basePath = hooksConfig.basePath;
73
166
  if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
74
167
  return false;
75
168
  }
76
- // pool-bot keeps the deprecation-warning approach for query-param tokens
77
- // (upstream hard-rejects; we warn and allow for now)
78
- const { token, fromQuery } = extractHookToken(req, url);
79
- if (!token || token !== hooksConfig.token) {
169
+ if (url.searchParams.has("token")) {
170
+ res.statusCode = 400;
171
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
172
+ res.end("Hook token must be provided via Authorization: Bearer <token> or X-PoolBot-Token header (query parameters are not allowed).");
173
+ return true;
174
+ }
175
+ const token = extractHookToken(req).token;
176
+ const clientKey = resolveHookClientKey(req);
177
+ if (!safeEqualSecret(token, hooksConfig.token)) {
178
+ const throttle = recordHookAuthFailure(clientKey, Date.now());
179
+ if (throttle.throttled) {
180
+ const retryAfter = throttle.retryAfterSeconds ?? 1;
181
+ res.statusCode = 429;
182
+ res.setHeader("Retry-After", String(retryAfter));
183
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
184
+ res.end("Too Many Requests");
185
+ logHooks.warn(`hook auth throttled for ${clientKey}; retry-after=${retryAfter}s`);
186
+ return true;
187
+ }
80
188
  res.statusCode = 401;
81
189
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
82
190
  res.end("Unauthorized");
83
191
  return true;
84
192
  }
85
- if (fromQuery) {
86
- logHooks.warn("Hook token provided via query parameter is deprecated for security reasons. " +
87
- "Tokens in URLs appear in logs, browser history, and referrer headers. " +
88
- "Use Authorization: Bearer <token> or X-Poolbot-Token header instead.");
89
- }
193
+ clearHookAuthFailure(clientKey);
90
194
  if (req.method !== "POST") {
91
195
  res.statusCode = 405;
92
196
  res.setHeader("Allow", "POST");
@@ -103,7 +207,11 @@ export function createHooksRequestHandler(opts) {
103
207
  }
104
208
  const body = await readJsonBody(req, hooksConfig.maxBodyBytes);
105
209
  if (!body.ok) {
106
- const status = body.error === "payload too large" ? 413 : 400;
210
+ const status = body.error === "payload too large"
211
+ ? 413
212
+ : body.error === "request body timeout"
213
+ ? 408
214
+ : 400;
107
215
  sendJson(res, status, { ok: false, error: body.error });
108
216
  return true;
109
217
  }
@@ -129,8 +237,18 @@ export function createHooksRequestHandler(opts) {
129
237
  sendJson(res, 400, { ok: false, error: getHookAgentPolicyError() });
130
238
  return true;
131
239
  }
240
+ const sessionKey = resolveHookSessionKey({
241
+ hooksConfig,
242
+ source: "request",
243
+ sessionKey: normalized.value.sessionKey,
244
+ });
245
+ if (!sessionKey.ok) {
246
+ sendJson(res, 400, { ok: false, error: sessionKey.error });
247
+ return true;
248
+ }
132
249
  const runId = dispatchAgentHook({
133
250
  ...normalized.value,
251
+ sessionKey: sessionKey.value,
134
252
  agentId: resolveHookTargetAgentId(hooksConfig, normalized.value.agentId),
135
253
  });
136
254
  sendJson(res, 202, { ok: true, runId });
@@ -171,12 +289,21 @@ export function createHooksRequestHandler(opts) {
171
289
  sendJson(res, 400, { ok: false, error: getHookAgentPolicyError() });
172
290
  return true;
173
291
  }
292
+ const sessionKey = resolveHookSessionKey({
293
+ hooksConfig,
294
+ source: "mapping",
295
+ sessionKey: mapped.action.sessionKey,
296
+ });
297
+ if (!sessionKey.ok) {
298
+ sendJson(res, 400, { ok: false, error: sessionKey.error });
299
+ return true;
300
+ }
174
301
  const runId = dispatchAgentHook({
175
302
  message: mapped.action.message,
176
303
  name: mapped.action.name ?? "Hook",
177
304
  agentId: resolveHookTargetAgentId(hooksConfig, mapped.action.agentId),
178
305
  wakeMode: mapped.action.wakeMode,
179
- sessionKey: mapped.action.sessionKey ?? "",
306
+ sessionKey: sessionKey.value,
180
307
  deliver: resolveHookDeliver(mapped.action.deliver),
181
308
  channel,
182
309
  to: mapped.action.to,
@@ -202,7 +329,7 @@ export function createHooksRequestHandler(opts) {
202
329
  };
203
330
  }
204
331
  export function createGatewayHttpServer(opts) {
205
- const { canvasHost, clients, controlUiEnabled, controlUiBasePath, controlUiRoot, openAiChatCompletionsEnabled, openResponsesEnabled, openResponsesConfig, handleHooksRequest, handlePluginRequest, resolvedAuth, } = opts;
332
+ const { canvasHost, clients, controlUiEnabled, controlUiBasePath, controlUiRoot, openAiChatCompletionsEnabled, openResponsesEnabled, openResponsesConfig, handleHooksRequest, handlePluginRequest, resolvedAuth, rateLimiter, } = opts;
206
333
  const httpServer = opts.tlsOptions
207
334
  ? createHttpsServer(opts.tlsOptions, (req, res) => {
208
335
  void handleRequest(req, res);
@@ -211,69 +338,114 @@ export function createGatewayHttpServer(opts) {
211
338
  void handleRequest(req, res);
212
339
  });
213
340
  async function handleRequest(req, res) {
341
+ setDefaultSecurityHeaders(res);
214
342
  // Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
215
- if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket")
343
+ if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") {
216
344
  return;
345
+ }
217
346
  try {
218
347
  const configSnapshot = loadConfig();
219
348
  const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
220
- if (await handleHooksRequest(req, res))
349
+ const scopedCanvas = normalizeCanvasScopedUrl(req.url ?? "/");
350
+ if (scopedCanvas.malformedScopedPath) {
351
+ sendGatewayAuthFailure(res, { ok: false, reason: "unauthorized" });
352
+ return;
353
+ }
354
+ if (scopedCanvas.rewrittenUrl) {
355
+ req.url = scopedCanvas.rewrittenUrl;
356
+ }
357
+ const requestPath = new URL(req.url ?? "/", "http://localhost").pathname;
358
+ if (await handleHooksRequest(req, res)) {
221
359
  return;
360
+ }
222
361
  if (await handleToolsInvokeHttpRequest(req, res, {
223
362
  auth: resolvedAuth,
224
363
  trustedProxies,
225
- }))
226
- return;
227
- if (await handleSlackHttpRequest(req, res))
364
+ rateLimiter,
365
+ })) {
228
366
  return;
229
- if (handlePluginRequest && (await handlePluginRequest(req, res)))
367
+ }
368
+ if (await handleSlackHttpRequest(req, res)) {
230
369
  return;
370
+ }
371
+ if (handlePluginRequest) {
372
+ // Channel HTTP endpoints are gateway-auth protected by default.
373
+ // Non-channel plugin routes remain plugin-owned and must enforce
374
+ // their own auth when exposing sensitive functionality.
375
+ if (requestPath.startsWith("/api/channels/")) {
376
+ const token = getBearerToken(req);
377
+ const authResult = await authorizeGatewayConnect({
378
+ auth: resolvedAuth,
379
+ connectAuth: token ? { token, password: token } : null,
380
+ req,
381
+ trustedProxies,
382
+ rateLimiter,
383
+ });
384
+ if (!authResult.ok) {
385
+ sendGatewayAuthFailure(res, authResult);
386
+ return;
387
+ }
388
+ }
389
+ if (await handlePluginRequest(req, res)) {
390
+ return;
391
+ }
392
+ }
231
393
  if (openResponsesEnabled) {
232
394
  if (await handleOpenResponsesHttpRequest(req, res, {
233
395
  auth: resolvedAuth,
234
396
  config: openResponsesConfig,
235
397
  trustedProxies,
236
- }))
398
+ rateLimiter,
399
+ })) {
237
400
  return;
401
+ }
238
402
  }
239
403
  if (openAiChatCompletionsEnabled) {
240
404
  if (await handleOpenAiHttpRequest(req, res, {
241
405
  auth: resolvedAuth,
242
406
  trustedProxies,
243
- }))
407
+ rateLimiter,
408
+ })) {
244
409
  return;
410
+ }
245
411
  }
246
412
  if (canvasHost) {
247
- const url = new URL(req.url ?? "/", "http://localhost");
248
- if (isCanvasPath(url.pathname)) {
413
+ if (isCanvasPath(requestPath)) {
249
414
  const ok = await authorizeCanvasRequest({
250
415
  req,
251
416
  auth: resolvedAuth,
252
417
  trustedProxies,
253
418
  clients,
419
+ canvasCapability: scopedCanvas.capability,
420
+ malformedScopedPath: scopedCanvas.malformedScopedPath,
421
+ rateLimiter,
254
422
  });
255
- if (!ok) {
256
- sendUnauthorized(res);
423
+ if (!ok.ok) {
424
+ sendGatewayAuthFailure(res, ok);
257
425
  return;
258
426
  }
259
427
  }
260
- if (await handleA2uiHttpRequest(req, res))
428
+ if (await handleA2uiHttpRequest(req, res)) {
261
429
  return;
262
- if (await canvasHost.handleHttpRequest(req, res))
430
+ }
431
+ if (await canvasHost.handleHttpRequest(req, res)) {
263
432
  return;
433
+ }
264
434
  }
265
435
  if (controlUiEnabled) {
266
436
  if (handleControlUiAvatarRequest(req, res, {
267
437
  basePath: controlUiBasePath,
268
438
  resolveAvatar: (agentId) => resolveAgentAvatar(configSnapshot, agentId),
269
- }))
439
+ })) {
270
440
  return;
441
+ }
271
442
  if (handleControlUiHttpRequest(req, res, {
272
443
  basePath: controlUiBasePath,
273
444
  config: configSnapshot,
274
445
  root: controlUiRoot,
275
- }))
446
+ })) {
276
447
  return;
448
+ }
277
449
  }
278
450
  res.statusCode = 404;
279
451
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
@@ -288,9 +460,18 @@ export function createGatewayHttpServer(opts) {
288
460
  return httpServer;
289
461
  }
290
462
  export function attachGatewayUpgradeHandler(opts) {
291
- const { httpServer, wss, canvasHost, clients, resolvedAuth } = opts;
463
+ const { httpServer, wss, canvasHost, clients, resolvedAuth, rateLimiter } = opts;
292
464
  httpServer.on("upgrade", (req, socket, head) => {
293
465
  void (async () => {
466
+ const scopedCanvas = normalizeCanvasScopedUrl(req.url ?? "/");
467
+ if (scopedCanvas.malformedScopedPath) {
468
+ writeUpgradeAuthFailure(socket, { ok: false, reason: "unauthorized" });
469
+ socket.destroy();
470
+ return;
471
+ }
472
+ if (scopedCanvas.rewrittenUrl) {
473
+ req.url = scopedCanvas.rewrittenUrl;
474
+ }
294
475
  if (canvasHost) {
295
476
  const url = new URL(req.url ?? "/", "http://localhost");
296
477
  if (url.pathname === CANVAS_WS_PATH) {
@@ -301,9 +482,12 @@ export function attachGatewayUpgradeHandler(opts) {
301
482
  auth: resolvedAuth,
302
483
  trustedProxies,
303
484
  clients,
485
+ canvasCapability: scopedCanvas.capability,
486
+ malformedScopedPath: scopedCanvas.malformedScopedPath,
487
+ rateLimiter,
304
488
  });
305
- if (!ok) {
306
- socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
489
+ if (!ok.ok) {
490
+ writeUpgradeAuthFailure(socket, ok);
307
491
  socket.destroy();
308
492
  return;
309
493
  }