@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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## v2026.2.25 (2026-02-22)
2
+
3
+ ### Features
4
+ - **Upstream Port (OpenClaw):** ported 26 files from OpenClaw upstream covering agents, auto-reply, browser, gateway, hooks, plugins, config, and CLI
5
+ - **Agent tooling:** sandbox filesystem bridge, host sandbox bridge helpers, workspace sandbox integration, model fallback provider updates (openai/gpt-4.1-mini default)
6
+ - **Auto-reply:** commands registry extensions, templating updates, agent runner execution improvements, verbose-gated session notices
7
+ - **Browser:** config updates, agent act/snapshot/storage routes, tabs route enhancements, form layout contract tests
8
+ - **Gateway:** hook token extraction with query-string fallback, normalized agent payload with auto-generated session keys, HTTP server integration
9
+ - **Plugins:** plugin discovery and loading aligned with upstream patterns
10
+ - **Config:** browser config, CLI config, and schema updates
11
+
12
+ ### Fixes
13
+ - **Tests:** fixed 9 pre-existing test failures across 6 test files (52 tests total)
14
+ - `model-fallback.test.ts`: updated expectations for openai/gpt-4.1-mini default
15
+ - `image-tool.test.ts`: aligned sandbox parameter structure with new bridge API
16
+ - `hooks.test.ts`: updated for new `extractHookToken` and `normalizeAgentPayload` signatures
17
+ - `poolbot-tools.sessions.test.ts`: added mock sessions.list handler for spawned sessions
18
+ - `reply.media-note.test.ts` / `reply.raw-body.test.ts`: verbose-gated session notice, rebranded env vars and paths
19
+
20
+ ---
21
+
22
+ ## v2026.2.24 (2026-02-19)
23
+
24
+ ### Fixes
25
+ - **Tests:** fix 45 test files broken by upstream sync c7d07c3df — updated mocks and expectations across agents, config, cron, commands, routing, signal, and web modules; no production code changes
26
+ - **UI:** added `webhook` cron delivery mode to UI types and macOS Swift model to match production enum
27
+
28
+ ---
29
+
1
30
  ## v2026.2.23 (2026-02-18)
2
31
 
3
32
  ### Fixes
@@ -1,11 +1,187 @@
1
1
  import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
2
4
  import * as readline from "node:readline";
3
5
  import { Readable, Writable } from "node:stream";
6
+ import { fileURLToPath } from "node:url";
4
7
  import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream, } from "@agentclientprotocol/sdk";
5
8
  import { ensurePoolbotCliOnPath } from "../infra/path-env.js";
