@poolzin/pool-bot 2026.2.23 → 2026.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ export function makeAttemptResult(overrides = {}) {
2
+ return {
3
+ aborted: false,
4
+ timedOut: false,
5
+ timedOutDuringCompaction: false,
6
+ promptError: null,
7
+ sessionIdUsed: "test-session",
8
+ assistantTexts: ["Hello!"],
9
+ toolMetas: [],
10
+ lastAssistant: undefined,
11
+ messagesSnapshot: [],
12
+ didSendViaMessagingTool: false,
13
+ messagingToolSentTexts: [],
14
+ messagingToolSentMediaUrls: [],
15
+ messagingToolSentTargets: [],
16
+ cloudCodeAssistFormatError: false,
17
+ ...overrides,
18
+ };
19
+ }
20
+ export function mockOverflowRetrySuccess(params) {
21
+ const overflowError = new Error(params.overflowMessage ?? "request_too_large: Request size exceeds model context window");
22
+ params.runEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: overflowError }));
23
+ params.runEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
24
+ params.compactDirect.mockResolvedValueOnce({
25
+ ok: true,
26
+ compacted: true,
27
+ result: {
28
+ summary: "Compacted session",
29
+ firstKeptEntryId: "entry-5",
30
+ tokensBefore: 150000,
31
+ },
32
+ });
33
+ return overflowError;
34
+ }
@@ -0,0 +1,13 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function writeSkill(params) {
4
+ const { dir, name, description, body } = params;
5
+ await fs.mkdir(dir, { recursive: true });
6
+ await fs.writeFile(path.join(dir, "SKILL.md"), `---
7
+ name: ${name}
8
+ description: ${description}
9
+ ---
10
+
11
+ ${body ?? `# ${name}\n`}
12
+ `, "utf-8");
13
+ }
@@ -0,0 +1,12 @@
1
+ export function stableStringify(value) {
2
+ if (value === null || typeof value !== "object") {
3
+ return JSON.stringify(value) ?? "null";
4
+ }
5
+ if (Array.isArray(value)) {
6
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
7
+ }
8
+ const record = value;
9
+ const keys = Object.keys(record).toSorted();
10
+ const entries = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
11
+ return `{${entries.join(",")}}`;
12
+ }
@@ -0,0 +1,12 @@
1
+ import { vi } from "vitest";
2
+ const noop = () => { };
3
+ vi.mock("../gateway/call.js", () => ({
4
+ callGateway: vi.fn(async () => ({
5
+ status: "ok",
6
+ startedAt: 111,
7
+ endedAt: 222,
8
+ })),
9
+ }));
10
+ vi.mock("../infra/agent-events.js", () => ({
11
+ onAgentEvent: vi.fn(() => noop),
12
+ }));
@@ -0,0 +1,29 @@
1
+ const ZERO_USAGE = {
2
+ input: 0,
3
+ output: 0,
4
+ cacheRead: 0,
5
+ cacheWrite: 0,
6
+ totalTokens: 0,
7
+ cost: {
8
+ input: 0,
9
+ output: 0,
10
+ cacheRead: 0,
11
+ cacheWrite: 0,
12
+ total: 0,
13
+ },
14
+ };
15
+ export function makeAssistantMessageFixture(overrides = {}) {
16
+ const errorText = typeof overrides.errorMessage === "string" ? overrides.errorMessage : "error";
17
+ return {
18
+ role: "assistant",
19
+ api: "openai-responses",
20
+ provider: "openai",
21
+ model: "test-model",
22
+ usage: ZERO_USAGE,
23
+ timestamp: 0,
24
+ stopReason: "error",
25
+ errorMessage: errorText,
26
+ content: [{ type: "text", text: errorText }],
27
+ ...overrides,
28
+ };
29
+ }
@@ -0,0 +1,27 @@
1
+ export function createPiToolsSandboxContext(params) {
2
+ const workspaceDir = params.workspaceDir;
3
+ return {
4
+ enabled: true,
5
+ sessionKey: params.sessionKey ?? "sandbox:test",
6
+ workspaceDir,
7
+ agentWorkspaceDir: params.agentWorkspaceDir ?? workspaceDir,
8
+ workspaceAccess: params.workspaceAccess ?? "rw",
9
+ containerName: params.containerName ?? "poolbot-sbx-test",
10
+ containerWorkdir: params.containerWorkdir ?? "/workspace",
11
+ fsBridge: params.fsBridge,
12
+ docker: {
13
+ image: "poolbot-sandbox:bookworm-slim",
14
+ containerPrefix: "poolbot-sbx-",
15
+ workdir: "/workspace",
16
+ readOnlyRoot: true,
17
+ tmpfs: [],
18
+ network: "none",
19
+ user: "1000:1000",
20
+ capDrop: ["ALL"],
21
+ env: { LANG: "C.UTF-8" },
22
+ ...params.dockerOverrides,
23
+ },
24
+ tools: params.tools ?? { allow: [], deny: [] },
25
+ browserAllowHostControl: params.browserAllowHostControl ?? false,
26
+ };
27
+ }
@@ -0,0 +1,108 @@
1
+ const TOOL_NAME_ALIASES = {
2
+ bash: "exec",
3
+ "apply-patch": "apply_patch",
4
+ };
5
+ export const TOOL_GROUPS = {
6
+ // NOTE: Keep canonical (lowercase) tool names here.
7
+ "group:memory": ["memory_search", "memory_get"],
8
+ "group:web": ["web_search", "web_fetch"],
9
+ // Basic workspace/file tools
10
+ "group:fs": ["read", "write", "edit", "apply_patch"],
11
+ // Host/runtime execution tools
12
+ "group:runtime": ["exec", "process"],
13
+ // Session management tools
14
+ "group:sessions": [
15
+ "sessions_list",
16
+ "sessions_history",
17
+ "sessions_send",
18
+ "sessions_spawn",
19
+ "subagents",
20
+ "session_status",
21
+ ],
22
+ // UI helpers
23
+ "group:ui": ["browser", "canvas"],
24
+ // Automation + infra
25
+ "group:automation": ["cron", "gateway"],
26
+ // Messaging surface
27
+ "group:messaging": ["message"],
28
+ // Nodes + device tools
29
+ "group:nodes": ["nodes"],
30
+ // All Pool Bot native tools (excludes provider plugins).
31
+ "group:poolbot": [
32
+ "browser",
33
+ "canvas",
34
+ "nodes",
35
+ "cron",
36
+ "message",
37
+ "gateway",
38
+ "agents_list",
39
+ "sessions_list",
40
+ "sessions_history",
41
+ "sessions_send",
42
+ "sessions_spawn",
43
+ "subagents",
44
+ "session_status",
45
+ "memory_search",
46
+ "memory_get",
47
+ "web_search",
48
+ "web_fetch",
49
+ "image",
50
+ ],
51
+ };
52
+ const TOOL_PROFILES = {
53
+ minimal: {
54
+ allow: ["session_status"],
55
+ },
56
+ coding: {
57
+ allow: ["group:fs", "group:runtime", "group:sessions", "group:memory", "image"],
58
+ },
59
+ messaging: {
60
+ allow: [
61
+ "group:messaging",
62
+ "sessions_list",
63
+ "sessions_history",
64
+ "sessions_send",
65
+ "session_status",
66
+ ],
67
+ },
68
+ full: {},
69
+ };
70
+ export function normalizeToolName(name) {
71
+ const normalized = name.trim().toLowerCase();
72
+ return TOOL_NAME_ALIASES[normalized] ?? normalized;
73
+ }
74
+ export function normalizeToolList(list) {
75
+ if (!list) {
76
+ return [];
77
+ }
78
+ return list.map(normalizeToolName).filter(Boolean);
79
+ }
80
+ export function expandToolGroups(list) {
81
+ const normalized = normalizeToolList(list);
82
+ const expanded = [];
83
+ for (const value of normalized) {
84
+ const group = TOOL_GROUPS[value];
85
+ if (group) {
86
+ expanded.push(...group);
87
+ continue;
88
+ }
89
+ expanded.push(value);
90
+ }
91
+ return Array.from(new Set(expanded));
92
+ }
93
+ export function resolveToolProfilePolicy(profile) {
94
+ if (!profile) {
95
+ return undefined;
96
+ }
97
+ const resolved = TOOL_PROFILES[profile];
98
+ if (!resolved) {
99
+ return undefined;
100
+ }
101
+ if (!resolved.allow && !resolved.deny) {
102
+ return undefined;
103
+ }
104
+ return {
105
+ allow: resolved.allow ? [...resolved.allow] : undefined,
106
+ deny: resolved.deny ? [...resolved.deny] : undefined,
107
+ };
108
+ }
@@ -1,14 +1,35 @@
1
- import { browserCloseTab, browserFocusTab, browserOpenTab, browserProfiles, browserSnapshot, browserStart, browserStatus, browserStop, browserTabs, } from "../../browser/client.js";
2
- import { browserAct, browserArmDialog, browserArmFileChooser, browserConsoleMessages, browserNavigate, browserPdfSave, browserScreenshotAction, } from "../../browser/client-actions.js";
3
1
  import crypto from "node:crypto";
2
+ import { browserAct, browserArmDialog, browserArmFileChooser, browserConsoleMessages, browserNavigate, browserPdfSave, browserScreenshotAction, } from "../../browser/client-actions.js";
3
+ import { browserCloseTab, browserFocusTab, browserOpenTab, browserProfiles, browserSnapshot, browserStart, browserStatus, browserStop, browserTabs, } from "../../browser/client.js";
4
4
  import { resolveBrowserConfig } from "../../browser/config.js";
5
5
  import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
6
+ import { DEFAULT_UPLOAD_DIR, resolvePathsWithinRoot } from "../../browser/paths.js";
7
+ import { applyBrowserProxyPaths, persistBrowserProxyFiles } from "../../browser/proxy-files.js";
6
8
  import { loadConfig } from "../../config/config.js";
7
- import { saveMediaBuffer } from "../../media/store.js";
8
- import { listNodes, resolveNodeIdFromList } from "./nodes-utils.js";
9
+ import { wrapExternalContent } from "../../security/external-content.js";
9
10
  import { BrowserToolSchema } from "./browser-tool.schema.js";
10
11
  import { imageResultFromFile, jsonResult, readStringParam } from "./common.js";
11
12
  import { callGatewayTool } from "./gateway.js";
13
+ import { listNodes, resolveNodeIdFromList } from "./nodes-utils.js";
14
+ function wrapBrowserExternalJson(params) {
15
+ const extractedText = JSON.stringify(params.payload, null, 2);
16
+ const wrappedText = wrapExternalContent(extractedText, {
17
+ source: "browser",
18
+ includeWarning: params.includeWarning ?? true,
19
+ });
20
+ return {
21
+ wrappedText,
22
+ safeDetails: {
23
+ ok: true,
24
+ externalContent: {
25
+ untrusted: true,
26
+ source: "browser",
27
+ kind: params.kind,
28
+ wrapped: true,
29
+ },
30
+ },
31
+ };
32
+ }
12
33
  const DEFAULT_BROWSER_PROXY_TIMEOUT_MS = 20_000;
13
34
  function isBrowserNode(node) {
14
35
  const caps = Array.isArray(node.caps) ? node.caps : [];
@@ -28,8 +49,9 @@ async function resolveBrowserNodeTarget(params) {
28
49
  if (params.sandboxBridgeUrl?.trim() && params.target !== "node" && !params.requestedNode) {
29
50
  return null;
30
51
  }
31
- if (params.target && params.target !== "node")
52
+ if (params.target && params.target !== "node") {
32
53
  return null;
54
+ }
33
55
  if (mode === "manual" && params.target !== "node" && !params.requestedNode) {
34
56
  return null;
35
57
  }
@@ -54,8 +76,9 @@ async function resolveBrowserNodeTarget(params) {
54
76
  }
55
77
  throw new Error(`Multiple browser-capable nodes connected (${browserNodes.length}). Set gateway.nodes.browser.node or pass node=<id>.`);
56
78
  }
57
- if (mode === "manual")
79
+ if (mode === "manual") {
58
80
  return null;
81
+ }
59
82
  if (browserNodes.length === 1) {
60
83
  const node = browserNodes[0];
61
84
  return { nodeId: node.nodeId, label: node.displayName ?? node.remoteIp ?? node.nodeId };
@@ -66,7 +89,7 @@ async function callBrowserProxy(params) {
66
89
  const gatewayTimeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
67
90
  ? Math.max(1, Math.floor(params.timeoutMs))
68
91
  : DEFAULT_BROWSER_PROXY_TIMEOUT_MS;
69
- const payload = (await callGatewayTool("node.invoke", { timeoutMs: gatewayTimeoutMs }, {
92
+ const payload = await callGatewayTool("node.invoke", { timeoutMs: gatewayTimeoutMs }, {
70
93
  nodeId: params.nodeId,
71
94
  command: "browser.proxy",
72
95
  params: {
@@ -78,44 +101,21 @@ async function callBrowserProxy(params) {
78
101
  profile: params.profile,
79
102
  },
80
103
  idempotencyKey: crypto.randomUUID(),
81
- }));
104
+ });
82
105
  const parsed = payload?.payload ??
83
106
  (typeof payload?.payloadJSON === "string" && payload.payloadJSON
84
107
  ? JSON.parse(payload.payloadJSON)
85
108
  : null);
86
- if (!parsed || typeof parsed !== "object") {
109
+ if (!parsed || typeof parsed !== "object" || !("result" in parsed)) {
87
110
  throw new Error("browser proxy failed");
88
111
  }
89
112
  return parsed;
90
113
  }
91
114
  async function persistProxyFiles(files) {
92
- if (!files || files.length === 0)
93
- return new Map();
94
- const mapping = new Map();
95
- for (const file of files) {
96
- const buffer = Buffer.from(file.base64, "base64");
97
- const saved = await saveMediaBuffer(buffer, file.mimeType, "browser", buffer.byteLength);
98
- mapping.set(file.path, saved.path);
99
- }
100
- return mapping;
115
+ return await persistBrowserProxyFiles(files);
101
116
  }
102
117
  function applyProxyPaths(result, mapping) {
103
- if (!result || typeof result !== "object")
104
- return;
105
- const obj = result;
106
- if (typeof obj.path === "string" && mapping.has(obj.path)) {
107
- obj.path = mapping.get(obj.path);
108
- }
109
- if (typeof obj.imagePath === "string" && mapping.has(obj.imagePath)) {
110
- obj.imagePath = mapping.get(obj.imagePath);
111
- }
112
- const download = obj.download;
113
- if (download && typeof download === "object") {
114
- const d = download;
115
- if (typeof d.path === "string" && mapping.has(d.path)) {
116
- d.path = mapping.get(d.path);
117
- }
118
- }
118
+ applyBrowserProxyPaths(result, mapping);
119
119
  }
120
120
  function resolveBrowserBaseUrl(params) {
121
121
  const cfg = loadConfig();
@@ -143,11 +143,11 @@ export function createBrowserTool(opts) {
143
143
  label: "Browser",
144
144
  name: "browser",
145
145
  description: [
146
- "Control the browser via Poolbot's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
146
+ "Control the browser via Pool Bot's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
147
147
  'Profiles: use profile="chrome" for Chrome extension relay takeover (your existing Chrome tabs). Use profile="clawd" for the isolated clawd-managed browser.',
148
- 'If the user mentions the Chrome extension / Browser Relay / toolbar button / attach tab”, ALWAYS use profile="chrome" (do not ask which profile).',
148
+ 'If the user mentions the Chrome extension / Browser Relay / toolbar button / "attach tab", ALWAYS use profile="chrome" (do not ask which profile).',
149
149
  'When a node-hosted browser proxy is available, the tool may auto-route to it. Pin a node with node=<id|name> or target="node".',
150
- "Chrome extension relay needs an attached tab: user must click the Poolbot Browser Relay toolbar icon on the tab (badge ON). If no tab is connected, ask them to attach it.",
150
+ "Chrome extension relay needs an attached tab: user must click the Pool Bot Browser Relay toolbar icon on the tab (badge ON). If no tab is connected, ask them to attach it.",
151
151
  "When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc).",
152
152
  'For stable, self-resolving refs across calls, use snapshot with refs="aria" (Playwright aria-ref ids). Default refs="role" are role+name-based.',
153
153
  "Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
@@ -254,9 +254,28 @@ export function createBrowserTool(opts) {
254
254
  profile,
255
255
  });
256
256
  const tabs = result.tabs ?? [];
257
- return jsonResult({ tabs });
257
+ const wrapped = wrapBrowserExternalJson({
258
+ kind: "tabs",
259
+ payload: { tabs },
260
+ includeWarning: false,
261
+ });
262
+ return {
263
+ content: [{ type: "text", text: wrapped.wrappedText }],
264
+ details: { ...wrapped.safeDetails, tabCount: tabs.length },
265
+ };
266
+ }
267
+ {
268
+ const tabs = await browserTabs(baseUrl, { profile });
269
+ const wrapped = wrapBrowserExternalJson({
270
+ kind: "tabs",
271
+ payload: { tabs },
272
+ includeWarning: false,
273
+ });
274
+ return {
275
+ content: [{ type: "text", text: wrapped.wrappedText }],
276
+ details: { ...wrapped.safeDetails, tabCount: tabs.length },
277
+ };
258
278
  }
259
- return jsonResult({ tabs: await browserTabs(baseUrl, { profile }) });
260
279
  case "open": {
261
280
  const targetUrl = readStringParam(params, "targetUrl", {
262
281
  required: true,
@@ -305,10 +324,12 @@ export function createBrowserTool(opts) {
305
324
  });
306
325
  return jsonResult(result);
307
326
  }
308
- if (targetId)
327
+ if (targetId) {
309
328
  await browserCloseTab(baseUrl, targetId, { profile });
310
- else
329
+ }
330
+ else {
311
331
  await browserAct(baseUrl, { kind: "close" }, { profile });
332
+ }
312
333
  return jsonResult({ ok: true });
313
334
  }
314
335
  case "snapshot": {
@@ -383,20 +404,68 @@ export function createBrowserTool(opts) {
383
404
  profile,
384
405
  });
385
406
  if (snapshot.format === "ai") {
407
+ const extractedText = snapshot.snapshot ?? "";
408
+ const wrappedSnapshot = wrapExternalContent(extractedText, {
409
+ source: "browser",
410
+ includeWarning: true,
411
+ });
412
+ const safeDetails = {
413
+ ok: true,
414
+ format: snapshot.format,
415
+ targetId: snapshot.targetId,
416
+ url: snapshot.url,
417
+ truncated: snapshot.truncated,
418
+ stats: snapshot.stats,
419
+ refs: snapshot.refs ? Object.keys(snapshot.refs).length : undefined,
420
+ labels: snapshot.labels,
421
+ labelsCount: snapshot.labelsCount,
422
+ labelsSkipped: snapshot.labelsSkipped,
423
+ imagePath: snapshot.imagePath,
424
+ imageType: snapshot.imageType,
425
+ externalContent: {
426
+ untrusted: true,
427
+ source: "browser",
428
+ kind: "snapshot",
429
+ format: "ai",
430
+ wrapped: true,
431
+ },
432
+ };
386
433
  if (labels && snapshot.imagePath) {
387
434
  return await imageResultFromFile({
388
435
  label: "browser:snapshot",
389
436
  path: snapshot.imagePath,
390
- extraText: snapshot.snapshot,
391
- details: snapshot,
437
+ extraText: wrappedSnapshot,
438
+ details: safeDetails,
392
439
  });
393
440
  }
394
441
  return {
395
- content: [{ type: "text", text: snapshot.snapshot }],
396
- details: snapshot,
442
+ content: [{ type: "text", text: wrappedSnapshot }],
443
+ details: safeDetails,
444
+ };
445
+ }
446
+ {
447
+ const wrapped = wrapBrowserExternalJson({
448
+ kind: "snapshot",
449
+ payload: snapshot,
450
+ });
451
+ return {
452
+ content: [{ type: "text", text: wrapped.wrappedText }],
453
+ details: {
454
+ ...wrapped.safeDetails,
455
+ format: "aria",
456
+ targetId: snapshot.targetId,
457
+ url: snapshot.url,
458
+ nodeCount: snapshot.nodes.length,
459
+ externalContent: {
460
+ untrusted: true,
461
+ source: "browser",
462
+ kind: "snapshot",
463
+ format: "aria",
464
+ wrapped: true,
465
+ },
466
+ },
397
467
  };
398
468
  }
399
- return jsonResult(snapshot);
400
469
  }
401
470
  case "screenshot": {
402
471
  const targetId = readStringParam(params, "targetId");
@@ -458,7 +527,7 @@ export function createBrowserTool(opts) {
458
527
  const level = typeof params.level === "string" ? params.level.trim() : undefined;
459
528
  const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
460
529
  if (proxyRequest) {
461
- const result = await proxyRequest({
530
+ const result = (await proxyRequest({
462
531
  method: "GET",
463
532
  path: "/console",
464
533
  profile,
@@ -466,10 +535,37 @@ export function createBrowserTool(opts) {
466
535
  level,
467
536
  targetId,
468
537
  },
538
+ }));
539
+ const wrapped = wrapBrowserExternalJson({
540
+ kind: "console",
541
+ payload: result,
542
+ includeWarning: false,
543
+ });
544
+ return {
545
+ content: [{ type: "text", text: wrapped.wrappedText }],
546
+ details: {
547
+ ...wrapped.safeDetails,
548
+ targetId: typeof result.targetId === "string" ? result.targetId : undefined,
549
+ messageCount: Array.isArray(result.messages) ? result.messages.length : undefined,
550
+ },
551
+ };
552
+ }
553
+ {
554
+ const result = await browserConsoleMessages(baseUrl, { level, targetId, profile });
555
+ const wrapped = wrapBrowserExternalJson({
556
+ kind: "console",
557
+ payload: result,
558
+ includeWarning: false,
469
559
  });
470
- return jsonResult(result);
560
+ return {
561
+ content: [{ type: "text", text: wrapped.wrappedText }],
562
+ details: {
563
+ ...wrapped.safeDetails,
564
+ targetId: result.targetId,
565
+ messageCount: result.messages.length,
566
+ },
567
+ };
471
568
  }
472
- return jsonResult(await browserConsoleMessages(baseUrl, { level, targetId, profile }));
473
569
  }
474
570
  case "pdf": {
475
571
  const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
@@ -488,8 +584,18 @@ export function createBrowserTool(opts) {
488
584
  }
489
585
  case "upload": {
490
586
  const paths = Array.isArray(params.paths) ? params.paths.map((p) => String(p)) : [];
491
- if (paths.length === 0)
587
+ if (paths.length === 0) {
492
588
  throw new Error("paths required");
589
+ }
590
+ const uploadPathsResult = resolvePathsWithinRoot({
591
+ rootDir: DEFAULT_UPLOAD_DIR,
592
+ requestedPaths: paths,
593
+ scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`,
594
+ });
595
+ if (!uploadPathsResult.ok) {
596
+ throw new Error(uploadPathsResult.error);
597
+ }
598
+ const normalizedPaths = uploadPathsResult.paths;
493
599
  const ref = readStringParam(params, "ref");
