@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
@@ -104,28 +104,60 @@ async function resolveVerifiedTailscaleUser(params) {
104
104
  };
105
105
  }
106
106
  export function resolveGatewayAuth(params) {
107
- const authConfig = params.authConfig ?? {};
107
+ const baseAuthConfig = params.authConfig ?? {};
108
+ const authOverride = params.authOverride ?? undefined;
109
+ const authConfig = { ...baseAuthConfig };
110
+ if (authOverride) {
111
+ if (authOverride.mode !== undefined) {
112
+ authConfig.mode = authOverride.mode;
113
+ }
114
+ if (authOverride.token !== undefined) {
115
+ authConfig.token = authOverride.token;
116
+ }
117
+ if (authOverride.password !== undefined) {
118
+ authConfig.password = authOverride.password;
119
+ }
120
+ if (authOverride.allowTailscale !== undefined) {
121
+ authConfig.allowTailscale = authOverride.allowTailscale;
122
+ }
123
+ if (authOverride.rateLimit !== undefined) {
124
+ authConfig.rateLimit = authOverride.rateLimit;
125
+ }
126
+ if (authOverride.trustedProxy !== undefined) {
127
+ authConfig.trustedProxy = authOverride.trustedProxy;
128
+ }
129
+ }
108
130
  const env = params.env ?? process.env;
109
131
  const token = authConfig.token ?? env.POOLBOT_GATEWAY_TOKEN ?? undefined;
110
132
  const password = authConfig.password ?? env.POOLBOT_GATEWAY_PASSWORD ?? undefined;
111
133
  const trustedProxy = authConfig.trustedProxy;
112
134
  let mode;
113
- if (authConfig.mode) {
135
+ let modeSource;
136
+ if (authOverride?.mode !== undefined) {
137
+ mode = authOverride.mode;
138
+ modeSource = "override";
139
+ }
140
+ else if (authConfig.mode) {
114
141
  mode = authConfig.mode;
142
+ modeSource = "config";
115
143
  }
116
144
  else if (password) {
117
145
  mode = "password";
146
+ modeSource = "password";
118
147
  }
119
148
  else if (token) {
120
149
  mode = "token";
150
+ modeSource = "token";
121
151
  }
122
152
  else {
123
- mode = "none";
153
+ mode = "token";
154
+ modeSource = "default";
124
155
  }
125
156
  const allowTailscale = authConfig.allowTailscale ??
126
157
  (params.tailscaleMode === "serve" && mode !== "password" && mode !== "trusted-proxy");
127
158
  return {
128
159
  mode,
160
+ modeSource,
129
161
  token,
130
162
  password,
131
163
  allowTailscale,
@@ -203,6 +235,9 @@ export async function authorizeGatewayConnect(params) {
203
235
  }
204
236
  return { ok: false, reason: result.reason };
205
237
  }
238
+ if (auth.mode === "none") {
239
+ return { ok: true, method: "none" };
240
+ }
206
241
  const limiter = params.rateLimiter;
207
242
  const ip = params.clientIp ?? resolveRequestClientIp(req, trustedProxies) ?? req?.socket?.remoteAddress;
208
243
  const rateLimitScope = params.rateLimitScope ?? AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET;
@@ -5,7 +5,8 @@ import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
5
5
  import { loadGatewayTlsRuntime } from "../infra/tls/gateway.js";
6
6
  import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, } from "../utils/message-channel.js";
7
7
  import { GatewayClient } from "./client.js";
8
- import { pickPrimaryLanIPv4 } from "./net.js";
8
+ import { CLI_DEFAULT_OPERATOR_SCOPES, resolveLeastPrivilegeOperatorScopesForMethod, } from "./method-scopes.js";
9
+ import { isSecureWebSocketUrl, pickPrimaryLanIPv4 } from "./net.js";
9
10
  import { PROTOCOL_VERSION } from "./protocol/index.js";
10
11
  export function resolveExplicitGatewayAuth(opts) {
11
12
  const token = typeof opts?.token === "string" && opts.token.trim().length > 0 ? opts.token.trim() : undefined;
@@ -69,6 +70,18 @@ export function buildGatewayConnectionDetails(options = {}) {
69
70
  ? "Warn: gateway.mode=remote but gateway.remote.url is missing; set gateway.remote.url or switch gateway.mode=local."
70
71
  : undefined;
71
72
  const bindDetail = !urlOverride && !remoteUrl ? `Bind: ${bindMode}` : undefined;
73
+ // Security check: block ALL insecure ws:// to non-loopback addresses (CWE-319, CVSS 9.8)
74
+ // This applies to the FINAL resolved URL, regardless of source (config, CLI override, etc).
75
+ // Both credentials and chat/conversation data must not be transmitted over plaintext to remote hosts.
76
+ if (!isSecureWebSocketUrl(url)) {
77
+ throw new Error([
78
+ `SECURITY ERROR: Gateway URL "${url}" uses plaintext ws:// to a non-loopback address.`,
79
+ "Both credentials and chat data would be exposed to network interception.",
80
+ `Source: ${urlSource}`,
81
+ `Config: ${configPath}`,
82
+ "Fix: Use wss:// for the gateway URL, or connect via SSH tunnel to localhost.",
83
+ ].join("\n"));
84
+ }
72
85
  const message = [
73
86
  `Gateway target: ${url}`,
74
87
  `Source: ${urlSource}`,
@@ -86,77 +99,89 @@ export function buildGatewayConnectionDetails(options = {}) {
86
99
  message,
87
100
  };
88
101
  }
89
- export async function callGateway(opts) {
90
- const timeoutMs = typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 10_000;
102
+ function trimToUndefined(value) {
103
+ if (typeof value !== "string") {
104
+ return undefined;
105
+ }
106
+ const trimmed = value.trim();
107
+ return trimmed.length > 0 ? trimmed : undefined;
108
+ }
109
+ function resolveGatewayCallTimeout(timeoutValue) {
110
+ const timeoutMs = typeof timeoutValue === "number" && Number.isFinite(timeoutValue) ? timeoutValue : 10_000;
91
111
  const safeTimerTimeoutMs = Math.max(1, Math.min(Math.floor(timeoutMs), 2_147_483_647));
112
+ return { timeoutMs, safeTimerTimeoutMs };
113
+ }
114
+ function resolveGatewayCallContext(opts) {
92
115
  const config = opts.config ?? loadConfig();
116
+ const configPath = opts.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env));
93
117
  const isRemoteMode = config.gateway?.mode === "remote";
94
- const remote = isRemoteMode ? config.gateway?.remote : undefined;
95
- const urlOverride = typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : undefined;
118
+ const remote = isRemoteMode
119
+ ? config.gateway?.remote
120
+ : undefined;
121
+ const urlOverride = trimToUndefined(opts.url);
122
+ const remoteUrl = trimToUndefined(remote?.url);
96
123
  const explicitAuth = resolveExplicitGatewayAuth({ token: opts.token, password: opts.password });
97
- ensureExplicitGatewayAuth({
98
- urlOverride,
99
- auth: explicitAuth,
100
- errorHint: "Fix: pass --token or --password (or gatewayToken in tools).",
101
- configPath: opts.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env)),
102
- });
103
- const remoteUrl = typeof remote?.url === "string" && remote.url.trim().length > 0 ? remote.url.trim() : undefined;
104
- if (isRemoteMode && !urlOverride && !remoteUrl) {
105
- const configPath = opts.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env));
106
- throw new Error([
107
- "gateway remote mode misconfigured: gateway.remote.url missing",
108
- `Config: ${configPath}`,
109
- "Fix: set gateway.remote.url, or set gateway.mode=local.",
110
- ].join("\n"));
124
+ return { config, configPath, isRemoteMode, remote, urlOverride, remoteUrl, explicitAuth };
125
+ }
126
+ function ensureRemoteModeUrlConfigured(context) {
127
+ if (!context.isRemoteMode || context.urlOverride || context.remoteUrl) {
128
+ return;
111
129
  }
112
- const authToken = config.gateway?.auth?.token;
113
- const authPassword = config.gateway?.auth?.password;
114
- const connectionDetails = buildGatewayConnectionDetails({
115
- config,
116
- url: urlOverride,
117
- ...(opts.configPath ? { configPath: opts.configPath } : {}),
118
- });
119
- const url = connectionDetails.url;
120
- const useLocalTls = config.gateway?.tls?.enabled === true && !urlOverride && !remoteUrl && url.startsWith("wss://");
121
- const tlsRuntime = useLocalTls ? await loadGatewayTlsRuntime(config.gateway?.tls) : undefined;
122
- const remoteTlsFingerprint = isRemoteMode && !urlOverride && remoteUrl && typeof remote?.tlsFingerprint === "string"
123
- ? remote.tlsFingerprint.trim()
124
- : undefined;
125
- const overrideTlsFingerprint = typeof opts.tlsFingerprint === "string" ? opts.tlsFingerprint.trim() : undefined;
126
- const tlsFingerprint = overrideTlsFingerprint ||
127
- remoteTlsFingerprint ||
128
- (tlsRuntime?.enabled ? tlsRuntime.fingerprintSha256 : undefined);
129
- const token = explicitAuth.token ||
130
- (!urlOverride
131
- ? isRemoteMode
132
- ? typeof remote?.token === "string" && remote.token.trim().length > 0
133
- ? remote.token.trim()
134
- : undefined
135
- : process.env.POOLBOT_GATEWAY_TOKEN?.trim() ||
136
- process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() ||
137
- (typeof authToken === "string" && authToken.trim().length > 0
138
- ? authToken.trim()
139
- : undefined)
130
+ throw new Error([
131
+ "gateway remote mode misconfigured: gateway.remote.url missing",
132
+ `Config: ${context.configPath}`,
133
+ "Fix: set gateway.remote.url, or set gateway.mode=local.",
134
+ ].join("\n"));
135
+ }
136
+ function resolveGatewayCredentials(context) {
137
+ const authToken = context.config.gateway?.auth?.token;
138
+ const authPassword = context.config.gateway?.auth?.password;
139
+ const token = context.explicitAuth.token ||
140
+ (!context.urlOverride
141
+ ? context.isRemoteMode
142
+ ? trimToUndefined(context.remote?.token)
143
+ : trimToUndefined(process.env.POOLBOT_GATEWAY_TOKEN) ||
144
+ trimToUndefined(process.env.CLAWDBOT_GATEWAY_TOKEN) ||
145
+ trimToUndefined(authToken)
140
146
  : undefined);
141
- const password = explicitAuth.password ||
142
- (!urlOverride
143
- ? process.env.POOLBOT_GATEWAY_PASSWORD?.trim() ||
144
- process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() ||
145
- (isRemoteMode
146
- ? typeof remote?.password === "string" && remote.password.trim().length > 0
147
- ? remote.password.trim()
148
- : undefined
149
- : typeof authPassword === "string" && authPassword.trim().length > 0
150
- ? authPassword.trim()
151
- : undefined)
147
+ const password = context.explicitAuth.password ||
148
+ (!context.urlOverride
149
+ ? trimToUndefined(process.env.POOLBOT_GATEWAY_PASSWORD) ||
150
+ trimToUndefined(process.env.CLAWDBOT_GATEWAY_PASSWORD) ||
151
+ (context.isRemoteMode
152
+ ? trimToUndefined(context.remote?.password)
153
+ : trimToUndefined(authPassword))
152
154
  : undefined);
153
- const formatCloseError = (code, reason) => {
154
- const reasonText = reason?.trim() || "no close reason";
155
- const hint = code === 1006 ? "abnormal closure (no close frame)" : code === 1000 ? "normal closure" : "";
156
- const suffix = hint ? ` ${hint}` : "";
157
- return `gateway closed (${code}${suffix}): ${reasonText}\n${connectionDetails.message}`;
158
- };
159
- const formatTimeoutError = () => `gateway timeout after ${timeoutMs}ms\n${connectionDetails.message}`;
155
+ return { token, password };
156
+ }
157
+ async function resolveGatewayTlsFingerprint(params) {
158
+ const { opts, context, url } = params;
159
+ const useLocalTls = context.config.gateway?.tls?.enabled === true &&
160
+ !context.urlOverride &&
161
+ !context.remoteUrl &&
162
+ url.startsWith("wss://");
163
+ const tlsRuntime = useLocalTls
164
+ ? await loadGatewayTlsRuntime(context.config.gateway?.tls)
165
+ : undefined;
166
+ const overrideTlsFingerprint = trimToUndefined(opts.tlsFingerprint);
167
+ const remoteTlsFingerprint = context.isRemoteMode && !context.urlOverride && context.remoteUrl
168
+ ? trimToUndefined(context.remote?.tlsFingerprint)
169
+ : undefined;
170
+ return (overrideTlsFingerprint ||
171
+ remoteTlsFingerprint ||
172
+ (tlsRuntime?.enabled ? tlsRuntime.fingerprintSha256 : undefined));
173
+ }
174
+ function formatGatewayCloseError(code, reason, connectionDetails) {
175
+ const reasonText = reason?.trim() || "no close reason";
176
+ const hint = code === 1006 ? "abnormal closure (no close frame)" : code === 1000 ? "normal closure" : "";
177
+ const suffix = hint ? ` ${hint}` : "";
178
+ return `gateway closed (${code}${suffix}): ${reasonText}\n${connectionDetails.message}`;
179
+ }
180
+ function formatGatewayTimeoutError(timeoutMs, connectionDetails) {
181
+ return `gateway timeout after ${timeoutMs}ms\n${connectionDetails.message}`;
182
+ }
183
+ async function executeGatewayRequestWithScopes(params) {
184
+ const { opts, scopes, url, token, password, tlsFingerprint, timeoutMs, safeTimerTimeoutMs } = params;
160
185
  return await new Promise((resolve, reject) => {
161
186
  let settled = false;
162
187
  let ignoreClose = false;
@@ -185,7 +210,7 @@ export async function callGateway(opts) {
185
210
  platform: opts.platform,
186
211
  mode: opts.mode ?? GATEWAY_CLIENT_MODES.CLI,
187
212
  role: "operator",
188
- scopes: ["operator.admin", "operator.approvals", "operator.pairing"],
213
+ scopes,
189
214
  deviceIdentity: loadOrCreateDeviceIdentity(),
190
215
  minProtocol: opts.minProtocol ?? PROTOCOL_VERSION,
191
216
  maxProtocol: opts.maxProtocol ?? PROTOCOL_VERSION,
@@ -210,17 +235,73 @@ export async function callGateway(opts) {
210
235
  }
211
236
  ignoreClose = true;
212
237
  client.stop();
213
- stop(new Error(formatCloseError(code, reason)));
238
+ stop(new Error(formatGatewayCloseError(code, reason, params.connectionDetails)));
214
239
  },
215
240
  });
216
241
  const timer = setTimeout(() => {
217
242
  ignoreClose = true;
218
243
  client.stop();
219
- stop(new Error(formatTimeoutError()));
244
+ stop(new Error(formatGatewayTimeoutError(timeoutMs, params.connectionDetails)));
220
245
  }, safeTimerTimeoutMs);
221
246
  client.start();
222
247
  });
223
248
  }