9
+ import { DANGEROUS_ACP_TOOLS } from "../security/dangerous-tools.js";
10
+ const SAFE_AUTO_APPROVE_KINDS = new Set(["read", "search"]);
11
+ function asRecord(value) {
12
+ return value && typeof value === "object" && !Array.isArray(value)
13
+ ? value
14
+ : undefined;
15
+ }
16
+ function readFirstStringValue(source, keys) {
17
+ if (!source) {
18
+ return undefined;
19
+ }
20
+ for (const key of keys) {
21
+ const value = source[key];
22
+ if (typeof value === "string" && value.trim()) {
23
+ return value.trim();
24
+ }
25
+ }
26
+ return undefined;
27
+ }
28
+ function normalizeToolName(value) {
29
+ const normalized = value.trim().toLowerCase();
30
+ if (!normalized) {
31
+ return undefined;
32
+ }
33
+ return normalized;
34
+ }
35
+ function parseToolNameFromTitle(title) {
36
+ if (!title) {
37
+ return undefined;
38
+ }
39
+ const head = title.split(":", 1)[0]?.trim();
40
+ if (!head || !/^[a-zA-Z0-9._-]+$/.test(head)) {
41
+ return undefined;
42
+ }
43
+ return normalizeToolName(head);
44
+ }
45
+ function resolveToolKindForPermission(params, toolName) {
46
+ const toolCall = params.toolCall;
47
+ const kindRaw = typeof toolCall?.kind === "string" ? toolCall.kind.trim().toLowerCase() : "";
48
+ if (kindRaw) {
49
+ return kindRaw;
50
+ }
51
+ const name = toolName ??
52
+ parseToolNameFromTitle(typeof toolCall?.title === "string" ? toolCall.title : undefined);
53
+ if (!name) {
54
+ return undefined;
55
+ }
56
+ const normalized = name.toLowerCase();
57
+ const hasToken = (token) => {
58
+ // Tool names tend to be snake_case. Avoid substring heuristics (ex: "thread" contains "read").
59
+ const re = new RegExp(`(?:^|[._-])${token}(?:$|[._-])`);
60
+ return re.test(normalized);
61
+ };
62
+ // Prefer a conservative classifier: only classify safe kinds when confident.
63
+ if (normalized === "read" || hasToken("read")) {
64
+ return "read";
65
+ }
66
+ if (normalized === "search" || hasToken("search") || hasToken("find")) {
67
+ return "search";
68
+ }
69
+ if (normalized.includes("fetch") || normalized.includes("http")) {
70
+ return "fetch";
71
+ }
72
+ if (normalized.includes("write") || normalized.includes("edit") || normalized.includes("patch")) {
73
+ return "edit";
74
+ }
75
+ if (normalized.includes("delete") || normalized.includes("remove")) {
76
+ return "delete";
77
+ }
78
+ if (normalized.includes("move") || normalized.includes("rename")) {
79
+ return "move";
80
+ }
81
+ if (normalized.includes("exec") || normalized.includes("run") || normalized.includes("bash")) {
82
+ return "execute";
83
+ }
84
+ return "other";
85
+ }
86
+ function resolveToolNameForPermission(params) {
87
+ const toolCall = params.toolCall;
88
+ const toolMeta = asRecord(toolCall?._meta);
89
+ const rawInput = asRecord(toolCall?.rawInput);
90
+ const fromMeta = readFirstStringValue(toolMeta, ["toolName", "tool_name", "name"]);
91
+ const fromRawInput = readFirstStringValue(rawInput, ["tool", "toolName", "tool_name", "name"]);
92
+ const fromTitle = parseToolNameFromTitle(toolCall?.title);
93
+ return normalizeToolName(fromMeta ?? fromRawInput ?? fromTitle ?? "");
94
+ }
95
+ function pickOption(options, kinds) {
96
+ for (const kind of kinds) {
97
+ const match = options.find((option) => option.kind === kind);
98
+ if (match) {
99
+ return match;
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+ function selectedPermission(optionId) {
105
+ return { outcome: { outcome: "selected", optionId } };
106
+ }
107
+ function cancelledPermission() {
108
+ return { outcome: { outcome: "cancelled" } };
109
+ }
110
+ function promptUserPermission(toolName, toolTitle) {
111
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
112
+ console.error(`[permission denied] ${toolName ?? "unknown"}: non-interactive terminal`);
113
+ return Promise.resolve(false);
114
+ }
115
+ return new Promise((resolve) => {
116
+ let settled = false;
117
+ const rl = readline.createInterface({
118
+ input: process.stdin,
119
+ output: process.stderr,
120
+ });
121
+ const finish = (approved) => {
122
+ if (settled) {
123
+ return;
124
+ }
125
+ settled = true;
126
+ clearTimeout(timeout);
127
+ rl.close();
128
+ resolve(approved);
129
+ };
130
+ const timeout = setTimeout(() => {
131
+ console.error(`\n[permission timeout] denied: ${toolName ?? "unknown"}`);
132
+ finish(false);
133
+ }, 30_000);
134
+ const label = toolTitle
135
+ ? toolName
136
+ ? `${toolTitle} (${toolName})`
137
+ : toolTitle
138
+ : (toolName ?? "unknown tool");
139
+ rl.question(`\n[permission] Allow "${label}"? (y/N) `, (answer) => {
140
+ const approved = answer.trim().toLowerCase() === "y";
141
+ console.error(`[permission ${approved ? "approved" : "denied"}] ${toolName ?? "unknown"}`);
142
+ finish(approved);
143
+ });
144
+ });
145
+ }
146
+ export async function resolvePermissionRequest(params, deps = {}) {
147
+ const log = deps.log ?? ((line) => console.error(line));
148
+ const prompt = deps.prompt ?? promptUserPermission;
149
+ const options = params.options ?? [];
150
+ const toolTitle = params.toolCall?.title ?? "tool";
151
+ const toolName = resolveToolNameForPermission(params);
152
+ const toolKind = resolveToolKindForPermission(params, toolName);
153
+ if (options.length === 0) {
154
+ log(`[permission cancelled] ${toolName ?? "unknown"}: no options available`);
155
+ return cancelledPermission();
156
+ }
157
+ const allowOption = pickOption(options, ["allow_once", "allow_always"]);
158
+ const rejectOption = pickOption(options, ["reject_once", "reject_always"]);
159
+ const isSafeKind = Boolean(toolKind && SAFE_AUTO_APPROVE_KINDS.has(toolKind));
160
+ const promptRequired = !toolName || !isSafeKind || DANGEROUS_ACP_TOOLS.has(toolName);
161
+ if (!promptRequired) {
162
+ const option = allowOption ?? options[0];
163
+ if (!option) {
164
+ log(`[permission cancelled] ${toolName}: no selectable options`);
165
+ return cancelledPermission();
166
+ }
167
+ log(`[permission auto-approved] ${toolName} (${toolKind ?? "unknown"})`);
168
+ return selectedPermission(option.optionId);
169
+ }
170
+ log(`\n[permission requested] ${toolTitle}${toolName ? ` (${toolName})` : ""}${toolKind ? ` [${toolKind}]` : ""}`);
171
+ const approved = await prompt(toolName, toolTitle);
172
+ if (approved && allowOption) {
173
+ return selectedPermission(allowOption.optionId);
174
+ }
175
+ if (!approved && rejectOption) {
176
+ return selectedPermission(rejectOption.optionId);
177
+ }
178
+ log(`[permission cancelled] ${toolName ?? "unknown"}: missing ${approved ? "allow" : "reject"} option`);
179
+ return cancelledPermission();
180
+ }
6
181
  function toArgs(value) {
7
- if (!value)
182
+ if (!value) {
8
183
  return [];
184
+ }
9
185
  return Array.isArray(value) ? value : [value];
10
186
  }
11
187
  function buildServerArgs(opts) {
@@ -15,10 +191,29 @@ function buildServerArgs(opts) {
15
191
  }
16
192
  return args;
17
193
  }
194
+ function resolveSelfEntryPath() {
195
+ // Prefer a path relative to the built module location (dist/acp/client.js -> dist/entry.js).
196
+ try {
197
+ const here = fileURLToPath(import.meta.url);
198
+ const candidate = path.resolve(path.dirname(here), "..", "entry.js");
199
+ if (fs.existsSync(candidate)) {
200
+ return candidate;
201
+ }
202
+ }
203
+ catch {
204
+ // ignore
205
+ }
206
+ const argv1 = process.argv[1]?.trim();
207
+ if (argv1) {
208
+ return path.isAbsolute(argv1) ? argv1 : path.resolve(process.cwd(), argv1);
209
+ }
210
+ return null;
211
+ }
18
212
  function printSessionUpdate(notification) {
19
213
  const update = notification.update;
20
- if (!("sessionUpdate" in update))
214
+ if (!("sessionUpdate" in update)) {
21
215
  return;
216
+ }
22
217
  switch (update.sessionUpdate) {
23
218
  case "agent_message_chunk": {
24
219
  if (update.content?.type === "text") {
@@ -38,8 +233,9 @@ function printSessionUpdate(notification) {
38
233
  }
39
234
  case "available_commands_update": {
40
235
  const names = update.availableCommands?.map((cmd) => `/${cmd.name}`).join(" ");
41
- if (names)
236
+ if (names) {
42
237
  console.log(`\n[commands] ${names}`);
238
+ }
43
239
  return;
44
240
  }
45
241
  default:
@@ -50,11 +246,13 @@ export async function createAcpClient(opts = {}) {
50
246
  const cwd = opts.cwd ?? process.cwd();
51
247
  const verbose = Boolean(opts.verbose);
52
248
  const log = verbose ? (msg) => console.error(`[acp-client] ${msg}`) : () => { };
53
- ensurePoolbotCliOnPath({ cwd });
54
- const serverCommand = opts.serverCommand ?? "poolbot";
249
+ ensurePoolbotCliOnPath();
55
250
  const serverArgs = buildServerArgs(opts);
56
- log(`spawning: ${serverCommand} ${serverArgs.join(" ")}`);
57
- const agent = spawn(serverCommand, serverArgs, {
251
+ const entryPath = resolveSelfEntryPath();
252
+ const serverCommand = opts.serverCommand ?? (entryPath ? process.execPath : "poolbot");
253
+ const effectiveArgs = opts.serverCommand || !entryPath ? serverArgs : [entryPath, ...serverArgs];
254
+ log(`spawning: ${serverCommand} ${effectiveArgs.join(" ")}`);
255
+ const agent = spawn(serverCommand, effectiveArgs, {
58
256
  stdio: ["pipe", "pipe", "inherit"],
59
257
  cwd,
60
258
  });
@@ -69,16 +267,7 @@ export async function createAcpClient(opts = {}) {
69
267
  printSessionUpdate(params);
70
268
  },
71
269
  requestPermission: async (params) => {
72
- console.log("\n[permission requested]", params.toolCall?.title ?? "tool");
73
- const options = params.options ?? [];
74
- const allowOnce = options.find((option) => option.kind === "allow_once");
75
- const fallback = options[0];
76
- return {
77
- outcome: {
78
- outcome: "selected",
79
- optionId: allowOnce?.optionId ?? fallback?.optionId ?? "allow",
80
- },
81
- };
270
+ return resolvePermissionRequest(params);
82
271
  },
83
272
  }), stream);
84
273
  log("initializing");
@@ -107,7 +296,7 @@ export async function runAcpClientInteractive(opts = {}) {
107
296
  input: process.stdin,
108
297
  output: process.stdout,
109
298
  });
110
- console.log("Poolbot ACP client");
299
+ console.log("Pool Bot ACP client");
111
300
  console.log(`Session: ${sessionId}`);
112
301
  console.log('Type a prompt, or "exit" to quit.\n');
113
302
  const prompt = () => {
@@ -0,0 +1,22 @@
1
+ import fs from "node:fs";
2
+ import { resolveUserPath } from "../utils.js";
3
+ export function readSecretFromFile(filePath, label) {
4
+ const resolvedPath = resolveUserPath(filePath.trim());
5
+ if (!resolvedPath) {
6
+ throw new Error(`${label} file path is empty.`);
7
+ }
8
+ let raw = "";
9
+ try {
10
+ raw = fs.readFileSync(resolvedPath, "utf8");
11
+ }
12
+ catch (err) {
13
+ throw new Error(`Failed to read ${label} file at ${resolvedPath}: ${String(err)}`, {
14
+ cause: err,
15
+ });
16
+ }
17
+ const secret = raw.trim();
18
+ if (!secret) {
19
+ throw new Error(`${label} file at ${resolvedPath} is empty.`);
20
+ }
21
+ return secret;
22
+ }
@@ -98,6 +98,16 @@ export function resolveAgentModelFallbacksOverride(cfg, agentId) {
98
98
  return undefined;
99
99
  return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
100
100
  }
101
+ export function resolveEffectiveModelFallbacks(params) {
102
+ const agentFallbacksOverride = resolveAgentModelFallbacksOverride(params.cfg, params.agentId);
103
+ if (!params.hasSessionModelOverride) {
104
+ return agentFallbacksOverride;
105
+ }
106
+ const defaultFallbacks = typeof params.cfg.agents?.defaults?.model === "object"
107
+ ? (params.cfg.agents.defaults.model.fallbacks ?? [])
108
+ : [];
109
+ return agentFallbacksOverride ?? defaultFallbacks;
110
+ }
101
111
  export function resolveAgentWorkspaceDir(cfg, agentId) {
102
112
  const id = normalizeAgentId(agentId);
103
113
  const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
@@ -0,0 +1,29 @@
1
+ export function createProcessSessionFixture(params) {
2
+ const session = {
3
+ id: params.id,
4
+ command: params.command ?? "test",
5
+ startedAt: params.startedAt ?? Date.now(),
6
+ cwd: params.cwd ?? "/tmp",
7
+ maxOutputChars: params.maxOutputChars ?? 10_000,
8
+ pendingMaxOutputChars: params.pendingMaxOutputChars ?? 30_000,
9
+ totalOutputChars: 0,
10
+ pendingStdout: [],
11
+ pendingStderr: [],
12
+ pendingStdoutChars: 0,
13
+ pendingStderrChars: 0,
14
+ aggregated: "",
15
+ tail: "",
16
+ exited: false,
17
+ exitCode: undefined,
18
+ exitSignal: undefined,
19
+ truncated: false,
20
+ backgrounded: params.backgrounded ?? false,
21
+ };
22
+ if (params.pid !== undefined) {
23
+ session.pid = params.pid;
24
+ }
25
+ if (params.child) {
26
+ session.child = params.child;
27
+ }
28
+ return session;
29
+ }
@@ -0,0 +1,20 @@
1
+ import { DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS, DEFAULT_APPROVAL_TIMEOUT_MS, } from "./bash-tools.exec-runtime.js";
2
+ import { callGatewayTool } from "./tools/gateway.js";
3
+ export async function requestExecApprovalDecision(params) {
4
+ const decisionResult = await callGatewayTool("exec.approval.request", { timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS }, {
5
+ id: params.id,
6
+ command: params.command,
7
+ cwd: params.cwd,
8
+ host: params.host,
9
+ security: params.security,
10
+ ask: params.ask,
11
+ agentId: params.agentId,
12
+ resolvedPath: params.resolvedPath,
13
+ sessionKey: params.sessionKey,
14
+ timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
15
+ });
16
+ const decisionValue = decisionResult && typeof decisionResult === "object"
17
+ ? decisionResult.decision
18
+ : undefined;
19
+ return typeof decisionValue === "string" ? decisionValue : null;
20
+ }
@@ -0,0 +1,230 @@
1
+ import crypto from "node:crypto";
2
+ import { addAllowlistEntry, buildSafeBinsShellCommand, buildSafeShellCommand, evaluateShellAllowlist, maxAsk, minSecurity, recordAllowlistUse, requiresExecApproval, resolveExecApprovals, } from "../infra/exec-approvals.js";
3
+ import { markBackgrounded, tail } from "./bash-process-registry.js";
4
+ import { requestExecApprovalDecision } from "./bash-tools.exec-approval-request.js";
5
+ import { DEFAULT_APPROVAL_TIMEOUT_MS, DEFAULT_NOTIFY_TAIL_CHARS, createApprovalSlug, emitExecSystemEvent, normalizeNotifyOutput, runExecProcess, } from "./bash-tools.exec-runtime.js";
6
+ export async function processGatewayAllowlist(params) {
7
+ const approvals = resolveExecApprovals(params.agentId, {
8
+ security: params.security,
9
+ ask: params.ask,
10
+ });
11
+ const hostSecurity = minSecurity(params.security, approvals.agent.security);
12
+ const hostAsk = maxAsk(params.ask, approvals.agent.ask);
13
+ const askFallback = approvals.agent.askFallback;
14
+ if (hostSecurity === "deny") {
15
+ throw new Error("exec denied: host=gateway security=deny");
16
+ }
17
+ const allowlistEval = evaluateShellAllowlist({
18
+ command: params.command,
19
+ allowlist: approvals.allowlist,
20
+ safeBins: params.safeBins,
21
+ cwd: params.workdir,
22
+ env: params.env,
23
+ platform: process.platform,
24
+ trustedSafeBinDirs: params.trustedSafeBinDirs,
25
+ });
26
+ const allowlistMatches = allowlistEval.allowlistMatches;
27
+ const analysisOk = allowlistEval.analysisOk;
28
+ const allowlistSatisfied = hostSecurity === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
29
+ const requiresAsk = requiresExecApproval({
30
+ ask: hostAsk,
31
+ security: hostSecurity,
32
+ analysisOk,
33
+ allowlistSatisfied,
34
+ });
35
+ if (requiresAsk) {
36
+ const approvalId = crypto.randomUUID();
37
+ const approvalSlug = createApprovalSlug(approvalId);
38
+ const expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
39
+ const contextKey = `exec:${approvalId}`;
40
+ const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath;
41
+ const noticeSeconds = Math.max(1, Math.round(params.approvalRunningNoticeMs / 1000));
42
+ const effectiveTimeout = typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec;
43
+ const warningText = params.warnings.length ? `${params.warnings.join("\n")}\n\n` : "";
44
+ void (async () => {
45
+ let decision = null;
46
+ try {
47
+ decision = await requestExecApprovalDecision({
48
+ id: approvalId,
49
+ command: params.command,
50
+ cwd: params.workdir,
51
+ host: "gateway",
52
+ security: hostSecurity,
53
+ ask: hostAsk,
54
+ agentId: params.agentId,
55
+ resolvedPath,
56
+ sessionKey: params.sessionKey,
57
+ });
58
+ }
59
+ catch {
60
+ emitExecSystemEvent(`Exec denied (gateway id=${approvalId}, approval-request-failed): ${params.command}`, {
61
+ sessionKey: params.notifySessionKey,
62
+ contextKey,
63
+ });
64
+ return;
65
+ }
66
+ let approvedByAsk = false;
67
+ let deniedReason = null;
68
+ if (decision === "deny") {
69
+ deniedReason = "user-denied";
70
+ }
71
+ else if (!decision) {
72
+ if (askFallback === "full") {
73
+ approvedByAsk = true;
74
+ }
75
+ else if (askFallback === "allowlist") {
76
+ if (!analysisOk || !allowlistSatisfied) {
77
+ deniedReason = "approval-timeout (allowlist-miss)";
78
+ }
79
+ else {
80
+ approvedByAsk = true;
81
+ }
82
+ }
83
+ else {
84
+ deniedReason = "approval-timeout";
85
+ }
86
+ }
87
+ else if (decision === "allow-once") {
88
+ approvedByAsk = true;
89
+ }
90
+ else if (decision === "allow-always") {
91
+ approvedByAsk = true;
92
+ if (hostSecurity === "allowlist") {
93
+ for (const segment of allowlistEval.segments) {
94
+ const pattern = segment.resolution?.resolvedPath ?? "";
95
+ if (pattern) {
96
+ addAllowlistEntry(approvals.file, params.agentId, pattern);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ if (hostSecurity === "allowlist" && (!analysisOk || !allowlistSatisfied) && !approvedByAsk) {
102
+ deniedReason = deniedReason ?? "allowlist-miss";
103
+ }
104
+ if (deniedReason) {
105
+ emitExecSystemEvent(`Exec denied (gateway id=${approvalId}, ${deniedReason}): ${params.command}`, {
106
+ sessionKey: params.notifySessionKey,
107
+ contextKey,
108
+ });
109
+ return;
110
+ }
111
+ if (allowlistMatches.length > 0) {
112
+ const seen = new Set();
113
+ for (const match of allowlistMatches) {
114
+ if (seen.has(match.pattern)) {
115
+ continue;
116
+ }
117
+ seen.add(match.pattern);
118
+ recordAllowlistUse(approvals.file, params.agentId, match, params.command, resolvedPath ?? undefined);
119
+ }
120
+ }
121
+ let run = null;
122
+ try {
123
+ run = await runExecProcess({
124
+ command: params.command,
125
+ workdir: params.workdir,
126
+ env: params.env,
127
+ sandbox: undefined,
128
+ containerWorkdir: null,
129
+ usePty: params.pty,
130
+ warnings: params.warnings,
131
+ maxOutput: params.maxOutput,
132
+ pendingMaxOutput: params.pendingMaxOutput,
133
+ notifyOnExit: false,
134
+ notifyOnExitEmptySuccess: false,
135
+ scopeKey: params.scopeKey,
136
+ sessionKey: params.notifySessionKey,
137
+ timeoutSec: effectiveTimeout,
138
+ });
139
+ }
140
+ catch {
141
+ emitExecSystemEvent(`Exec denied (gateway id=${approvalId}, spawn-failed): ${params.command}`, {
142
+ sessionKey: params.notifySessionKey,
143
+ contextKey,
144
+ });
145
+ return;
146
+ }
147
+ markBackgrounded(run.session);
148
+ let runningTimer = null;
149
+ if (params.approvalRunningNoticeMs > 0) {
150
+ runningTimer = setTimeout(() => {
151
+ emitExecSystemEvent(`Exec running (gateway id=${approvalId}, session=${run?.session.id}, >${noticeSeconds}s): ${params.command}`, { sessionKey: params.notifySessionKey, contextKey });
152
+ }, params.approvalRunningNoticeMs);
153
+ }
154
+ const outcome = await run.promise;
155
+ if (runningTimer) {
156
+ clearTimeout(runningTimer);
157
+ }
158
+ const output = normalizeNotifyOutput(tail(outcome.aggregated || "", DEFAULT_NOTIFY_TAIL_CHARS));
159
+ const exitLabel = outcome.timedOut ? "timeout" : `code ${outcome.exitCode ?? "?"}`;
160
+ const summary = output
161
+ ? `Exec finished (gateway id=${approvalId}, session=${run.session.id}, ${exitLabel})\n${output}`
162
+ : `Exec finished (gateway id=${approvalId}, session=${run.session.id}, ${exitLabel})`;
163
+ emitExecSystemEvent(summary, { sessionKey: params.notifySessionKey, contextKey });
164
+ })();
165
+ return {
166
+ pendingResult: {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: `${warningText}Approval required (id ${approvalSlug}). ` +
171
+ "Approve to run; updates will arrive after completion.",
172
+ },
173
+ ],
174
+ details: {
175
+ status: "approval-pending",
176
+ approvalId,
177
+ approvalSlug,
178
+ expiresAtMs,
179
+ host: "gateway",
180
+ command: params.command,
181
+ cwd: params.workdir,
182
+ },
183
+ },
184
+ };
185
+ }
186
+ if (hostSecurity === "allowlist" && (!analysisOk || !allowlistSatisfied)) {
187
+ throw new Error("exec denied: allowlist miss");
188
+ }
189
+ let execCommandOverride;
190
+ // If allowlist uses safeBins, sanitize only those stdin-only segments:
191
+ // disable glob/var expansion by forcing argv tokens to be literal via single-quoting.
192
+ if (hostSecurity === "allowlist" &&
193
+ analysisOk &&
194
+ allowlistSatisfied &&
195
+ allowlistEval.segmentSatisfiedBy.some((by) => by === "safeBins")) {
196
+ const safe = buildSafeBinsShellCommand({
197
+ command: params.command,
198
+ segments: allowlistEval.segments,
199
+ segmentSatisfiedBy: allowlistEval.segmentSatisfiedBy,
200
+ platform: process.platform,
201
+ });
202
+ if (!safe.ok || !safe.command) {
203
+ // Fallback: quote everything (safe, but may change glob behavior).
204
+ const fallback = buildSafeShellCommand({
205
+ command: params.command,
206
+ platform: process.platform,
207
+ });
208
+ if (!fallback.ok || !fallback.command) {
209
+ throw new Error(`exec denied: safeBins sanitize failed (${safe.reason ?? "unknown"})`);
210
+ }
211
+ params.warnings.push("Warning: safeBins hardening used fallback quoting due to parser mismatch.");
212
+ execCommandOverride = fallback.command;
213
+ }
214
+ else {
215
+ params.warnings.push("Warning: safeBins hardening disabled glob/variable expansion for stdin-only segments.");
216
+ execCommandOverride = safe.command;
217
+ }
218
+ }
219
+ if (allowlistMatches.length > 0) {
220
+ const seen = new Set();
221
+ for (const match of allowlistMatches) {
222
+ if (seen.has(match.pattern)) {
223
+ continue;
224
+ }
225
+ seen.add(match.pattern);
226
+ recordAllowlistUse(approvals.file, params.agentId, match, params.command, allowlistEval.segments[0]?.resolution?.resolvedPath);
227
+ }
228
+ }
229
+ return { execCommandOverride };
230
+ }