@poolzin/pool-bot 2026.2.24 → 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 (191) hide show
  1. package/CHANGELOG.md +21 -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/sessions.test-helpers.js +61 -0
  63. package/dist/config/commands.js +3 -0
  64. package/dist/config/config.js +1 -1
  65. package/dist/config/env-substitution.js +62 -34
  66. package/dist/config/env-vars.js +9 -0
  67. package/dist/config/io.js +571 -171
  68. package/dist/config/merge-patch.js +50 -4
  69. package/dist/config/redact-snapshot.js +404 -76
  70. package/dist/config/schema.js +58 -570
  71. package/dist/config/validation.js +140 -85
  72. package/dist/config/zod-schema.hooks.js +40 -11
  73. package/dist/config/zod-schema.installs.js +20 -0
  74. package/dist/config/zod-schema.js +8 -7
  75. package/dist/daemon/cmd-argv.js +21 -0
  76. package/dist/daemon/cmd-set.js +58 -0
  77. package/dist/daemon/service-types.js +1 -0
  78. package/dist/discord/monitor/exec-approvals.js +357 -162
  79. package/dist/gateway/auth.js +38 -3
  80. package/dist/gateway/call.js +149 -68
  81. package/dist/gateway/canvas-capability.js +75 -0
  82. package/dist/gateway/control-plane-audit.js +28 -0
  83. package/dist/gateway/control-plane-rate-limit.js +53 -0
  84. package/dist/gateway/events.js +1 -0
  85. package/dist/gateway/hooks.js +109 -54
  86. package/dist/gateway/http-common.js +22 -0
  87. package/dist/gateway/method-scopes.js +169 -0
  88. package/dist/gateway/net.js +23 -0
  89. package/dist/gateway/openresponses-http.js +120 -110
  90. package/dist/gateway/probe-auth.js +2 -0
  91. package/dist/gateway/protocol/index.js +3 -2
  92. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  93. package/dist/gateway/protocol/schema/push.js +18 -0
  94. package/dist/gateway/protocol/schema.js +1 -0
  95. package/dist/gateway/server-http.js +236 -52
  96. package/dist/gateway/server-methods/agent.js +162 -24
  97. package/dist/gateway/server-methods/chat.js +461 -130
  98. package/dist/gateway/server-methods/config.js +193 -150
  99. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  100. package/dist/gateway/server-methods/nodes.js +251 -69
  101. package/dist/gateway/server-methods/push.js +53 -0
  102. package/dist/gateway/server-reload-handlers.js +2 -3
  103. package/dist/gateway/server-runtime-config.js +5 -0
  104. package/dist/gateway/server-runtime-state.js +2 -0
  105. package/dist/gateway/server-ws-runtime.js +1 -0
  106. package/dist/gateway/server.impl.js +296 -139
  107. package/dist/gateway/session-preview.test-helpers.js +11 -0
  108. package/dist/gateway/startup-auth.js +126 -0
  109. package/dist/gateway/test-helpers.agent-results.js +15 -0
  110. package/dist/gateway/test-helpers.mocks.js +37 -14
  111. package/dist/gateway/test-helpers.server.js +161 -77
  112. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  113. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  114. package/dist/infra/archive-path.js +49 -0
  115. package/dist/infra/device-pairing.js +148 -167
  116. package/dist/infra/exec-approvals-allowlist.js +19 -70
  117. package/dist/infra/exec-approvals-analysis.js +44 -17
  118. package/dist/infra/exec-safe-bin-policy.js +269 -0
  119. package/dist/infra/fixed-window-rate-limit.js +33 -0
  120. package/dist/infra/git-root.js +61 -0
  121. package/dist/infra/heartbeat-active-hours.js +2 -2
  122. package/dist/infra/heartbeat-reason.js +40 -0
  123. package/dist/infra/heartbeat-runner.js +72 -32
  124. package/dist/infra/install-source-utils.js +91 -7
  125. package/dist/infra/node-pairing.js +50 -105
  126. package/dist/infra/npm-integrity.js +45 -0
  127. package/dist/infra/npm-pack-install.js +40 -0
  128. package/dist/infra/outbound/channel-adapters.js +20 -7
  129. package/dist/infra/outbound/message-action-runner.js +107 -327
  130. package/dist/infra/outbound/message.js +59 -36
  131. package/dist/infra/outbound/outbound-policy.js +52 -25
  132. package/dist/infra/outbound/outbound-send-service.js +58 -71
  133. package/dist/infra/pairing-files.js +10 -0
  134. package/dist/infra/plain-object.js +9 -0
  135. package/dist/infra/push-apns.js +365 -0
  136. package/dist/infra/restart-sentinel.js +16 -1
  137. package/dist/infra/restart.js +229 -26
  138. package/dist/infra/scp-host.js +54 -0
  139. package/dist/infra/update-startup.js +86 -9
  140. package/dist/media/inbound-path-policy.js +114 -0
  141. package/dist/media/input-files.js +16 -0
  142. package/dist/memory/test-manager.js +8 -0
  143. package/dist/plugin-sdk/temp-path.js +47 -0
  144. package/dist/plugins/discovery.js +217 -23
  145. package/dist/plugins/hook-runner-global.js +16 -0
  146. package/dist/plugins/loader.js +192 -26
  147. package/dist/plugins/logger.js +8 -0
  148. package/dist/plugins/manifest-registry.js +3 -0
  149. package/dist/plugins/path-safety.js +34 -0
  150. package/dist/plugins/registry.js +5 -2
  151. package/dist/plugins/runtime/index.js +271 -206
  152. package/dist/providers/github-copilot-models.js +4 -1
  153. package/dist/security/audit-channel.js +8 -19
  154. package/dist/security/audit-extra.async.js +354 -182
  155. package/dist/security/audit-extra.js +11 -1
  156. package/dist/security/audit-extra.sync.js +340 -33
  157. package/dist/security/audit-fs.js +31 -13
  158. package/dist/security/audit.js +145 -371
  159. package/dist/security/dm-policy-shared.js +24 -0
  160. package/dist/security/external-content.js +20 -8
  161. package/dist/security/fix.js +49 -85
  162. package/dist/security/scan-paths.js +20 -0
  163. package/dist/security/secret-equal.js +3 -7
  164. package/dist/security/windows-acl.js +30 -15
  165. package/dist/shared/node-list-parse.js +13 -0
  166. package/dist/shared/operator-scope-compat.js +37 -0
  167. package/dist/shared/text-chunking.js +29 -0
  168. package/dist/slack/blocks.test-helpers.js +31 -0
  169. package/dist/slack/monitor/mrkdwn.js +8 -0
  170. package/dist/telegram/bot-message-dispatch.js +366 -164
  171. package/dist/telegram/draft-stream.js +30 -7
  172. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  173. package/dist/terminal/prompt-select-styled.js +9 -0
  174. package/dist/test-utils/command-runner.js +6 -0
  175. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  176. package/dist/test-utils/model-auth-mock.js +12 -0
  177. package/dist/test-utils/provider-usage-fetch.js +14 -0
  178. package/dist/test-utils/temp-home.js +33 -0
  179. package/dist/tui/components/chat-log.js +9 -0
  180. package/dist/tui/tui-command-handlers.js +36 -27
  181. package/dist/tui/tui-event-handlers.js +122 -32
  182. package/dist/tui/tui.js +181 -45
  183. package/dist/utils/mask-api-key.js +10 -0
  184. package/dist/utils/run-with-concurrency.js +39 -0
  185. package/dist/web/media.js +4 -0
  186. package/docs/tools/slash-commands.md +5 -1
  187. package/extensions/feishu/src/external-keys.ts +19 -0
  188. package/extensions/lobster/src/windows-spawn.ts +193 -0
  189. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  190. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  191. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
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
+
1
22
  ## v2026.2.24 (2026-02-19)
2
23
 
3
24
  ### 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
+ }