494
600
  const inputRef = readStringParam(params, "inputRef");
495
601
  const element = readStringParam(params, "element");
@@ -503,7 +609,7 @@ export function createBrowserTool(opts) {
503
609
  path: "/hooks/file-chooser",
504
610
  profile,
505
611
  body: {
506
- paths,
612
+ paths: normalizedPaths,
507
613
  ref,
508
614
  inputRef,
509
615
  element,
@@ -514,7 +620,7 @@ export function createBrowserTool(opts) {
514
620
  return jsonResult(result);
515
621
  }
516
622
  return jsonResult(await browserArmFileChooser(baseUrl, {
517
- paths,
623
+ paths: normalizedPaths,
518
624
  ref,
519
625
  inputRef,
520
626
  element,
@@ -581,9 +687,9 @@ export function createBrowserTool(opts) {
581
687
  })).tabs ?? [])
582
688
  : await browserTabs(baseUrl, { profile }).catch(() => []);
583
689
  if (!tabs.length) {
584
- throw new Error("No Chrome tabs are attached via the Poolbot Browser Relay extension. Click the toolbar icon on the tab you want to control (badge ON), then retry.");
690
+ throw new Error("No Chrome tabs are attached via the Pool Bot Browser Relay extension. Click the toolbar icon on the tab you want to control (badge ON), then retry.", { cause: err });
585
691
  }
586
- throw new Error(`Chrome tab not found (stale targetId?). Run action=tabs profile="chrome" and use one of the returned targetIds.`);
692
+ throw new Error(`Chrome tab not found (stale targetId?). Run action=tabs profile="chrome" and use one of the returned targetIds.`, { cause: err });
587
693
  }
588
694
  throw err;
589
695
  }
