@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.
- package/CHANGELOG.md +21 -0
- package/dist/acp/client.js +207 -18
- package/dist/acp/secret-file.js +22 -0
- package/dist/agents/agent-scope.js +10 -0
- package/dist/agents/bash-process-registry.test-helpers.js +29 -0
- package/dist/agents/bash-tools.exec-approval-request.js +20 -0
- package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
- package/dist/agents/bash-tools.exec-host-node.js +235 -0
- package/dist/agents/bash-tools.exec-types.js +1 -0
- package/dist/agents/bash-tools.process.js +224 -218
- package/dist/agents/content-blocks.js +16 -0
- package/dist/agents/model-fallback.js +96 -101
- package/dist/agents/models-config.providers.js +299 -182
- package/dist/agents/pi-embedded-payloads.js +1 -0
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
- package/dist/agents/skills.test-helpers.js +13 -0
- package/dist/agents/stable-stringify.js +12 -0
- package/dist/agents/subagent-registry.mocks.shared.js +12 -0
- package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
- package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
- package/dist/agents/tool-policy-shared.js +108 -0
- package/dist/agents/tools/browser-tool.js +160 -54
- package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
- package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
- package/dist/agents/tools/image-tool.js +214 -99
- package/dist/agents/tools/sessions-history-tool.js +140 -108
- package/dist/agents/workspace.js +222 -46
- package/dist/auto-reply/commands-registry.js +15 -18
- package/dist/auto-reply/fallback-state.js +114 -0
- package/dist/auto-reply/model-runtime.js +68 -0
- package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
- package/dist/auto-reply/reply/agent-runner.js +165 -39
- package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
- package/dist/browser/config.js +26 -0
- package/dist/browser/navigation-guard.js +31 -0
- package/dist/browser/routes/agent.act.js +431 -424
- package/dist/browser/routes/agent.shared.js +47 -3
- package/dist/browser/routes/agent.snapshot.js +122 -116
- package/dist/browser/routes/agent.storage.js +303 -297
- package/dist/browser/routes/tabs.js +154 -100
- package/dist/browser/server-lifecycle.js +37 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/allow-from.js +25 -0
- package/dist/channels/plugins/account-action-gate.js +13 -0
- package/dist/channels/plugins/message-actions.js +10 -0
- package/dist/channels/telegram/api.js +18 -0
- package/dist/cli/argv.js +84 -21
- package/dist/cli/banner.js +2 -1
- package/dist/cli/exec-approvals-cli.js +92 -124
- package/dist/cli/memory-cli.js +158 -61
- package/dist/cli/nodes-cli/register.push.js +63 -0
- package/dist/cli/nodes-media-utils.js +21 -0
- package/dist/cli/plugins-cli.js +245 -61
- package/dist/cli/program/build-program.js +3 -1
- package/dist/cli/program/command-registry.js +223 -136
- package/dist/cli/program/help.js +43 -12
- package/dist/cli/route.js +1 -1
- package/dist/cli/test-runtime-capture.js +24 -0
- package/dist/commands/agent.js +163 -87
- package/dist/commands/channels.mock-harness.js +23 -0
- package/dist/commands/daemon-install-runtime-warning.js +11 -0
- package/dist/commands/sessions.test-helpers.js +61 -0
- package/dist/config/commands.js +3 -0
- package/dist/config/config.js +1 -1
- package/dist/config/env-substitution.js +62 -34
- package/dist/config/env-vars.js +9 -0
- package/dist/config/io.js +571 -171
- package/dist/config/merge-patch.js +50 -4
- package/dist/config/redact-snapshot.js +404 -76
- package/dist/config/schema.js +58 -570
- package/dist/config/validation.js +140 -85
- package/dist/config/zod-schema.hooks.js +40 -11
- package/dist/config/zod-schema.installs.js +20 -0
- package/dist/config/zod-schema.js +8 -7
- package/dist/daemon/cmd-argv.js +21 -0
- package/dist/daemon/cmd-set.js +58 -0
- package/dist/daemon/service-types.js +1 -0
- package/dist/discord/monitor/exec-approvals.js +357 -162
- package/dist/gateway/auth.js +38 -3
- package/dist/gateway/call.js +149 -68
- package/dist/gateway/canvas-capability.js +75 -0
- package/dist/gateway/control-plane-audit.js +28 -0
- package/dist/gateway/control-plane-rate-limit.js +53 -0
- package/dist/gateway/events.js +1 -0
- package/dist/gateway/hooks.js +109 -54
- package/dist/gateway/http-common.js +22 -0
- package/dist/gateway/method-scopes.js +169 -0
- package/dist/gateway/net.js +23 -0
- package/dist/gateway/openresponses-http.js +120 -110
- package/dist/gateway/probe-auth.js +2 -0
- package/dist/gateway/protocol/index.js +3 -2
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
- package/dist/gateway/protocol/schema/push.js +18 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-http.js +236 -52
- package/dist/gateway/server-methods/agent.js +162 -24
- package/dist/gateway/server-methods/chat.js +461 -130
- package/dist/gateway/server-methods/config.js +193 -150
- package/dist/gateway/server-methods/nodes.helpers.js +12 -0
- package/dist/gateway/server-methods/nodes.js +251 -69
- package/dist/gateway/server-methods/push.js +53 -0
- package/dist/gateway/server-reload-handlers.js +2 -3
- package/dist/gateway/server-runtime-config.js +5 -0
- package/dist/gateway/server-runtime-state.js +2 -0
- package/dist/gateway/server-ws-runtime.js +1 -0
- package/dist/gateway/server.impl.js +296 -139
- package/dist/gateway/session-preview.test-helpers.js +11 -0
- package/dist/gateway/startup-auth.js +126 -0
- package/dist/gateway/test-helpers.agent-results.js +15 -0
- package/dist/gateway/test-helpers.mocks.js +37 -14
- package/dist/gateway/test-helpers.server.js +161 -77
- package/dist/hooks/bundled/session-memory/handler.js +165 -34
- package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
- package/dist/infra/archive-path.js +49 -0
- package/dist/infra/device-pairing.js +148 -167
- package/dist/infra/exec-approvals-allowlist.js +19 -70
- package/dist/infra/exec-approvals-analysis.js +44 -17
- package/dist/infra/exec-safe-bin-policy.js +269 -0
- package/dist/infra/fixed-window-rate-limit.js +33 -0
- package/dist/infra/git-root.js +61 -0
- package/dist/infra/heartbeat-active-hours.js +2 -2
- package/dist/infra/heartbeat-reason.js +40 -0
- package/dist/infra/heartbeat-runner.js +72 -32
- package/dist/infra/install-source-utils.js +91 -7
- package/dist/infra/node-pairing.js +50 -105
- package/dist/infra/npm-integrity.js +45 -0
- package/dist/infra/npm-pack-install.js +40 -0
- package/dist/infra/outbound/channel-adapters.js +20 -7
- package/dist/infra/outbound/message-action-runner.js +107 -327
- package/dist/infra/outbound/message.js +59 -36
- package/dist/infra/outbound/outbound-policy.js +52 -25
- package/dist/infra/outbound/outbound-send-service.js +58 -71
- package/dist/infra/pairing-files.js +10 -0
- package/dist/infra/plain-object.js +9 -0
- package/dist/infra/push-apns.js +365 -0
- package/dist/infra/restart-sentinel.js +16 -1
- package/dist/infra/restart.js +229 -26
- package/dist/infra/scp-host.js +54 -0
- package/dist/infra/update-startup.js +86 -9
- package/dist/media/inbound-path-policy.js +114 -0
- package/dist/media/input-files.js +16 -0
- package/dist/memory/test-manager.js +8 -0
- package/dist/plugin-sdk/temp-path.js +47 -0
- package/dist/plugins/discovery.js +217 -23
- package/dist/plugins/hook-runner-global.js +16 -0
- package/dist/plugins/loader.js +192 -26
- package/dist/plugins/logger.js +8 -0
- package/dist/plugins/manifest-registry.js +3 -0
- package/dist/plugins/path-safety.js +34 -0
- package/dist/plugins/registry.js +5 -2
- package/dist/plugins/runtime/index.js +271 -206
- package/dist/providers/github-copilot-models.js +4 -1
- package/dist/security/audit-channel.js +8 -19
- package/dist/security/audit-extra.async.js +354 -182
- package/dist/security/audit-extra.js +11 -1
- package/dist/security/audit-extra.sync.js +340 -33
- package/dist/security/audit-fs.js +31 -13
- package/dist/security/audit.js +145 -371
- package/dist/security/dm-policy-shared.js +24 -0
- package/dist/security/external-content.js +20 -8
- package/dist/security/fix.js +49 -85
- package/dist/security/scan-paths.js +20 -0
- package/dist/security/secret-equal.js +3 -7
- package/dist/security/windows-acl.js +30 -15
- package/dist/shared/node-list-parse.js +13 -0
- package/dist/shared/operator-scope-compat.js +37 -0
- package/dist/shared/text-chunking.js +29 -0
- package/dist/slack/blocks.test-helpers.js +31 -0
- package/dist/slack/monitor/mrkdwn.js +8 -0
- package/dist/telegram/bot-message-dispatch.js +366 -164
- package/dist/telegram/draft-stream.js +30 -7
- package/dist/telegram/reasoning-lane-coordinator.js +128 -0
- package/dist/terminal/prompt-select-styled.js +9 -0
- package/dist/test-utils/command-runner.js +6 -0
- package/dist/test-utils/internal-hook-event-payload.js +10 -0
- package/dist/test-utils/model-auth-mock.js +12 -0
- package/dist/test-utils/provider-usage-fetch.js +14 -0
- package/dist/test-utils/temp-home.js +33 -0
- package/dist/tui/components/chat-log.js +9 -0
- package/dist/tui/tui-command-handlers.js +36 -27
- package/dist/tui/tui-event-handlers.js +122 -32
- package/dist/tui/tui.js +181 -45
- package/dist/utils/mask-api-key.js +10 -0
- package/dist/utils/run-with-concurrency.js +39 -0
- package/dist/web/media.js +4 -0
- package/docs/tools/slash-commands.md +5 -1
- package/extensions/feishu/src/external-keys.ts +19 -0
- package/extensions/lobster/src/windows-spawn.ts +193 -0
- package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
- package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
- 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
|
package/dist/acp/client.js
CHANGED
|
@@ -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(
|
|
54
|
-
const serverCommand = opts.serverCommand ?? "poolbot";
|
|
249
|
+
ensurePoolbotCliOnPath();
|
|
55
250
|
const serverArgs = buildServerArgs(opts);
|
|
56
|
-
|
|
57
|
-
const
|
|
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
|
-
|
|
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("
|
|
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
|
+
}
|