249
+ async function callGatewayWithScopes(opts, scopes) {
250
+ const { timeoutMs, safeTimerTimeoutMs } = resolveGatewayCallTimeout(opts.timeoutMs);
251
+ const context = resolveGatewayCallContext(opts);
252
+ ensureExplicitGatewayAuth({
253
+ urlOverride: context.urlOverride,
254
+ auth: context.explicitAuth,
255
+ errorHint: "Fix: pass --token or --password (or gatewayToken in tools).",
256
+ configPath: context.configPath,
257
+ });
258
+ ensureRemoteModeUrlConfigured(context);
259
+ const connectionDetails = buildGatewayConnectionDetails({
260
+ config: context.config,
261
+ url: context.urlOverride,
262
+ ...(opts.configPath ? { configPath: opts.configPath } : {}),
263
+ });
264
+ const url = connectionDetails.url;
265
+ const tlsFingerprint = await resolveGatewayTlsFingerprint({ opts, context, url });
266
+ const { token, password } = resolveGatewayCredentials(context);
267
+ return await executeGatewayRequestWithScopes({
268
+ opts,
269
+ scopes,
270
+ url,
271
+ token,
272
+ password,
273
+ tlsFingerprint,
274
+ timeoutMs,
275
+ safeTimerTimeoutMs,
276
+ connectionDetails,
277
+ });
278
+ }
279
+ export async function callGatewayScoped(opts) {
280
+ return await callGatewayWithScopes(opts, opts.scopes);
281
+ }
282
+ export async function callGatewayCli(opts) {
283
+ const scopes = Array.isArray(opts.scopes) ? opts.scopes : CLI_DEFAULT_OPERATOR_SCOPES;
284
+ return await callGatewayWithScopes(opts, scopes);
285
+ }
286
+ export async function callGatewayLeastPrivilege(opts) {
287
+ const scopes = resolveLeastPrivilegeOperatorScopesForMethod(opts.method);
288
+ return await callGatewayWithScopes(opts, scopes);
289
+ }
290
+ export async function callGateway(opts) {
291
+ if (Array.isArray(opts.scopes)) {
292
+ return await callGatewayWithScopes(opts, opts.scopes);
293
+ }
294
+ const callerMode = opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND;
295
+ const callerName = opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT;
296
+ if (callerMode === GATEWAY_CLIENT_MODES.CLI || callerName === GATEWAY_CLIENT_NAMES.CLI) {
297
+ return await callGatewayCli(opts);
298
+ }
299
+ return await callGatewayLeastPrivilege({
300
+ ...opts,
301
+ mode: callerMode,
302
+ clientName: callerName,
303
+ });
304
+ }
224
305
  export function randomIdempotencyKey() {
225
306
  return randomUUID();
226
307
  }