@@ -0,0 +1,12 @@
1
+ import { vi } from "vitest";
2
+ export const callGatewayMock = vi.fn();
3
+ vi.mock("../../gateway/call.js", () => ({
4
+ callGateway: (opts) => callGatewayMock(opts),
5
+ }));
6
+ vi.mock("../agent-scope.js", () => ({
7
+ resolveSessionAgentId: () => "agent-123",
8
+ }));
9
+ export function resetCronToolGatewayMock() {
10
+ callGatewayMock.mockReset();
11
+ callGatewayMock.mockResolvedValue({ ok: true });
12
+ }
@@ -0,0 +1,27 @@
1
+ import { PermissionFlagsBits } from "discord-api-types/v10";
2
+ import { readNumberParam, readStringParam } from "./common.js";
3
+ const moderationPermissions = {
4
+ timeout: PermissionFlagsBits.ModerateMembers,
5
+ kick: PermissionFlagsBits.KickMembers,
6
+ ban: PermissionFlagsBits.BanMembers,
7
+ };
8
+ export function isDiscordModerationAction(action) {
9
+ return action === "timeout" || action === "kick" || action === "ban";
10
+ }
11
+ export function requiredGuildPermissionForModerationAction(action) {
12
+ return moderationPermissions[action];
13
+ }
14
+ export function readDiscordModerationCommand(action, params) {
15
+ if (!isDiscordModerationAction(action)) {
16
+ throw new Error(`Unsupported Discord moderation action: ${action}`);
17
+ }
18
+ return {
19
+ action,
20
+ guildId: readStringParam(params, "guildId", { required: true }),
21
+ userId: readStringParam(params, "userId", { required: true }),
22
+ durationMinutes: readNumberParam(params, "durationMinutes", { integer: true }),
23
+ until: readStringParam(params, "until"),
24
+ reason: readStringParam(params, "reason"),
25
+ deleteMessageDays: readNumberParam(params, "deleteMessageDays", { integer: true }),
26
+ };
27
+ }