@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.
- package/CHANGELOG.md +29 -0
- package/dist/acp/client.js +207 -18
- package/dist/acp/secret-file.js +22 -0
- package/dist/agents/agent-scope.js +10 -0
- package/dist/agents/bash-process-registry.test-helpers.js +29 -0
- package/dist/agents/bash-tools.exec-approval-request.js +20 -0
- package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
- package/dist/agents/bash-tools.exec-host-node.js +235 -0
- package/dist/agents/bash-tools.exec-types.js +1 -0
- package/dist/agents/bash-tools.process.js +224 -218
- package/dist/agents/content-blocks.js +16 -0
- package/dist/agents/model-fallback.js +96 -101
- package/dist/agents/models-config.providers.js +299 -182
- package/dist/agents/pi-embedded-payloads.js +1 -0
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
- package/dist/agents/skills.test-helpers.js +13 -0
- package/dist/agents/stable-stringify.js +12 -0
- package/dist/agents/subagent-registry.mocks.shared.js +12 -0
- package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
- package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
- package/dist/agents/tool-policy-shared.js +108 -0
- package/dist/agents/tools/browser-tool.js +160 -54
- package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
- package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
- package/dist/agents/tools/image-tool.js +214 -99
- package/dist/agents/tools/sessions-history-tool.js +140 -108
- package/dist/agents/workspace.js +222 -46
- package/dist/auto-reply/commands-registry.js +15 -18
- package/dist/auto-reply/fallback-state.js +114 -0
- package/dist/auto-reply/model-runtime.js +68 -0
- package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
- package/dist/auto-reply/reply/agent-runner.js +165 -39
- package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
- package/dist/browser/config.js +26 -0
- package/dist/browser/navigation-guard.js +31 -0
- package/dist/browser/routes/agent.act.js +431 -424
- package/dist/browser/routes/agent.shared.js +47 -3
- package/dist/browser/routes/agent.snapshot.js +122 -116
- package/dist/browser/routes/agent.storage.js +303 -297
- package/dist/browser/routes/tabs.js +154 -100
- package/dist/browser/server-lifecycle.js +37 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/allow-from.js +25 -0
- package/dist/channels/plugins/account-action-gate.js +13 -0
- package/dist/channels/plugins/message-actions.js +10 -0
- package/dist/channels/telegram/api.js +18 -0
- package/dist/cli/argv.js +84 -21
- package/dist/cli/banner.js +2 -1
- package/dist/cli/exec-approvals-cli.js +92 -124
- package/dist/cli/memory-cli.js +158 -61
- package/dist/cli/nodes-cli/register.push.js +63 -0
- package/dist/cli/nodes-media-utils.js +21 -0
- package/dist/cli/plugins-cli.js +245 -61
- package/dist/cli/program/build-program.js +3 -1
- package/dist/cli/program/command-registry.js +223 -136
- package/dist/cli/program/help.js +43 -12
- package/dist/cli/route.js +1 -1
- package/dist/cli/test-runtime-capture.js +24 -0
- package/dist/commands/agent.js +163 -87
- package/dist/commands/channels.mock-harness.js +23 -0
- package/dist/commands/daemon-install-runtime-warning.js +11 -0
- package/dist/commands/onboard-helpers.js +4 -4
- package/dist/commands/sessions.test-helpers.js +61 -0
- package/dist/compat/legacy-names.js +2 -2
- package/dist/config/commands.js +3 -0
- package/dist/config/config.js +1 -1
- package/dist/config/env-substitution.js +62 -34
- package/dist/config/env-vars.js +9 -0
- package/dist/config/io.js +571 -171
- package/dist/config/merge-patch.js +50 -4
- package/dist/config/redact-snapshot.js +404 -76
- package/dist/config/schema.js +58 -570
- package/dist/config/validation.js +140 -85
- package/dist/config/zod-schema.hooks.js +40 -11
- package/dist/config/zod-schema.installs.js +20 -0
- package/dist/config/zod-schema.js +8 -7
- package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
- package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
- package/dist/control-ui/index.html +1 -1
- package/dist/daemon/cmd-argv.js +21 -0
- package/dist/daemon/cmd-set.js +58 -0
- package/dist/daemon/service-types.js +1 -0
- package/dist/discord/monitor/exec-approvals.js +357 -162
- package/dist/gateway/auth.js +38 -3
- package/dist/gateway/call.js +149 -68
- package/dist/gateway/canvas-capability.js +75 -0
- package/dist/gateway/control-plane-audit.js +28 -0
- package/dist/gateway/control-plane-rate-limit.js +53 -0
- package/dist/gateway/events.js +1 -0
- package/dist/gateway/hooks.js +109 -54
- package/dist/gateway/http-common.js +22 -0
- package/dist/gateway/method-scopes.js +169 -0
- package/dist/gateway/net.js +23 -0
- package/dist/gateway/openresponses-http.js +120 -110
- package/dist/gateway/probe-auth.js +2 -0
- package/dist/gateway/protocol/index.js +3 -2
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
- package/dist/gateway/protocol/schema/push.js +18 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-http.js +236 -52
- package/dist/gateway/server-methods/agent.js +162 -24
- package/dist/gateway/server-methods/chat.js +461 -130
- package/dist/gateway/server-methods/config.js +193 -150
- package/dist/gateway/server-methods/nodes.helpers.js +12 -0
- package/dist/gateway/server-methods/nodes.js +251 -69
- package/dist/gateway/server-methods/push.js +53 -0
- package/dist/gateway/server-reload-handlers.js +2 -3
- package/dist/gateway/server-runtime-config.js +5 -0
- package/dist/gateway/server-runtime-state.js +2 -0
- package/dist/gateway/server-ws-runtime.js +1 -0
- package/dist/gateway/server.impl.js +296 -139
- package/dist/gateway/session-preview.test-helpers.js +11 -0
- package/dist/gateway/startup-auth.js +126 -0
- package/dist/gateway/test-helpers.agent-results.js +15 -0
- package/dist/gateway/test-helpers.mocks.js +37 -14
- package/dist/gateway/test-helpers.server.js +161 -77
- package/dist/hooks/bundled/session-memory/handler.js +165 -34
- package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
- package/dist/infra/archive-path.js +49 -0
- package/dist/infra/device-pairing.js +148 -167
- package/dist/infra/exec-approvals-allowlist.js +19 -70
- package/dist/infra/exec-approvals-analysis.js +44 -17
- package/dist/infra/exec-safe-bin-policy.js +269 -0
- package/dist/infra/fixed-window-rate-limit.js +33 -0
- package/dist/infra/git-root.js +61 -0
- package/dist/infra/heartbeat-active-hours.js +2 -2
- package/dist/infra/heartbeat-reason.js +40 -0
- package/dist/infra/heartbeat-runner.js +72 -32
- package/dist/infra/install-source-utils.js +91 -7
- package/dist/infra/node-pairing.js +50 -105
- package/dist/infra/npm-integrity.js +45 -0
- package/dist/infra/npm-pack-install.js +40 -0
- package/dist/infra/outbound/channel-adapters.js +20 -7
- package/dist/infra/outbound/message-action-runner.js +107 -327
- package/dist/infra/outbound/message.js +59 -36
- package/dist/infra/outbound/outbound-policy.js +52 -25
- package/dist/infra/outbound/outbound-send-service.js +58 -71
- package/dist/infra/pairing-files.js +10 -0
- package/dist/infra/plain-object.js +9 -0
- package/dist/infra/push-apns.js +365 -0
- package/dist/infra/restart-sentinel.js +16 -1
- package/dist/infra/restart.js +229 -26
- package/dist/infra/scp-host.js +54 -0
- package/dist/infra/update-startup.js +86 -9
- package/dist/media/inbound-path-policy.js +114 -0
- package/dist/media/input-files.js +16 -0
- package/dist/memory/test-manager.js +8 -0
- package/dist/plugin-sdk/temp-path.js +47 -0
- package/dist/plugins/discovery.js +217 -23
- package/dist/plugins/hook-runner-global.js +16 -0
- package/dist/plugins/loader.js +192 -26
- package/dist/plugins/logger.js +8 -0
- package/dist/plugins/manifest-registry.js +3 -0
- package/dist/plugins/path-safety.js +34 -0
- package/dist/plugins/registry.js +5 -2
- package/dist/plugins/runtime/index.js +271 -206
- package/dist/providers/github-copilot-models.js +4 -1
- package/dist/security/audit-channel.js +8 -19
- package/dist/security/audit-extra.async.js +354 -182
- package/dist/security/audit-extra.js +11 -1
- package/dist/security/audit-extra.sync.js +340 -33
- package/dist/security/audit-fs.js +31 -13
- package/dist/security/audit.js +145 -371
- package/dist/security/dm-policy-shared.js +24 -0
- package/dist/security/external-content.js +20 -8
- package/dist/security/fix.js +49 -85
- package/dist/security/scan-paths.js +20 -0
- package/dist/security/secret-equal.js +3 -7
- package/dist/security/windows-acl.js +30 -15
- package/dist/shared/node-list-parse.js +13 -0
- package/dist/shared/operator-scope-compat.js +37 -0
- package/dist/shared/text-chunking.js +29 -0
- package/dist/slack/blocks.test-helpers.js +31 -0
- package/dist/slack/monitor/mrkdwn.js +8 -0
- package/dist/telegram/bot-message-dispatch.js +366 -164
- package/dist/telegram/draft-stream.js +30 -7
- package/dist/telegram/reasoning-lane-coordinator.js +128 -0
- package/dist/terminal/prompt-select-styled.js +9 -0
- package/dist/test-utils/command-runner.js +6 -0
- package/dist/test-utils/internal-hook-event-payload.js +10 -0
- package/dist/test-utils/model-auth-mock.js +12 -0
- package/dist/test-utils/provider-usage-fetch.js +14 -0
- package/dist/test-utils/temp-home.js +33 -0
- package/dist/tui/components/chat-log.js +9 -0
- package/dist/tui/tui-command-handlers.js +36 -27
- package/dist/tui/tui-event-handlers.js +122 -32
- package/dist/tui/tui.js +181 -45
- package/dist/utils/mask-api-key.js +10 -0
- package/dist/utils/run-with-concurrency.js +39 -0
- package/dist/web/media.js +4 -0
- package/docs/tools/slash-commands.md +5 -1
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/feishu/src/external-keys.ts +19 -0
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/lobster/src/windows-spawn.ts +193 -0
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
- package/extensions/mattermost/package.json +1 -1
- package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
package/dist/gateway/auth.js
CHANGED
|
@@ -104,28 +104,60 @@ async function resolveVerifiedTailscaleUser(params) {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
export function resolveGatewayAuth(params) {
|
|
107
|
-
const
|
|
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
|
-
|
|
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 = "
|
|
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;
|
package/dist/gateway/call.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
144
|
-
process.env.CLAWDBOT_GATEWAY_PASSWORD
|
|
145
|
-
(isRemoteMode
|
|
146
|
-
?
|
|
147
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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";
|