@@ -0,0 +1,75 @@
1
+ import { randomBytes } from "node:crypto";
2
+ export const CANVAS_CAPABILITY_PATH_PREFIX = "/__poolbot__/cap";
3
+ export const CANVAS_CAPABILITY_QUERY_PARAM = "oc_cap";
4
+ export const CANVAS_CAPABILITY_TTL_MS = 10 * 60_000;
5
+ function normalizeCapability(raw) {
6
+ const trimmed = raw?.trim();
7
+ return trimmed ? trimmed : undefined;
8
+ }
9
+ export function mintCanvasCapabilityToken() {
10
+ return randomBytes(18).toString("base64url");
11
+ }
12
+ export function buildCanvasScopedHostUrl(baseUrl, capability) {
13
+ const normalizedCapability = normalizeCapability(capability);
14
+ if (!normalizedCapability) {
15
+ return undefined;
16
+ }
17
+ try {
18
+ const url = new URL(baseUrl);
19
+ const trimmedPath = url.pathname.replace(/\/+$/, "");
20
+ const prefix = `${CANVAS_CAPABILITY_PATH_PREFIX}/${encodeURIComponent(normalizedCapability)}`;
21
+ url.pathname = `${trimmedPath}${prefix}`;
22
+ url.search = "";
23
+ url.hash = "";
24
+ return url.toString().replace(/\/$/, "");
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ export function normalizeCanvasScopedUrl(rawUrl) {
31
+ const url = new URL(rawUrl, "http://localhost");
32
+ const prefix = `${CANVAS_CAPABILITY_PATH_PREFIX}/`;
33
+ let scopedPath = false;
34
+ let malformedScopedPath = false;
35
+ let capabilityFromPath;
36
+ let rewrittenUrl;
37
+ if (url.pathname.startsWith(prefix)) {
38
+ scopedPath = true;
39
+ const remainder = url.pathname.slice(prefix.length);
40
+ const slashIndex = remainder.indexOf("/");
41
+ if (slashIndex <= 0) {
42
+ malformedScopedPath = true;
43
+ }
44
+ else {
45
+ const encodedCapability = remainder.slice(0, slashIndex);
46
+ const canonicalPath = remainder.slice(slashIndex) || "/";
47
+ let decoded;
48
+ try {
49
+ decoded = decodeURIComponent(encodedCapability);
50
+ }
51
+ catch {
52
+ malformedScopedPath = true;
53
+ }
54
+ capabilityFromPath = normalizeCapability(decoded);
55
+ if (!capabilityFromPath || !canonicalPath.startsWith("/")) {
56
+ malformedScopedPath = true;
57
+ }
58
+ else {
59
+ url.pathname = canonicalPath;
60
+ if (!url.searchParams.has(CANVAS_CAPABILITY_QUERY_PARAM)) {
61
+ url.searchParams.set(CANVAS_CAPABILITY_QUERY_PARAM, capabilityFromPath);
62
+ }
63
+ rewrittenUrl = `${url.pathname}${url.search}`;
64
+ }
65
+ }
66
+ }
67
+ const capability = capabilityFromPath ?? normalizeCapability(url.searchParams.get(CANVAS_CAPABILITY_QUERY_PARAM));
68
+ return {
69
+ pathname: url.pathname,
70
+ capability,
71
+ rewrittenUrl,
72
+ scopedPath,
73
+ malformedScopedPath,
74
+ };
75
+ }
@@ -0,0 +1,28 @@
1
+ function normalizePart(value, fallback) {
2
+ if (typeof value !== "string") {
3
+ return fallback;
4
+ }
5
+ const normalized = value.trim();
6
+ return normalized.length > 0 ? normalized : fallback;
7
+ }
8
+ export function resolveControlPlaneActor(client) {
9
+ return {
10
+ actor: normalizePart(client?.connect?.client?.id, "unknown-actor"),
11
+ deviceId: normalizePart(client?.connect?.device?.id, "unknown-device"),
12
+ clientIp: normalizePart(client?.clientIp, "unknown-ip"),
13
+ connId: normalizePart(client?.connId, "unknown-conn"),
14
+ };
15
+ }
16
+ export function formatControlPlaneActor(actor) {
17
+ return `actor=${actor.actor} device=${actor.deviceId} ip=${actor.clientIp} conn=${actor.connId}`;
18
+ }
19
+ export function summarizeChangedPaths(paths, maxPaths = 8) {
20
+ if (paths.length === 0) {
21
+ return "<none>";
22
+ }
23
+ if (paths.length <= maxPaths) {
24
+ return paths.join(",");
25
+ }
26
+ const head = paths.slice(0, maxPaths).join(",");
27
+ return `${head},+${paths.length - maxPaths} more`;
28
+ }
@@ -0,0 +1,53 @@
1
+ const CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS = 3;
2
+ const CONTROL_PLANE_RATE_LIMIT_WINDOW_MS = 60_000;
3
+ const controlPlaneBuckets = new Map();
4
+ function normalizePart(value, fallback) {
5
+ if (typeof value !== "string") {
6
+ return fallback;
7
+ }
8
+ const normalized = value.trim();
9
+ return normalized.length > 0 ? normalized : fallback;
10
+ }
11
+ export function resolveControlPlaneRateLimitKey(client) {
12
+ const deviceId = normalizePart(client?.connect?.device?.id, "unknown-device");
13
+ const clientIp = normalizePart(client?.clientIp, "unknown-ip");
14
+ return `${deviceId}|${clientIp}`;
15
+ }
16
+ export function consumeControlPlaneWriteBudget(params) {
17
+ const nowMs = params.nowMs ?? Date.now();
18
+ const key = resolveControlPlaneRateLimitKey(params.client);
19
+ const bucket = controlPlaneBuckets.get(key);
20
+ if (!bucket || nowMs - bucket.windowStartMs >= CONTROL_PLANE_RATE_LIMIT_WINDOW_MS) {
21
+ controlPlaneBuckets.set(key, {
22
+ count: 1,
23
+ windowStartMs: nowMs,
24
+ });
25
+ return {
26
+ allowed: true,
27
+ retryAfterMs: 0,
28
+ remaining: CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS - 1,
29
+ key,
30
+ };
31
+ }
32
+ if (bucket.count >= CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS) {
33
+ const retryAfterMs = Math.max(0, bucket.windowStartMs + CONTROL_PLANE_RATE_LIMIT_WINDOW_MS - nowMs);
34
+ return {
35
+ allowed: false,
36
+ retryAfterMs,
37
+ remaining: 0,
38
+ key,
39
+ };
40
+ }
41
+ bucket.count += 1;
42
+ return {
43
+ allowed: true,
44
+ retryAfterMs: 0,
45
+ remaining: Math.max(0, CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS - bucket.count),
46
+ key,
47
+ };
48
+ }
49
+ export const __testing = {
50
+ resetControlPlaneRateLimitState() {
51
+ controlPlaneBuckets.clear();
52
+ },
53
+ };
@@ -0,0 +1 @@
1
+ export const GATEWAY_EVENT_UPDATE_AVAILABLE = "update.available";