@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,14 +1,16 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js";
3
3
  import { listChannelPlugins } from "../channels/plugins/index.js";
4
+ import { readJsonBodyWithLimit, requestBodyErrorToText } from "../infra/http-body.js";
4
5
  import { normalizeAgentId } from "../routing/session-key.js";
5
6
  import { normalizeMessageChannel } from "../utils/message-channel.js";
6
7
  import { resolveHookMappings } from "./hooks-mapping.js";
7
8
  const DEFAULT_HOOKS_PATH = "/hooks";
8
9
  const DEFAULT_HOOKS_MAX_BODY_BYTES = 256 * 1024;
9
10
  export function resolveHooksConfig(cfg) {
10
- if (cfg.hooks?.enabled !== true)
11
+ if (cfg.hooks?.enabled !== true) {
11
12
  return null;
13
+ }
12
14
  const token = cfg.hooks?.token?.trim();
13
15
  if (!token) {
14
16
  throw new Error("hooks.enabled requires hooks.token");
@@ -26,6 +28,18 @@ export function resolveHooksConfig(cfg) {
26
28
  const defaultAgentId = resolveDefaultAgentId(cfg);
27
29
  const knownAgentIds = resolveKnownAgentIds(cfg, defaultAgentId);
28
30
  const allowedAgentIds = resolveAllowedAgentIds(cfg.hooks?.allowedAgentIds);
31
+ const defaultSessionKey = resolveSessionKey(cfg.hooks?.defaultSessionKey);
32
+ const allowedSessionKeyPrefixes = resolveAllowedSessionKeyPrefixes(cfg.hooks?.allowedSessionKeyPrefixes);
33
+ if (defaultSessionKey &&
34
+ allowedSessionKeyPrefixes &&
35
+ !isSessionKeyAllowedByPrefix(defaultSessionKey, allowedSessionKeyPrefixes)) {
36
+ throw new Error("hooks.defaultSessionKey must match hooks.allowedSessionKeyPrefixes");
37
+ }
38
+ if (!defaultSessionKey &&
39
+ allowedSessionKeyPrefixes &&
40
+ !isSessionKeyAllowedByPrefix("hook:example", allowedSessionKeyPrefixes)) {
41
+ throw new Error("hooks.allowedSessionKeyPrefixes must include 'hook:' when hooks.defaultSessionKey is unset");
42
+ }
29
43
  return {
30
44
  basePath: trimmed,
31
45
  token,
@@ -36,6 +50,11 @@ export function resolveHooksConfig(cfg) {
36
50
  knownAgentIds,
37
51
  allowedAgentIds,
38
52
  },
53
+ sessionPolicy: {
54
+ defaultSessionKey,
55
+ allowRequestSessionKey: cfg.hooks?.allowRequestSessionKey === true,
56
+ allowedSessionKeyPrefixes,
57
+ },
39
58
  };
40
59
  }
41
60
  function resolveKnownAgentIds(cfg, defaultAgentId) {
@@ -65,62 +84,68 @@ function resolveAllowedAgentIds(raw) {
65
84
  }
66
85
  return allowed;
67
86
  }
87
+ function resolveSessionKey(raw) {
88
+ const value = raw?.trim();
89
+ return value ? value : undefined;
90
+ }
91
+ function normalizeSessionKeyPrefix(raw) {
92
+ const value = raw.trim().toLowerCase();
93
+ return value ? value : undefined;
94
+ }
95
+ function resolveAllowedSessionKeyPrefixes(raw) {
96
+ if (!Array.isArray(raw)) {
97
+ return undefined;
98
+ }
99
+ const set = new Set();
100
+ for (const prefix of raw) {
101
+ const normalized = normalizeSessionKeyPrefix(prefix);
102
+ if (!normalized) {
103
+ continue;
104
+ }
105
+ set.add(normalized);
106
+ }
107
+ return set.size > 0 ? Array.from(set) : undefined;
108
+ }
109
+ function isSessionKeyAllowedByPrefix(sessionKey, prefixes) {
110
+ const normalized = sessionKey.trim().toLowerCase();
111
+ if (!normalized) {
112
+ return false;
113
+ }
114
+ return prefixes.some((prefix) => normalized.startsWith(prefix));
115
+ }
68
116
  export function extractHookToken(req, url) {
69
117
  const auth = typeof req.headers.authorization === "string" ? req.headers.authorization.trim() : "";
70
118
  if (auth.toLowerCase().startsWith("bearer ")) {
71
119
  const token = auth.slice(7).trim();
72
- if (token)
120
+ if (token) {
73
121
  return { token, fromQuery: false };
122
+ }
74
123
  }
75
124
  const headerToken = typeof req.headers["x-poolbot-token"] === "string" ? req.headers["x-poolbot-token"].trim() : "";
76
- if (headerToken)
125
+ if (headerToken) {
77
126
  return { token: headerToken, fromQuery: false };
78
- const queryToken = url.searchParams.get("token");
79
- if (queryToken)
80
- return { token: queryToken.trim(), fromQuery: true };
127
+ }
128
+ const queryToken = url?.searchParams.get("token")?.trim();
129
+ if (queryToken) {
130
+ return { token: queryToken, fromQuery: true };
131
+ }
81
132
  return { token: undefined, fromQuery: false };
82
133
  }
83
134
  export async function readJsonBody(req, maxBytes) {
84
- return await new Promise((resolve) => {
85
- let done = false;
86
- let total = 0;
87
- const chunks = [];
88
- req.on("data", (chunk) => {
89
- if (done)
90
- return;
91
- total += chunk.length;
92
- if (total > maxBytes) {
93
- done = true;
94
- resolve({ ok: false, error: "payload too large" });
95
- req.destroy();
96
- return;
97
- }
98
- chunks.push(chunk);
99
- });
100
- req.on("end", () => {
101
- if (done)
102
- return;
103
- done = true;
104
- const raw = Buffer.concat(chunks).toString("utf-8").trim();
105
- if (!raw) {
106
- resolve({ ok: true, value: {} });
107
- return;
108
- }
109
- try {
110
- const parsed = JSON.parse(raw);
111
- resolve({ ok: true, value: parsed });
112
- }
113
- catch (err) {
114
- resolve({ ok: false, error: String(err) });
115
- }
116
- });
117
- req.on("error", (err) => {
118
- if (done)
119
- return;
120
- done = true;
121
- resolve({ ok: false, error: String(err) });
122
- });
123
- });
135
+ const result = await readJsonBodyWithLimit(req, { maxBytes, emptyObjectOnEmpty: true });
136
+ if (result.ok) {
137
+ return result;
138
+ }
139
+ if (result.code === "PAYLOAD_TOO_LARGE") {
140
+ return { ok: false, error: "payload too large" };
141
+ }
142
+ if (result.code === "REQUEST_BODY_TIMEOUT") {
143
+ return { ok: false, error: "request body timeout" };
144
+ }
145
+ if (result.code === "CONNECTION_CLOSED") {
146
+ return { ok: false, error: requestBodyErrorToText("CONNECTION_CLOSED") };
147
+ }
148
+ return { ok: false, error: result.error };
124
149
  }
125
150
  export function normalizeHookHeaders(req) {
126
151
  const headers = {};
@@ -136,8 +161,9 @@ export function normalizeHookHeaders(req) {
136
161
  }
137
162
  export function normalizeWakePayload(payload) {
138
163
  const text = typeof payload.text === "string" ? payload.text.trim() : "";
139
- if (!text)
164
+ if (!text) {
140
165
  return { ok: false, error: "text required" };
166
+ }
141
167
  const mode = payload.mode === "next-heartbeat" ? "next-heartbeat" : "now";
142
168
  return { ok: true, value: { text, mode } };
143
169
  }
@@ -145,13 +171,16 @@ const listHookChannelValues = () => ["last", ...listChannelPlugins().map((plugin
145
171
  const getHookChannelSet = () => new Set(listHookChannelValues());
146
172
  export const getHookChannelError = () => `channel must be ${listHookChannelValues().join("|")}`;
147
173
  export function resolveHookChannel(raw) {
148
- if (raw === undefined)
174
+ if (raw === undefined) {
149
175
  return "last";
150
- if (typeof raw !== "string")
176
+ }
177
+ if (typeof raw !== "string") {
151
178
  return null;
179
+ }
152
180
  const normalized = normalizeMessageChannel(raw);
153
- if (!normalized || !getHookChannelSet().has(normalized))
181
+ if (!normalized || !getHookChannelSet().has(normalized)) {
154
182
  return null;
183
+ }
155
184
  return normalized;
156
185
  }
157
186
  export function resolveHookDeliver(raw) {
@@ -182,23 +211,49 @@ export function isHookAgentAllowed(hooksConfig, agentId) {
182
211
  return resolved ? allowed.has(resolved) : false;
183
212
  }
184
213
  export const getHookAgentPolicyError = () => "agentId is not allowed by hooks.allowedAgentIds";
214
+ export const getHookSessionKeyRequestPolicyError = () => "sessionKey is disabled for external /hooks/agent payloads; set hooks.allowRequestSessionKey=true to enable";
215
+ export const getHookSessionKeyPrefixError = (prefixes) => `sessionKey must start with one of: ${prefixes.join(", ")}`;
216
+ export function resolveHookSessionKey(params) {
217
+ const requested = resolveSessionKey(params.sessionKey);
218
+ if (requested) {
219
+ if (params.source === "request" && !params.hooksConfig.sessionPolicy.allowRequestSessionKey) {
220
+ return { ok: false, error: getHookSessionKeyRequestPolicyError() };
221
+ }
222
+ const allowedPrefixes = params.hooksConfig.sessionPolicy.allowedSessionKeyPrefixes;
223
+ if (allowedPrefixes && !isSessionKeyAllowedByPrefix(requested, allowedPrefixes)) {
224
+ return { ok: false, error: getHookSessionKeyPrefixError(allowedPrefixes) };
225
+ }
226
+ return { ok: true, value: requested };
227
+ }
228
+ const defaultSessionKey = params.hooksConfig.sessionPolicy.defaultSessionKey;
229
+ if (defaultSessionKey) {
230
+ return { ok: true, value: defaultSessionKey };
231
+ }
232
+ const generated = `hook:${(params.idFactory ?? randomUUID)()}`;
233
+ const allowedPrefixes = params.hooksConfig.sessionPolicy.allowedSessionKeyPrefixes;
234
+ if (allowedPrefixes && !isSessionKeyAllowedByPrefix(generated, allowedPrefixes)) {
235
+ return { ok: false, error: getHookSessionKeyPrefixError(allowedPrefixes) };
236
+ }
237
+ return { ok: true, value: generated };
238
+ }
185
239
  export function normalizeAgentPayload(payload, opts) {
186
240
  const message = typeof payload.message === "string" ? payload.message.trim() : "";
187
- if (!message)
241
+ if (!message) {
188
242
  return { ok: false, error: "message required" };
243
+ }
189
244
  const nameRaw = payload.name;
190
245
  const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "Hook";
191
246
  const agentIdRaw = payload.agentId;
192
247
  const agentId = typeof agentIdRaw === "string" && agentIdRaw.trim() ? agentIdRaw.trim() : undefined;
193
248
  const wakeMode = payload.wakeMode === "next-heartbeat" ? "next-heartbeat" : "now";
194
249
  const sessionKeyRaw = payload.sessionKey;
195
- const idFactory = opts?.idFactory ?? randomUUID;
196
250
  const sessionKey = typeof sessionKeyRaw === "string" && sessionKeyRaw.trim()
197
251
  ? sessionKeyRaw.trim()
198
- : `hook:${idFactory()}`;
252
+ : `hook:${(opts?.idFactory ?? randomUUID)()}`;
199
253
  const channel = resolveHookChannel(payload.channel);
200
- if (!channel)
254
+ if (!channel) {
201
255
  return { ok: false, error: getHookChannelError() };
256
+ }
202
257
  const toRaw = payload.to;
203
258
  const to = typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
204
259
  const modelRaw = payload.model;
@@ -1,4 +1,14 @@
1
1
  import { readJsonBody } from "./hooks.js";
2
+ /**
3
+ * Apply baseline security headers that are safe for all response types (API JSON,
4
+ * HTML pages, static assets, SSE streams). Headers that restrict framing or set a
5
+ * Content-Security-Policy are intentionally omitted here because some handlers
6
+ * (canvas host, A2UI) serve content that may be loaded inside frames.
7
+ */
8
+ export function setDefaultSecurityHeaders(res) {
9
+ res.setHeader("X-Content-Type-Options", "nosniff");
10
+ res.setHeader("Referrer-Policy", "no-referrer");
11
+ }
2
12
  export function sendJson(res, status, body) {
3
13
  res.statusCode = status;
4
14
  res.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -44,6 +54,18 @@ export function sendInvalidRequest(res, message) {
44
54
  export async function readJsonBodyOrError(req, res, maxBytes) {
45
55
  const body = await readJsonBody(req, maxBytes);
46
56
  if (!body.ok) {
57
+ if (body.error === "payload too large") {
58
+ sendJson(res, 413, {
59
+ error: { message: "Payload too large", type: "invalid_request_error" },
60
+ });
61
+ return undefined;
62
+ }
63
+ if (body.error === "request body timeout") {
64
+ sendJson(res, 408, {
65
+ error: { message: "Request body timeout", type: "invalid_request_error" },
66
+ });
67
+ return undefined;
68
+ }
47
69
  sendInvalidRequest(res, body.error);
48
70
  return undefined;
49
71
  }
@@ -0,0 +1,169 @@
1
+ export const ADMIN_SCOPE = "operator.admin";
2
+ export const READ_SCOPE = "operator.read";
3
+ export const WRITE_SCOPE = "operator.write";
4
+ export const APPROVALS_SCOPE = "operator.approvals";
5
+ export const PAIRING_SCOPE = "operator.pairing";
6
+ export const CLI_DEFAULT_OPERATOR_SCOPES = [
7
+ ADMIN_SCOPE,
8
+ APPROVALS_SCOPE,
9
+ PAIRING_SCOPE,
10
+ ];
11
+ const NODE_ROLE_METHODS = new Set(["node.invoke.result", "node.event", "skills.bins"]);
12
+ const METHOD_SCOPE_GROUPS = {
13
+ [APPROVALS_SCOPE]: [
14
+ "exec.approval.request",
15
+ "exec.approval.waitDecision",
16
+ "exec.approval.resolve",
17
+ ],
18
+ [PAIRING_SCOPE]: [
19
+ "node.pair.request",
20
+ "node.pair.list",
21
+ "node.pair.approve",
22
+ "node.pair.reject",
23
+ "node.pair.verify",
24
+ "device.pair.list",
25
+ "device.pair.approve",
26
+ "device.pair.reject",
27
+ "device.pair.remove",
28
+ "device.token.rotate",
29
+ "device.token.revoke",
30
+ "node.rename",
31
+ ],
32
+ [READ_SCOPE]: [
33
+ "health",
34
+ "logs.tail",
35
+ "channels.status",
36
+ "status",
37
+ "usage.status",
38
+ "usage.cost",
39
+ "tts.status",
40
+ "tts.providers",
41
+ "models.list",
42
+ "agents.list",
43
+ "agent.identity.get",
44
+ "skills.status",
45
+ "voicewake.get",
46
+ "sessions.list",
47
+ "sessions.preview",
48
+ "sessions.resolve",
49
+ "sessions.usage",
50
+ "sessions.usage.timeseries",
51
+ "sessions.usage.logs",
52
+ "cron.list",
53
+ "cron.status",
54
+ "cron.runs",
55
+ "system-presence",
56
+ "last-heartbeat",
57
+ "node.list",
58
+ "node.describe",
59
+ "chat.history",
60
+ "config.get",
61
+ "talk.config",
62
+ "agents.files.list",
63
+ "agents.files.get",
64
+ ],
65
+ [WRITE_SCOPE]: [
66
+ "send",
67
+ "poll",
68
+ "agent",
69
+ "agent.wait",
70
+ "wake",
71
+ "talk.mode",
72
+ "tts.enable",
73
+ "tts.disable",
74
+ "tts.convert",
75
+ "tts.setProvider",
76
+ "voicewake.set",
77
+ "node.invoke",
78
+ "chat.send",
79
+ "chat.abort",
80
+ "browser.request",
81
+ "push.test",
82
+ ],
83
+ [ADMIN_SCOPE]: [
84
+ "channels.logout",
85
+ "agents.create",
86
+ "agents.update",
87
+ "agents.delete",
88
+ "skills.install",
89
+ "skills.update",
90
+ "cron.add",
91
+ "cron.update",
92
+ "cron.remove",
93
+ "cron.run",
94
+ "sessions.patch",
95
+ "sessions.reset",
96
+ "sessions.delete",
97
+ "sessions.compact",
98
+ "connect",
99
+ "chat.inject",
100
+ "web.login.start",
101
+ "web.login.wait",
102
+ "set-heartbeats",
103
+ "system-event",
104
+ "agents.files.set",
105
+ ],
106
+ };
107
+ const ADMIN_METHOD_PREFIXES = ["exec.approvals.", "config.", "wizard.", "update."];
108
+ const METHOD_SCOPE_BY_NAME = new Map(Object.entries(METHOD_SCOPE_GROUPS).flatMap(([scope, methods]) => methods.map((method) => [method, scope])));
109
+ function resolveScopedMethod(method) {
110
+ const explicitScope = METHOD_SCOPE_BY_NAME.get(method);
111
+ if (explicitScope) {
112
+ return explicitScope;
113
+ }
114
+ if (ADMIN_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix))) {
115
+ return ADMIN_SCOPE;
116
+ }
117
+ return undefined;
118
+ }
119
+ export function isApprovalMethod(method) {
120
+ return resolveScopedMethod(method) === APPROVALS_SCOPE;
121
+ }
122
+ export function isPairingMethod(method) {
123
+ return resolveScopedMethod(method) === PAIRING_SCOPE;
124
+ }
125
+ export function isReadMethod(method) {
126
+ return resolveScopedMethod(method) === READ_SCOPE;
127
+ }
128
+ export function isWriteMethod(method) {
129
+ return resolveScopedMethod(method) === WRITE_SCOPE;
130
+ }
131
+ export function isNodeRoleMethod(method) {
132
+ return NODE_ROLE_METHODS.has(method);
133
+ }
134
+ export function isAdminOnlyMethod(method) {
135
+ return resolveScopedMethod(method) === ADMIN_SCOPE;
136
+ }
137
+ export function resolveRequiredOperatorScopeForMethod(method) {
138
+ return resolveScopedMethod(method);
139
+ }
140
+ export function resolveLeastPrivilegeOperatorScopesForMethod(method) {
141
+ const requiredScope = resolveRequiredOperatorScopeForMethod(method);
142
+ if (requiredScope) {
143
+ return [requiredScope];
144
+ }
145
+ // Default-deny for unclassified methods.
146
+ return [];
147
+ }
148
+ export function authorizeOperatorScopesForMethod(method, scopes) {
149
+ if (scopes.includes(ADMIN_SCOPE)) {
150
+ return { allowed: true };
151
+ }
152
+ const requiredScope = resolveRequiredOperatorScopeForMethod(method) ?? ADMIN_SCOPE;
153
+ if (requiredScope === READ_SCOPE) {
154
+ if (scopes.includes(READ_SCOPE) || scopes.includes(WRITE_SCOPE)) {
155
+ return { allowed: true };
156
+ }
157
+ return { allowed: false, missingScope: READ_SCOPE };
158
+ }
159
+ if (scopes.includes(requiredScope)) {
160
+ return { allowed: true };
161
+ }
162
+ return { allowed: false, missingScope: requiredScope };
163
+ }
164
+ export function isGatewayMethodClassified(method) {
165
+ if (isNodeRoleMethod(method)) {
166
+ return true;
167
+ }
168
+ return resolveRequiredOperatorScopeForMethod(method) !== undefined;
169
+ }
@@ -347,3 +347,26 @@ export function isLoopbackHost(host) {
347
347
  const unbracket = h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
348
348
  return isLoopbackAddress(unbracket);
349
349
  }
350
+ /**
351
+ * Returns true if the URL uses wss:// (encrypted) OR ws:// to a loopback address.
352
+ * Plaintext ws:// to non-loopback addresses is insecure (CWE-319, CVSS 9.8):
353
+ * both credentials (gateway token/password) and chat/conversation data
354
+ * AND chat/conversation data would be exposed to network interception.
355
+ */
356
+ export function isSecureWebSocketUrl(url) {
357
+ let parsed;
358
+ try {
359
+ parsed = new URL(url);
360
+ }
361
+ catch {
362
+ return false;
363
+ }
364
+ if (parsed.protocol === "wss:") {
365
+ return true;
366
+ }
367
+ if (parsed.protocol !== "ws:") {
368
+ return false;
369
+ }
370
+ // ws:// is only secure for loopback addresses
371
+ return isLoopbackHost(parsed.hostname);
372
+ }