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