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