@poolzin/pool-bot 2026.2.21 → 2026.2.22
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 +17 -0
- package/dist/agents/api-key-rotation.js +47 -0
- package/dist/agents/apply-patch-update.js +19 -9
- package/dist/agents/apply-patch.js +72 -47
- package/dist/agents/bash-tools.exec.js +141 -559
- package/dist/agents/cli-backends.js +49 -6
- package/dist/agents/cli-runner/helpers.js +69 -152
- package/dist/agents/cli-runner.js +70 -19
- package/dist/agents/identity.js +20 -1
- package/dist/agents/image-sanitization.js +9 -0
- package/dist/agents/live-auth-keys.js +123 -26
- package/dist/agents/live-model-filter.js +13 -4
- package/dist/agents/model-catalog.js +40 -9
- package/dist/agents/model-forward-compat.js +60 -23
- package/dist/agents/model-selection.js +134 -41
- package/dist/agents/pi-auth-json.js +2 -2
- package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
- package/dist/agents/pi-embedded-helpers/errors.js +140 -15
- package/dist/agents/pi-embedded-helpers/images.js +22 -12
- package/dist/agents/pi-embedded-helpers.js +2 -2
- package/dist/agents/pi-embedded-runner/abort.js +10 -3
- package/dist/agents/pi-embedded-runner/compact.js +230 -32
- package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
- package/dist/agents/pi-embedded-runner/google.js +109 -19
- package/dist/agents/pi-embedded-runner/history.js +35 -17
- package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
- package/dist/agents/pi-embedded-runner/run/images.js +81 -55
- package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
- package/dist/agents/pi-embedded-runner/run.js +193 -25
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
- package/dist/agents/pi-embedded-runner/runs.js +17 -8
- package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
- package/dist/agents/pi-embedded-runner.js +1 -1
- package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
- package/dist/agents/pi-embedded-subscribe.js +37 -0
- package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
- package/dist/agents/pi-model-discovery.js +9 -2
- package/dist/agents/pi-tool-definition-adapter.js +60 -8
- package/dist/agents/pi-tools.before-tool-call.js +1 -1
- package/dist/agents/pi-tools.js +113 -94
- package/dist/agents/pi-tools.read.js +337 -38
- package/dist/agents/poolbot-tools.js +14 -5
- package/dist/agents/sandbox/docker.js +10 -5
- package/dist/agents/sandbox/registry.js +96 -46
- package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
- package/dist/agents/sandbox-paths.js +43 -10
- package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
- package/dist/agents/session-tool-result-guard.js +39 -39
- package/dist/agents/session-transcript-repair.js +36 -33
- package/dist/agents/session-write-lock.js +62 -44
- package/dist/agents/skills/frontmatter.js +49 -88
- package/dist/agents/skills/workspace.js +335 -28
- package/dist/agents/subagent-announce.js +508 -174
- package/dist/agents/subagent-registry.js +45 -4
- package/dist/agents/subagent-spawn.js +16 -33
- package/dist/agents/system-prompt-report.js +27 -10
- package/dist/agents/system-prompt.js +26 -32
- package/dist/agents/tool-call-id.js +69 -17
- package/dist/agents/tool-display-common.js +1 -1
- package/dist/agents/tool-images.js +64 -31
- package/dist/agents/tools/canvas-tool.js +17 -11
- package/dist/agents/tools/common.js +37 -19
- package/dist/agents/tools/cron-tool.js +40 -38
- package/dist/agents/tools/gateway.js +70 -2
- package/dist/agents/tools/message-tool.js +181 -40
- package/dist/agents/tools/nodes-tool.js +128 -36
- package/dist/agents/tools/nodes-utils.js +12 -38
- package/dist/agents/tools/session-status-tool.js +24 -71
- package/dist/agents/tools/sessions-helpers.js +38 -210
- package/dist/agents/tools/sessions-spawn-tool.js +28 -198
- package/dist/agents/tools/telegram-actions.js +58 -7
- package/dist/agents/tools/web-fetch-utils.js +112 -7
- package/dist/agents/tools/web-fetch.js +279 -175
- package/dist/agents/tools/web-shared.js +71 -8
- package/dist/agents/usage.js +25 -16
- package/dist/auto-reply/commands-registry.data.js +85 -11
- package/dist/auto-reply/dispatch.js +40 -21
- package/dist/auto-reply/reply/abort.js +102 -33
- package/dist/auto-reply/reply/commands-core.js +82 -33
- package/dist/auto-reply/reply/commands-export-session.js +1 -1
- package/dist/auto-reply/reply/commands-info.js +41 -12
- package/dist/auto-reply/reply/commands-subagents.js +352 -100
- package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
- package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
- package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
- package/dist/auto-reply/reply/inbound-meta.js +12 -1
- package/dist/auto-reply/reply/mentions.js +18 -11
- package/dist/auto-reply/reply/normalize-reply.js +17 -8
- package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
- package/dist/auto-reply/reply/session.js +102 -21
- package/dist/auto-reply/reply/streaming-directives.js +16 -5
- package/dist/auto-reply/status.js +73 -50
- package/dist/browser/extension-relay.js +3 -3
- package/dist/browser/http-auth.js +1 -1
- package/dist/browser/paths.js +2 -2
- package/dist/build-info.json +3 -3
- package/dist/channels/allowlist-match.js +20 -0
- package/dist/channels/allowlists/resolve-utils.js +65 -2
- package/dist/channels/chat-type.js +8 -4
- package/dist/channels/dock.js +127 -35
- package/dist/channels/draft-stream-loop.js +6 -2
- package/dist/channels/plugins/actions/telegram.js +42 -18
- package/dist/channels/plugins/allowlist-match.js +1 -1
- package/dist/channels/plugins/group-mentions.js +51 -41
- package/dist/channels/plugins/message-action-names.js +2 -0
- package/dist/channels/plugins/message-actions.js +24 -5
- package/dist/channels/plugins/normalize/discord.js +26 -4
- package/dist/channels/plugins/normalize/signal.js +35 -22
- package/dist/channels/plugins/onboarding/helpers.js +8 -26
- package/dist/channels/plugins/outbound/imessage.js +15 -14
- package/dist/channels/registry.js +20 -7
- package/dist/cli/acp-cli.js +7 -5
- package/dist/cli/browser-cli-extension.js +25 -12
- package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
- package/dist/cli/browser-cli-state.js +101 -145
- package/dist/cli/command-options.js +28 -0
- package/dist/cli/completion-cli.js +6 -6
- package/dist/cli/cron-cli/register.cron-add.js +25 -1
- package/dist/cli/cron-cli/register.cron-edit.js +44 -0
- package/dist/cli/cron-cli/shared.js +7 -1
- package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
- package/dist/cli/daemon-cli/lifecycle.js +23 -247
- package/dist/cli/daemon-cli/register-service-commands.js +25 -4
- package/dist/cli/daemon-cli.js +1 -0
- package/dist/cli/devices-cli.js +33 -20
- package/dist/cli/gateway-cli/register.js +37 -105
- package/dist/cli/gateway-cli/run.js +49 -11
- package/dist/cli/nodes-camera.js +59 -4
- package/dist/cli/nodes-cli/register.camera.js +27 -24
- package/dist/cli/nodes-cli/rpc.js +21 -38
- package/dist/cli/qr-cli.js +2 -2
- package/dist/cli/skills-cli.format.js +2 -2
- package/dist/cli/update-cli/progress.js +2 -2
- package/dist/cli/update-cli/restart-helper.js +28 -7
- package/dist/cli/update-cli/shared.js +7 -7
- package/dist/cli/update-cli/status.js +1 -1
- package/dist/cli/update-cli/update-command.js +14 -8
- package/dist/cli/update-cli/wizard.js +2 -2
- package/dist/cli/update-cli.js +21 -1027
- package/dist/commands/auth-choice.apply.anthropic.js +10 -2
- package/dist/commands/channels/add-mutators.js +3 -35
- package/dist/commands/channels/add.js +39 -51
- package/dist/commands/config-validation.js +1 -1
- package/dist/commands/configure.gateway-auth.js +52 -15
- package/dist/commands/configure.gateway.js +84 -40
- package/dist/commands/doctor-completion.js +3 -3
- package/dist/commands/doctor-config-flow.js +536 -16
- package/dist/commands/doctor-gateway-services.js +103 -79
- package/dist/commands/doctor-memory-search.js +9 -9
- package/dist/commands/doctor-platform-notes.js +57 -30
- package/dist/commands/doctor-prompter.js +26 -15
- package/dist/commands/doctor-session-locks.js +1 -1
- package/dist/commands/doctor.js +21 -9
- package/dist/commands/model-picker.js +120 -95
- package/dist/commands/models/set.js +2 -21
- package/dist/commands/models/shared.js +65 -37
- package/dist/commands/onboard-helpers.js +81 -39
- package/dist/commands/openai-codex-oauth.js +1 -1
- package/dist/commands/sessions.js +52 -53
- package/dist/commands/status.summary.js +52 -34
- package/dist/commands/test-wizard-helpers.js +2 -2
- package/dist/config/defaults.js +79 -42
- package/dist/config/group-policy.js +50 -18
- package/dist/config/includes.js +37 -10
- package/dist/config/schema.help.js +5 -4
- package/dist/config/schema.hints.js +2 -2
- package/dist/config/schema.labels.js +1 -0
- package/dist/config/sessions/group.js +12 -11
- package/dist/config/sessions/paths.js +137 -11
- package/dist/config/sessions/store.js +185 -65
- package/dist/config/sessions/types.js +15 -1
- package/dist/config/sessions.js +1 -0
- package/dist/config/telegram-custom-commands.js +3 -2
- package/dist/config/types.js +2 -0
- package/dist/config/zod-schema.agent-defaults.js +6 -27
- package/dist/config/zod-schema.agent-runtime.js +171 -79
- package/dist/config/zod-schema.providers-core.js +138 -65
- package/dist/config/zod-schema.session.js +49 -22
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
- package/dist/cron/isolated-agent/run.js +224 -57
- package/dist/cron/normalize.js +48 -45
- package/dist/cron/run-log.js +14 -0
- package/dist/cron/service/jobs.js +190 -28
- package/dist/cron/service/normalize.js +29 -11
- package/dist/cron/service/store.js +30 -44
- package/dist/cron/service/timer.js +182 -96
- package/dist/cron/service.js +3 -0
- package/dist/cron/stagger.js +37 -0
- package/dist/daemon/inspect.js +132 -92
- package/dist/daemon/runtime-paths.js +25 -4
- package/dist/daemon/service-audit.js +47 -16
- package/dist/discord/accounts.js +23 -20
- package/dist/discord/monitor/agent-components.js +1115 -219
- package/dist/discord/monitor/allow-list.js +114 -34
- package/dist/discord/monitor/listeners.js +204 -97
- package/dist/discord/monitor/message-handler.js +21 -10
- package/dist/discord/monitor/message-handler.preflight.js +195 -101
- package/dist/discord/monitor/message-handler.process.js +384 -123
- package/dist/discord/monitor/message-utils.js +86 -23
- package/dist/discord/monitor/native-command.js +77 -57
- package/dist/discord/monitor/provider.js +122 -117
- package/dist/discord/monitor/reply-context.js +20 -16
- package/dist/discord/monitor/reply-delivery.js +40 -8
- package/dist/discord/monitor/rest-fetch.js +22 -0
- package/dist/discord/monitor/threading.js +117 -24
- package/dist/discord/send.js +2 -1
- package/dist/discord/send.outbound.js +124 -11
- package/dist/discord/send.shared.js +112 -72
- package/dist/discord/voice-message.js +3 -3
- package/dist/gateway/auth.js +119 -44
- package/dist/gateway/call.js +76 -34
- package/dist/gateway/channel-health-monitor.js +57 -50
- package/dist/gateway/client.js +63 -29
- package/dist/gateway/control-ui-contract.js +1 -1
- package/dist/gateway/gateway-config-prompts.shared.js +2 -2
- package/dist/gateway/net.js +109 -1
- package/dist/gateway/protocol/index.js +5 -8
- package/dist/gateway/protocol/schema/agent.js +19 -1
- package/dist/gateway/protocol/schema/channels.js +21 -0
- package/dist/gateway/protocol/schema/cron.js +43 -30
- package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
- package/dist/gateway/protocol/schema/sessions.js +5 -1
- package/dist/gateway/protocol/schema.js +0 -1
- package/dist/gateway/server/presence-events.js +12 -0
- package/dist/gateway/server/ws-connection/message-handler.js +203 -212
- package/dist/gateway/server/ws-connection.js +58 -21
- package/dist/gateway/server-broadcast.js +18 -13
- package/dist/gateway/server-cron.js +177 -10
- package/dist/gateway/server-methods/agent-job.js +131 -38
- package/dist/gateway/server-methods/send.js +60 -14
- package/dist/gateway/server-methods/sessions.js +160 -96
- package/dist/gateway/server-methods/system.js +5 -7
- package/dist/gateway/server-methods-list.js +8 -0
- package/dist/gateway/server-methods.js +24 -8
- package/dist/gateway/server-node-events.js +278 -68
- package/dist/gateway/session-utils.fs.js +316 -75
- package/dist/gateway/session-utils.js +224 -70
- package/dist/gateway/sessions-patch.js +63 -20
- package/dist/gateway/test-temp-config.js +1 -1
- package/dist/gateway/tools-invoke-http.js +118 -70
- package/dist/gateway/ws-log.js +135 -107
- package/dist/hooks/frontmatter.js +36 -82
- package/dist/hooks/install.js +149 -139
- package/dist/hooks/internal-hooks.js +29 -4
- package/dist/hooks/plugin-hooks.js +2 -1
- package/dist/imessage/monitor/deliver.js +10 -4
- package/dist/imessage/monitor/monitor-provider.js +138 -375
- package/dist/imessage/monitor/runtime.js +4 -8
- package/dist/imessage/send.js +65 -19
- package/dist/infra/exec-approvals-allowlist.js +7 -0
- package/dist/infra/exec-approvals.js +35 -920
- package/dist/infra/exec-safe-bin-trust.js +64 -0
- package/dist/infra/heartbeat-runner.js +207 -134
- package/dist/infra/heartbeat-wake.js +183 -22
- package/dist/infra/install-source-utils.js +47 -0
- package/dist/infra/net/ssrf.js +170 -36
- package/dist/infra/outbound/deliver.js +224 -58
- package/dist/infra/outbound/message-action-spec.js +12 -5
- package/dist/infra/outbound/outbound-session.js +27 -25
- package/dist/infra/poolbot-root.js +32 -22
- package/dist/infra/ports.js +14 -11
- package/dist/infra/skills-remote.js +48 -37
- package/dist/infra/system-events.js +25 -11
- package/dist/infra/system-presence.js +26 -33
- package/dist/infra/tmp-poolbot-dir.js +81 -2
- package/dist/infra/wsl.js +37 -1
- package/dist/line/bot-message-context.js +163 -191
- package/dist/logging/subsystem.js +59 -22
- package/dist/markdown/ir.js +124 -50
- package/dist/media/store.js +1 -1
- package/dist/media-understanding/runner.entries.js +42 -25
- package/dist/media-understanding/runner.js +53 -488
- package/dist/memory/embeddings-gemini.js +53 -38
- package/dist/memory/manager-embedding-ops.js +48 -69
- package/dist/pairing/pairing-store.js +178 -119
- package/dist/plugin-sdk/index.js +34 -6
- package/dist/plugins/hooks.js +135 -14
- package/dist/plugins/install.js +190 -152
- package/dist/polls.js +11 -0
- package/dist/routing/resolve-route.js +190 -56
- package/dist/routing/session-key.js +38 -22
- package/dist/runtime.js +35 -9
- package/dist/security/audit-channel.js +1 -1
- package/dist/sessions/session-key-utils.js +29 -11
- package/dist/shared/frontmatter.js +5 -5
- package/dist/shared/node-list-types.js +1 -0
- package/dist/shared/string-normalization.js +15 -0
- package/dist/signal/monitor/event-handler.js +68 -36
- package/dist/signal/send.js +29 -37
- package/dist/slack/monitor/allow-list.js +10 -11
- package/dist/slack/monitor/commands.js +14 -3
- package/dist/slack/monitor/events/interactions.js +4 -4
- package/dist/slack/monitor/media.js +224 -16
- package/dist/slack/monitor/message-handler/dispatch.js +247 -13
- package/dist/slack/monitor/message-handler/prepare.js +128 -45
- package/dist/slack/monitor/slash.js +357 -144
- package/dist/slack/streaming.js +77 -0
- package/dist/telegram/accounts.js +40 -13
- package/dist/telegram/allowed-updates.js +3 -0
- package/dist/telegram/bot/delivery.js +129 -66
- package/dist/telegram/bot/helpers.js +136 -122
- package/dist/telegram/bot-handlers.js +600 -339
- package/dist/telegram/bot-message-context.js +115 -73
- package/dist/telegram/bot-message-dispatch.js +235 -104
- package/dist/telegram/bot-native-command-menu.js +3 -1
- package/dist/telegram/bot-native-commands.js +213 -193
- package/dist/telegram/bot.js +24 -132
- package/dist/telegram/draft-stream.js +84 -75
- package/dist/telegram/format.js +150 -6
- package/dist/telegram/send.js +415 -255
- package/dist/telegram/targets.js +21 -2
- package/dist/telegram/update-offset-store.js +19 -3
- package/dist/terminal/restore.js +5 -2
- package/dist/test-utils/fetch-mock.js +5 -0
- package/dist/version.js +18 -5
- package/dist/web/auto-reply/monitor/broadcast.js +7 -3
- package/dist/web/auto-reply/monitor/on-message.js +6 -3
- package/dist/web/inbound/media.js +34 -8
- package/dist/web/inbound/monitor.js +34 -17
- package/dist/web/inbound/send-api.js +18 -17
- package/dist/web/outbound.js +12 -5
- package/dist/wizard/clack-prompter.js +40 -7
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
- package/skills/apple-reminders/SKILL.md +100 -49
- package/skills/coding-agent/SKILL.md +34 -28
- package/skills/github/SKILL.md +131 -16
- package/skills/imsg/SKILL.md +112 -15
- package/skills/openhue/SKILL.md +101 -19
- package/skills/tmux/SKILL.md +111 -79
- package/skills/weather/SKILL.md +88 -25
|
@@ -1,571 +1,98 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { addAllowlistEntry, evaluateShellAllowlist, maxAsk, minSecurity, requiresExecApproval, resolveSafeBins, recordAllowlistUse, resolveExecApprovals, resolveExecApprovalsFromFile, } from "../infra/exec-approvals.js";
|
|
5
|
-
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
|
|
4
|
+
import { addAllowlistEntry, evaluateShellAllowlist, maxAsk, minSecurity, requiresExecApproval, resolveSafeBins, recordAllowlistUse, resolveExecApprovals, resolveExecApprovalsFromFile, buildSafeShellCommand, buildSafeBinsShellCommand, } from "../infra/exec-approvals.js";
|
|
6
5
|
import { buildNodeShellCommand } from "../infra/node-shell.js";
|
|
7
6
|
import { getShellPathFromLoginShell, resolveShellEnvFallbackTimeoutMs, } from "../infra/shell-env.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
7
|
+
import { logInfo } from "../logger.js";
|
|
8
|
+
import { parseAgentSessionKey, resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
|
9
|
+
import { markBackgrounded, tail } from "./bash-process-registry.js";
|
|
10
|
+
import { DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS, DEFAULT_APPROVAL_TIMEOUT_MS, DEFAULT_MAX_OUTPUT, DEFAULT_NOTIFY_TAIL_CHARS, DEFAULT_PATH, DEFAULT_PENDING_MAX_OUTPUT, applyPathPrepend, applyShellPath, createApprovalSlug, emitExecSystemEvent, normalizeExecAsk, normalizeExecHost, normalizeExecSecurity, normalizeNotifyOutput, normalizePathPrepend, renderExecHostLabel, resolveApprovalRunningNoticeMs, runExecProcess, execSchema, validateHostEnv, } from "./bash-tools.exec-runtime.js";
|
|
11
|
+
import { buildSandboxEnv, clampWithDefault, coerceEnv, readEnvInt, resolveSandboxWorkdir, resolveWorkdir, truncateMiddle, } from "./bash-tools.shared.js";
|
|
13
12
|
import { callGatewayTool } from "./tools/gateway.js";
|
|
14
13
|
import { listNodes, resolveNodeIdFromList } from "./tools/nodes-utils.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// or inject code when running on non-sandboxed hosts (Gateway/Node).
|
|
20
|
-
const DANGEROUS_HOST_ENV_VARS = new Set([
|
|
21
|
-
"LD_PRELOAD",
|
|
22
|
-
"LD_LIBRARY_PATH",
|
|
23
|
-
"LD_AUDIT",
|
|
24
|
-
"DYLD_INSERT_LIBRARIES",
|
|
25
|
-
"DYLD_LIBRARY_PATH",
|
|
26
|
-
"NODE_OPTIONS",
|
|
27
|
-
"NODE_PATH",
|
|
28
|
-
"PYTHONPATH",
|
|
29
|
-
"PYTHONHOME",
|
|
30
|
-
"RUBYLIB",
|
|
31
|
-
"PERL5LIB",
|
|
32
|
-
"BASH_ENV",
|
|
33
|
-
"ENV",
|
|
34
|
-
"GCONV_PATH",
|
|
35
|
-
"IFS",
|
|
36
|
-
"SSLKEYLOGFILE",
|
|
37
|
-
]);
|
|
38
|
-
const DANGEROUS_HOST_ENV_PREFIXES = ["DYLD_", "LD_"];
|
|
39
|
-
// Centralized sanitization helper.
|
|
40
|
-
// Throws an error if dangerous variables or PATH modifications are detected on the host.
|
|
41
|
-
function validateHostEnv(env) {
|
|
42
|
-
for (const key of Object.keys(env)) {
|
|
43
|
-
const upperKey = key.toUpperCase();
|
|
44
|
-
// 1. Block known dangerous variables (Fail Closed)
|
|
45
|
-
if (DANGEROUS_HOST_ENV_PREFIXES.some((prefix) => upperKey.startsWith(prefix))) {
|
|
46
|
-
throw new Error(`Security Violation: Environment variable '${key}' is forbidden during host execution.`);
|
|
47
|
-
}
|
|
48
|
-
if (DANGEROUS_HOST_ENV_VARS.has(upperKey)) {
|
|
49
|
-
throw new Error(`Security Violation: Environment variable '${key}' is forbidden during host execution.`);
|
|
50
|
-
}
|
|
51
|
-
// 2. Strictly block PATH modification on host
|
|
52
|
-
// Allowing custom PATH on the gateway/node can lead to binary hijacking.
|
|
53
|
-
if (upperKey === "PATH") {
|
|
54
|
-
throw new Error("Security Violation: Custom 'PATH' variable is forbidden during host execution.");
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const DEFAULT_MAX_OUTPUT = clampNumber(readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"), 200_000, 1_000, 200_000);
|
|
59
|
-
const DEFAULT_PENDING_MAX_OUTPUT = clampNumber(readEnvInt("POOLBOT_BASH_PENDING_MAX_OUTPUT_CHARS") ??
|
|
60
|
-
readEnvInt("CLAWDBOT_BASH_PENDING_MAX_OUTPUT_CHARS"), 200_000, 1_000, 200_000);
|
|
61
|
-
// Default exec timeout: 1800s (30 min) to accommodate long installs/builds.
|
|
62
|
-
// Users can override via config (`tools.exec.timeoutSec`) or env var.
|
|
63
|
-
const DEFAULT_EXEC_TIMEOUT_SEC = clampNumber(readEnvInt("POOLBOT_EXEC_TIMEOUT_SEC") ?? readEnvInt("CLAWDBOT_EXEC_TIMEOUT_SEC"), 1800, 1, 86_400);
|
|
64
|
-
const DEFAULT_PATH = process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
65
|
-
const DEFAULT_NOTIFY_TAIL_CHARS = 400;
|
|
66
|
-
const DEFAULT_APPROVAL_TIMEOUT_MS = 120_000;
|
|
67
|
-
const DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS = 130_000;
|
|
68
|
-
const DEFAULT_APPROVAL_RUNNING_NOTICE_MS = 10_000;
|
|
69
|
-
const APPROVAL_SLUG_LENGTH = 8;
|
|
70
|
-
const execSchema = Type.Object({
|
|
71
|
-
command: Type.String({ description: "Shell command to execute" }),
|
|
72
|
-
workdir: Type.Optional(Type.String({ description: "Working directory (defaults to cwd)" })),
|
|
73
|
-
env: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
74
|
-
yieldMs: Type.Optional(Type.Number({
|
|
75
|
-
description: "Milliseconds to wait before backgrounding (default 10000)",
|
|
76
|
-
})),
|
|
77
|
-
background: Type.Optional(Type.Boolean({ description: "Run in background immediately" })),
|
|
78
|
-
timeout: Type.Optional(Type.Number({
|
|
79
|
-
description: "Timeout in seconds (default 1800, kills process on expiry).",
|
|
80
|
-
})),
|
|
81
|
-
pty: Type.Optional(Type.Boolean({
|
|
82
|
-
description: "Run in a pseudo-terminal (PTY) when available (TTY-required CLIs, coding agents)",
|
|
83
|
-
})),
|
|
84
|
-
elevated: Type.Optional(Type.Boolean({
|
|
85
|
-
description: "Run on the host with elevated permissions (if allowed)",
|
|
86
|
-
})),
|
|
87
|
-
host: Type.Optional(Type.String({
|
|
88
|
-
description: "Exec host (sandbox|gateway|node).",
|
|
89
|
-
})),
|
|
90
|
-
security: Type.Optional(Type.String({
|
|
91
|
-
description: "Exec security mode (deny|allowlist|full).",
|
|
92
|
-
})),
|
|
93
|
-
ask: Type.Optional(Type.String({
|
|
94
|
-
description: "Exec ask mode (off|on-miss|always).",
|
|
95
|
-
})),
|
|
96
|
-
node: Type.Optional(Type.String({
|
|
97
|
-
description: "Node id/name for host=node.",
|
|
98
|
-
})),
|
|
99
|
-
});
|
|
100
|
-
function normalizeExecHost(value) {
|
|
101
|
-
const normalized = value?.trim().toLowerCase();
|
|
102
|
-
if (normalized === "sandbox" || normalized === "gateway" || normalized === "node") {
|
|
103
|
-
return normalized;
|
|
14
|
+
function extractScriptTargetFromCommand(command) {
|
|
15
|
+
const raw = command.trim();
|
|
16
|
+
if (!raw) {
|
|
17
|
+
return null;
|
|
104
18
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
19
|
+
// Intentionally simple parsing: we only support common forms like
|
|
20
|
+
// python file.py
|
|
21
|
+
// python3 -u file.py
|
|
22
|
+
// node --experimental-something file.js
|
|
23
|
+
// If the command is more complex (pipes, heredocs, quoted paths with spaces), skip preflight.
|
|
24
|
+
const pythonMatch = raw.match(/^\s*(python3?|python)\s+(?:-[^\s]+\s+)*([^\s]+\.py)\b/i);
|
|
25
|
+
if (pythonMatch?.[2]) {
|
|
26
|
+
return { kind: "python", relOrAbsPath: pythonMatch[2] };
|
|
111
27
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const normalized = value?.trim().toLowerCase();
|
|
116
|
-
if (normalized === "off" || normalized === "on-miss" || normalized === "always") {
|
|
117
|
-
return normalized;
|
|
28
|
+
const nodeMatch = raw.match(/^\s*(node)\s+(?:--[^\s]+\s+)*([^\s]+\.js)\b/i);
|
|
29
|
+
if (nodeMatch?.[2]) {
|
|
30
|
+
return { kind: "node", relOrAbsPath: nodeMatch[2] };
|
|
118
31
|
}
|
|
119
32
|
return null;
|
|
120
33
|
}
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return value.replace(/\s+/g, " ").trim();
|
|
126
|
-
}
|
|
127
|
-
function normalizePathPrepend(entries) {
|
|
128
|
-
if (!Array.isArray(entries))
|
|
129
|
-
return [];
|
|
130
|
-
const seen = new Set();
|
|
131
|
-
const normalized = [];
|
|
132
|
-
for (const entry of entries) {
|
|
133
|
-
if (typeof entry !== "string")
|
|
134
|
-
continue;
|
|
135
|
-
const trimmed = entry.trim();
|
|
136
|
-
if (!trimmed || seen.has(trimmed))
|
|
137
|
-
continue;
|
|
138
|
-
seen.add(trimmed);
|
|
139
|
-
normalized.push(trimmed);
|
|
34
|
+
async function validateScriptFileForShellBleed(params) {
|
|
35
|
+
const target = extractScriptTargetFromCommand(params.command);
|
|
36
|
+
if (!target) {
|
|
37
|
+
return;
|
|
140
38
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
.map((part) => part.trim())
|
|
149
|
-
.filter(Boolean);
|
|
150
|
-
const merged = [];
|
|
151
|
-
const seen = new Set();
|
|
152
|
-
for (const part of [...prepend, ...partsExisting]) {
|
|
153
|
-
if (seen.has(part))
|
|
154
|
-
continue;
|
|
155
|
-
seen.add(part);
|
|
156
|
-
merged.push(part);
|
|
39
|
+
const absPath = path.isAbsolute(target.relOrAbsPath)
|
|
40
|
+
? path.resolve(target.relOrAbsPath)
|
|
41
|
+
: path.resolve(params.workdir, target.relOrAbsPath);
|
|
42
|
+
// Best-effort: only validate if file exists and is reasonably small.
|
|
43
|
+
let stat;
|
|
44
|
+
try {
|
|
45
|
+
stat = await fs.stat(absPath);
|
|
157
46
|
}
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
function applyPathPrepend(env, prepend, options) {
|
|
161
|
-
if (prepend.length === 0)
|
|
162
|
-
return;
|
|
163
|
-
if (options?.requireExisting && !env.PATH)
|
|
164
|
-
return;
|
|
165
|
-
const merged = mergePathPrepend(env.PATH, prepend);
|
|
166
|
-
if (merged)
|
|
167
|
-
env.PATH = merged;
|
|
168
|
-
}
|
|
169
|
-
function applyShellPath(env, shellPath) {
|
|
170
|
-
if (!shellPath)
|
|
171
|
-
return;
|
|
172
|
-
const entries = shellPath
|
|
173
|
-
.split(path.delimiter)
|
|
174
|
-
.map((part) => part.trim())
|
|
175
|
-
.filter(Boolean);
|
|
176
|
-
if (entries.length === 0)
|
|
177
|
-
return;
|
|
178
|
-
const merged = mergePathPrepend(env.PATH, entries);
|
|
179
|
-
if (merged)
|
|
180
|
-
env.PATH = merged;
|
|
181
|
-
}
|
|
182
|
-
function maybeNotifyOnExit(session, status) {
|
|
183
|
-
if (!session.backgrounded || !session.notifyOnExit || session.exitNotified)
|
|
184
|
-
return;
|
|
185
|
-
const sessionKey = session.sessionKey?.trim();
|
|
186
|
-
if (!sessionKey)
|
|
47
|
+
catch {
|
|
187
48
|
return;
|
|
188
|
-
session.exitNotified = true;
|
|
189
|
-
const exitLabel = session.exitSignal
|
|
190
|
-
? `signal ${session.exitSignal}`
|
|
191
|
-
: `code ${session.exitCode ?? 0}`;
|
|
192
|
-
const output = normalizeNotifyOutput(tail(session.tail || session.aggregated || "", DEFAULT_NOTIFY_TAIL_CHARS));
|
|
193
|
-
const summary = output
|
|
194
|
-
? `Exec ${status} (${session.id.slice(0, 8)}, ${exitLabel}) :: ${output}`
|
|
195
|
-
: `Exec ${status} (${session.id.slice(0, 8)}, ${exitLabel})`;
|
|
196
|
-
enqueueSystemEvent(summary, { sessionKey });
|
|
197
|
-
requestHeartbeatNow({ reason: `exec:${session.id}:exit` });
|
|
198
|
-
}
|
|
199
|
-
function createApprovalSlug(id) {
|
|
200
|
-
return id.slice(0, APPROVAL_SLUG_LENGTH);
|
|
201
|
-
}
|
|
202
|
-
function resolveApprovalRunningNoticeMs(value) {
|
|
203
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
204
|
-
return DEFAULT_APPROVAL_RUNNING_NOTICE_MS;
|
|
205
49
|
}
|
|
206
|
-
if (
|
|
207
|
-
return 0;
|
|
208
|
-
return Math.floor(value);
|
|
209
|
-
}
|
|
210
|
-
function emitExecSystemEvent(text, opts) {
|
|
211
|
-
const sessionKey = opts.sessionKey?.trim();
|
|
212
|
-
if (!sessionKey)
|
|
50
|
+
if (!stat.isFile()) {
|
|
213
51
|
return;
|
|
214
|
-
enqueueSystemEvent(text, { sessionKey, contextKey: opts.contextKey });
|
|
215
|
-
requestHeartbeatNow({ reason: "exec-event" });
|
|
216
|
-
}
|
|
217
|
-
async function runExecProcess(opts) {
|
|
218
|
-
const startedAt = Date.now();
|
|
219
|
-
const sessionId = createSessionSlug();
|
|
220
|
-
let child = null;
|
|
221
|
-
let pty = null;
|
|
222
|
-
let stdin;
|
|
223
|
-
if (opts.sandbox) {
|
|
224
|
-
const { child: spawned } = await spawnWithFallback({
|
|
225
|
-
argv: [
|
|
226
|
-
"docker",
|
|
227
|
-
...buildDockerExecArgs({
|
|
228
|
-
containerName: opts.sandbox.containerName,
|
|
229
|
-
command: opts.command,
|
|
230
|
-
workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
|
|
231
|
-
env: opts.env,
|
|
232
|
-
tty: opts.usePty,
|
|
233
|
-
}),
|
|
234
|
-
],
|
|
235
|
-
options: {
|
|
236
|
-
cwd: opts.workdir,
|
|
237
|
-
env: process.env,
|
|
238
|
-
detached: process.platform !== "win32",
|
|
239
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
240
|
-
windowsHide: true,
|
|
241
|
-
},
|
|
242
|
-
fallbacks: [
|
|
243
|
-
{
|
|
244
|
-
label: "no-detach",
|
|
245
|
-
options: { detached: false },
|
|
246
|
-
},
|
|
247
|
-
],
|
|
248
|
-
onFallback: (err, fallback) => {
|
|
249
|
-
const errText = formatSpawnError(err);
|
|
250
|
-
const warning = `Warning: spawn failed (${errText}); retrying with ${fallback.label}.`;
|
|
251
|
-
logWarn(`exec: spawn failed (${errText}); retrying with ${fallback.label}.`);
|
|
252
|
-
opts.warnings.push(warning);
|
|
253
|
-
},
|
|
254
|
-
});
|
|
255
|
-
child = spawned;
|
|
256
|
-
stdin = child.stdin;
|
|
257
52
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
const ptyModule = (await import("@lydell/node-pty"));
|
|
262
|
-
const spawnPty = ptyModule.spawn ?? ptyModule.default?.spawn;
|
|
263
|
-
if (!spawnPty) {
|
|
264
|
-
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
|
265
|
-
}
|
|
266
|
-
pty = spawnPty(shell, [...shellArgs, opts.command], {
|
|
267
|
-
cwd: opts.workdir,
|
|
268
|
-
env: opts.env,
|
|
269
|
-
name: process.env.TERM ?? "xterm-256color",
|
|
270
|
-
cols: 120,
|
|
271
|
-
rows: 30,
|
|
272
|
-
});
|
|
273
|
-
stdin = {
|
|
274
|
-
destroyed: false,
|
|
275
|
-
write: (data, cb) => {
|
|
276
|
-
try {
|
|
277
|
-
pty?.write(data);
|
|
278
|
-
cb?.(null);
|
|
279
|
-
}
|
|
280
|
-
catch (err) {
|
|
281
|
-
cb?.(err);
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
end: () => {
|
|
285
|
-
try {
|
|
286
|
-
const eof = process.platform === "win32" ? "\x1a" : "\x04";
|
|
287
|
-
pty?.write(eof);
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// ignore EOF errors
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
catch (err) {
|
|
296
|
-
const errText = String(err);
|
|
297
|
-
const warning = `Warning: PTY spawn failed (${errText}); retrying without PTY for \`${opts.command}\`.`;
|
|
298
|
-
logWarn(`exec: PTY spawn failed (${errText}); retrying without PTY for "${opts.command}".`);
|
|
299
|
-
opts.warnings.push(warning);
|
|
300
|
-
const { child: spawned } = await spawnWithFallback({
|
|
301
|
-
argv: [shell, ...shellArgs, opts.command],
|
|
302
|
-
options: {
|
|
303
|
-
cwd: opts.workdir,
|
|
304
|
-
env: opts.env,
|
|
305
|
-
detached: process.platform !== "win32",
|
|
306
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
307
|
-
windowsHide: true,
|
|
308
|
-
},
|
|
309
|
-
fallbacks: [
|
|
310
|
-
{
|
|
311
|
-
label: "no-detach",
|
|
312
|
-
options: { detached: false },
|
|
313
|
-
},
|
|
314
|
-
],
|
|
315
|
-
onFallback: (fallbackErr, fallback) => {
|
|
316
|
-
const fallbackText = formatSpawnError(fallbackErr);
|
|
317
|
-
const fallbackWarning = `Warning: spawn failed (${fallbackText}); retrying with ${fallback.label}.`;
|
|
318
|
-
logWarn(`exec: spawn failed (${fallbackText}); retrying with ${fallback.label}.`);
|
|
319
|
-
opts.warnings.push(fallbackWarning);
|
|
320
|
-
},
|
|
321
|
-
});
|
|
322
|
-
child = spawned;
|
|
323
|
-
stdin = child.stdin;
|
|
324
|
-
}
|
|
53
|
+
if (stat.size > 512 * 1024) {
|
|
54
|
+
return;
|
|
325
55
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
onFallback: (err, fallback) => {
|
|
344
|
-
const errText = formatSpawnError(err);
|
|
345
|
-
const warning = `Warning: spawn failed (${errText}); retrying with ${fallback.label}.`;
|
|
346
|
-
logWarn(`exec: spawn failed (${errText}); retrying with ${fallback.label}.`);
|
|
347
|
-
opts.warnings.push(warning);
|
|
348
|
-
},
|
|
349
|
-
});
|
|
350
|
-
child = spawned;
|
|
351
|
-
stdin = child.stdin;
|
|
56
|
+
const content = await fs.readFile(absPath, "utf-8");
|
|
57
|
+
// Common failure mode: shell env var syntax leaking into Python/JS.
|
|
58
|
+
// We deliberately match all-caps/underscore vars to avoid false positives with `$` as a JS identifier.
|
|
59
|
+
const envVarRegex = /\$[A-Z_][A-Z0-9_]{1,}/g;
|
|
60
|
+
const first = envVarRegex.exec(content);
|
|
61
|
+
if (first) {
|
|
62
|
+
const idx = first.index;
|
|
63
|
+
const before = content.slice(0, idx);
|
|
64
|
+
const line = before.split("\n").length;
|
|
65
|
+
const token = first[0];
|
|
66
|
+
throw new Error([
|
|
67
|
+
`exec preflight: detected likely shell variable injection (${token}) in ${target.kind} script: ${path.basename(absPath)}:${line}.`,
|
|
68
|
+
target.kind === "python"
|
|
69
|
+
? `In Python, use os.environ.get(${JSON.stringify(token.slice(1))}) instead of raw ${token}.`
|
|
70
|
+
: `In Node.js, use process.env[${JSON.stringify(token.slice(1))}] instead of raw ${token}.`,
|
|
71
|
+
"(If this is inside a string literal on purpose, escape it or restructure the code.)",
|
|
72
|
+
].join("\n"));
|
|
352
73
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
pid: child?.pid ?? pty?.pid,
|
|
363
|
-
startedAt,
|
|
364
|
-
cwd: opts.workdir,
|
|
365
|
-
maxOutputChars: opts.maxOutput,
|
|
366
|
-
pendingMaxOutputChars: opts.pendingMaxOutput,
|
|
367
|
-
totalOutputChars: 0,
|
|
368
|
-
pendingStdout: [],
|
|
369
|
-
pendingStderr: [],
|
|
370
|
-
pendingStdoutChars: 0,
|
|
371
|
-
pendingStderrChars: 0,
|
|
372
|
-
aggregated: "",
|
|
373
|
-
tail: "",
|
|
374
|
-
exited: false,
|
|
375
|
-
exitCode: undefined,
|
|
376
|
-
exitSignal: undefined,
|
|
377
|
-
truncated: false,
|
|
378
|
-
backgrounded: false,
|
|
379
|
-
};
|
|
380
|
-
addSession(session);
|
|
381
|
-
let settled = false;
|
|
382
|
-
let timeoutTimer = null;
|
|
383
|
-
let timeoutFinalizeTimer = null;
|
|
384
|
-
let timedOut = false;
|
|
385
|
-
const timeoutFinalizeMs = 1000;
|
|
386
|
-
let resolveFn = null;
|
|
387
|
-
const settle = (outcome) => {
|
|
388
|
-
if (settled)
|
|
389
|
-
return;
|
|
390
|
-
settled = true;
|
|
391
|
-
resolveFn?.(outcome);
|
|
392
|
-
};
|
|
393
|
-
const finalizeTimeout = () => {
|
|
394
|
-
if (session.exited)
|
|
395
|
-
return;
|
|
396
|
-
markExited(session, null, "SIGKILL", "failed");
|
|
397
|
-
maybeNotifyOnExit(session, "failed");
|
|
398
|
-
const aggregated = session.aggregated.trim();
|
|
399
|
-
const reason = `Command timed out after ${opts.timeoutSec} seconds`;
|
|
400
|
-
settle({
|
|
401
|
-
status: "failed",
|
|
402
|
-
exitCode: null,
|
|
403
|
-
exitSignal: "SIGKILL",
|
|
404
|
-
durationMs: Date.now() - startedAt,
|
|
405
|
-
aggregated,
|
|
406
|
-
timedOut: true,
|
|
407
|
-
reason: aggregated ? `${aggregated}\n\n${reason}` : reason,
|
|
408
|
-
});
|
|
409
|
-
};
|
|
410
|
-
const onTimeout = () => {
|
|
411
|
-
timedOut = true;
|
|
412
|
-
killSession(session);
|
|
413
|
-
if (!timeoutFinalizeTimer) {
|
|
414
|
-
timeoutFinalizeTimer = setTimeout(() => {
|
|
415
|
-
finalizeTimeout();
|
|
416
|
-
}, timeoutFinalizeMs);
|
|
74
|
+
// Another recurring pattern from the issue: shell commands accidentally emitted as JS.
|
|
75
|
+
if (target.kind === "node") {
|
|
76
|
+
const firstNonEmpty = content
|
|
77
|
+
.split(/\r?\n/)
|
|
78
|
+
.map((l) => l.trim())
|
|
79
|
+
.find((l) => l.length > 0);
|
|
80
|
+
if (firstNonEmpty && /^NODE\b/.test(firstNonEmpty)) {
|
|
81
|
+
throw new Error(`exec preflight: JS file starts with shell syntax (${firstNonEmpty}). ` +
|
|
82
|
+
`This looks like a shell command, not JavaScript.`);
|
|
417
83
|
}
|
|
418
|
-
};
|
|
419
|
-
if (opts.timeoutSec > 0) {
|
|
420
|
-
timeoutTimer = setTimeout(() => {
|
|
421
|
-
onTimeout();
|
|
422
|
-
}, opts.timeoutSec * 1000);
|
|
423
84
|
}
|
|
424
|
-
const emitUpdate = () => {
|
|
425
|
-
if (!opts.onUpdate)
|
|
426
|
-
return;
|
|
427
|
-
const tailText = session.tail || session.aggregated;
|
|
428
|
-
const warningText = opts.warnings.length ? `${opts.warnings.join("\n")}\n\n` : "";
|
|
429
|
-
opts.onUpdate({
|
|
430
|
-
content: [{ type: "text", text: warningText + (tailText || "") }],
|
|
431
|
-
details: {
|
|
432
|
-
status: "running",
|
|
433
|
-
sessionId,
|
|
434
|
-
pid: session.pid ?? undefined,
|
|
435
|
-
startedAt,
|
|
436
|
-
cwd: session.cwd,
|
|
437
|
-
tail: session.tail,
|
|
438
|
-
},
|
|
439
|
-
});
|
|
440
|
-
};
|
|
441
|
-
const handleStdout = (data) => {
|
|
442
|
-
const str = sanitizeBinaryOutput(data.toString());
|
|
443
|
-
for (const chunk of chunkString(str)) {
|
|
444
|
-
appendOutput(session, "stdout", chunk);
|
|
445
|
-
emitUpdate();
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
const handleStderr = (data) => {
|
|
449
|
-
const str = sanitizeBinaryOutput(data.toString());
|
|
450
|
-
for (const chunk of chunkString(str)) {
|
|
451
|
-
appendOutput(session, "stderr", chunk);
|
|
452
|
-
emitUpdate();
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
if (pty) {
|
|
456
|
-
const cursorResponse = buildCursorPositionResponse();
|
|
457
|
-
pty.onData((data) => {
|
|
458
|
-
const raw = data.toString();
|
|
459
|
-
const { cleaned, requests } = stripDsrRequests(raw);
|
|
460
|
-
if (requests > 0) {
|
|
461
|
-
for (let i = 0; i < requests; i += 1) {
|
|
462
|
-
pty.write(cursorResponse);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
handleStdout(cleaned);
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
else if (child) {
|
|
469
|
-
child.stdout.on("data", handleStdout);
|
|
470
|
-
child.stderr.on("data", handleStderr);
|
|
471
|
-
}
|
|
472
|
-
const promise = new Promise((resolve) => {
|
|
473
|
-
resolveFn = resolve;
|
|
474
|
-
const handleExit = (code, exitSignal) => {
|
|
475
|
-
if (timeoutTimer)
|
|
476
|
-
clearTimeout(timeoutTimer);
|
|
477
|
-
if (timeoutFinalizeTimer)
|
|
478
|
-
clearTimeout(timeoutFinalizeTimer);
|
|
479
|
-
const durationMs = Date.now() - startedAt;
|
|
480
|
-
const wasSignal = exitSignal != null;
|
|
481
|
-
const isSuccess = code === 0 && !wasSignal && !timedOut;
|
|
482
|
-
const status = isSuccess ? "completed" : "failed";
|
|
483
|
-
markExited(session, code, exitSignal, status);
|
|
484
|
-
maybeNotifyOnExit(session, status);
|
|
485
|
-
if (!session.child && session.stdin) {
|
|
486
|
-
session.stdin.destroyed = true;
|
|
487
|
-
}
|
|
488
|
-
if (settled)
|
|
489
|
-
return;
|
|
490
|
-
const aggregated = session.aggregated.trim();
|
|
491
|
-
if (!isSuccess) {
|
|
492
|
-
const reason = timedOut
|
|
493
|
-
? `Command timed out after ${opts.timeoutSec} seconds`
|
|
494
|
-
: wasSignal && exitSignal
|
|
495
|
-
? `Command aborted by signal ${exitSignal}`
|
|
496
|
-
: code === null
|
|
497
|
-
? "Command aborted before exit code was captured"
|
|
498
|
-
: `Command exited with code ${code}`;
|
|
499
|
-
const message = aggregated ? `${aggregated}\n\n${reason}` : reason;
|
|
500
|
-
settle({
|
|
501
|
-
status: "failed",
|
|
502
|
-
exitCode: code ?? null,
|
|
503
|
-
exitSignal: exitSignal ?? null,
|
|
504
|
-
durationMs,
|
|
505
|
-
aggregated,
|
|
506
|
-
timedOut,
|
|
507
|
-
reason: message,
|
|
508
|
-
});
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
settle({
|
|
512
|
-
status: "completed",
|
|
513
|
-
exitCode: code ?? 0,
|
|
514
|
-
exitSignal: exitSignal ?? null,
|
|
515
|
-
durationMs,
|
|
516
|
-
aggregated,
|
|
517
|
-
timedOut: false,
|
|
518
|
-
});
|
|
519
|
-
};
|
|
520
|
-
if (pty) {
|
|
521
|
-
pty.onExit((event) => {
|
|
522
|
-
const rawSignal = event.signal ?? null;
|
|
523
|
-
const normalizedSignal = rawSignal === 0 ? null : rawSignal;
|
|
524
|
-
handleExit(event.exitCode ?? null, normalizedSignal);
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
else if (child) {
|
|
528
|
-
child.once("close", (code, exitSignal) => {
|
|
529
|
-
handleExit(code, exitSignal);
|
|
530
|
-
});
|
|
531
|
-
child.once("error", (err) => {
|
|
532
|
-
if (timeoutTimer)
|
|
533
|
-
clearTimeout(timeoutTimer);
|
|
534
|
-
if (timeoutFinalizeTimer)
|
|
535
|
-
clearTimeout(timeoutFinalizeTimer);
|
|
536
|
-
markExited(session, null, null, "failed");
|
|
537
|
-
maybeNotifyOnExit(session, "failed");
|
|
538
|
-
const aggregated = session.aggregated.trim();
|
|
539
|
-
const message = aggregated ? `${aggregated}\n\n${String(err)}` : String(err);
|
|
540
|
-
settle({
|
|
541
|
-
status: "failed",
|
|
542
|
-
exitCode: null,
|
|
543
|
-
exitSignal: null,
|
|
544
|
-
durationMs: Date.now() - startedAt,
|
|
545
|
-
aggregated,
|
|
546
|
-
timedOut,
|
|
547
|
-
reason: message,
|
|
548
|
-
});
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
return {
|
|
553
|
-
session,
|
|
554
|
-
startedAt,
|
|
555
|
-
pid: session.pid ?? undefined,
|
|
556
|
-
promise,
|
|
557
|
-
kill: () => killSession(session),
|
|
558
|
-
};
|
|
559
85
|
}
|
|
560
86
|
export function createExecTool(defaults) {
|
|
561
|
-
const defaultBackgroundMs =
|
|
87
|
+
const defaultBackgroundMs = clampWithDefault(defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), 10_000, 10, 120_000);
|
|
562
88
|
const allowBackground = defaults?.allowBackground ?? true;
|
|
563
89
|
const defaultTimeoutSec = typeof defaults?.timeoutSec === "number" && defaults.timeoutSec > 0
|
|
564
90
|
? defaults.timeoutSec
|
|
565
|
-
:
|
|
91
|
+
: 1800;
|
|
566
92
|
const defaultPathPrepend = normalizePathPrepend(defaults?.pathPrepend);
|
|
567
93
|
const safeBins = resolveSafeBins(defaults?.safeBins);
|
|
568
94
|
const notifyOnExit = defaults?.notifyOnExit !== false;
|
|
95
|
+
const notifyOnExitEmptySuccess = defaults?.notifyOnExitEmptySuccess === true;
|
|
569
96
|
const notifySessionKey = defaults?.sessionKey?.trim() || undefined;
|
|
570
97
|
const approvalRunningNoticeMs = resolveApprovalRunningNoticeMs(defaults?.approvalRunningNoticeMs);
|
|
571
98
|
// Derive agentId only when sessionKey is an agent session key.
|
|
@@ -585,6 +112,7 @@ export function createExecTool(defaults) {
|
|
|
585
112
|
const maxOutput = DEFAULT_MAX_OUTPUT;
|
|
586
113
|
const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT;
|
|
587
114
|
const warnings = [];
|
|
115
|
+
let execCommandOverride;
|
|
588
116
|
const backgroundRequested = params.background === true;
|
|
589
117
|
const yieldRequested = typeof params.yieldMs === "number";
|
|
590
118
|
if (!allowBackground && (backgroundRequested || yieldRequested)) {
|
|
@@ -593,7 +121,7 @@ export function createExecTool(defaults) {
|
|
|
593
121
|
const yieldWindow = allowBackground
|
|
594
122
|
? backgroundRequested
|
|
595
123
|
? 0
|
|
596
|
-
:
|
|
124
|
+
: clampWithDefault(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000)
|
|
597
125
|
: null;
|
|
598
126
|
const elevatedDefaults = defaults?.elevated;
|
|
599
127
|
const elevatedAllowed = Boolean(elevatedDefaults?.enabled && elevatedDefaults.allowed);
|
|
@@ -620,10 +148,12 @@ export function createExecTool(defaults) {
|
|
|
620
148
|
const contextParts = [];
|
|
621
149
|
const provider = defaults?.messageProvider?.trim();
|
|
622
150
|
const sessionKey = defaults?.sessionKey?.trim();
|
|
623
|
-
if (provider)
|
|
151
|
+
if (provider) {
|
|
624
152
|
contextParts.push(`provider=${provider}`);
|
|
625
|
-
|
|
153
|
+
}
|
|
154
|
+
if (sessionKey) {
|
|
626
155
|
contextParts.push(`session=${sessionKey}`);
|
|
156
|
+
}
|
|
627
157
|
if (!elevatedDefaults?.enabled) {
|
|
628
158
|
gates.push("enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)");
|
|
629
159
|
}
|
|
@@ -708,7 +238,14 @@ export function createExecTool(defaults) {
|
|
|
708
238
|
});
|
|
709
239
|
applyShellPath(env, shellPath);
|
|
710
240
|
}
|
|
711
|
-
|
|
241
|
+
// `tools.exec.pathPrepend` is only meaningful when exec runs locally (gateway) or in the sandbox.
|
|
242
|
+
// Node hosts intentionally ignore request-scoped PATH overrides, so don't pretend this applies.
|
|
243
|
+
if (host === "node" && defaultPathPrepend.length > 0) {
|
|
244
|
+
warnings.push("Warning: tools.exec.pathPrepend is ignored for host=node. Configure PATH on the node host/service instead.");
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
applyPathPrepend(env, defaultPathPrepend);
|
|
248
|
+
}
|
|
712
249
|
if (host === "node") {
|
|
713
250
|
const approvals = resolveExecApprovals(agentId, { security, ask });
|
|
714
251
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
|
@@ -746,9 +283,6 @@ export function createExecTool(defaults) {
|
|
|
746
283
|
}
|
|
747
284
|
const argv = buildNodeShellCommand(params.command, nodeInfo?.platform);
|
|
748
285
|
const nodeEnv = params.env ? { ...params.env } : undefined;
|
|
749
|
-
if (nodeEnv) {
|
|
750
|
-
applyPathPrepend(nodeEnv, defaultPathPrepend, { requireExisting: true });
|
|
751
|
-
}
|
|
752
286
|
const baseAllowlistEval = evaluateShellAllowlist({
|
|
753
287
|
command: params.command,
|
|
754
288
|
allowlist: [],
|
|
@@ -887,8 +421,9 @@ export function createExecTool(defaults) {
|
|
|
887
421
|
emitExecSystemEvent(`Exec denied (node=${nodeId} id=${approvalId}, invoke-failed): ${commandText}`, { sessionKey: notifySessionKey, contextKey });
|
|
888
422
|
}
|
|
889
423
|
finally {
|
|
890
|
-
if (runningTimer)
|
|
424
|
+
if (runningTimer) {
|
|
891
425
|
clearTimeout(runningTimer);
|
|
426
|
+
}
|
|
892
427
|
}
|
|
893
428
|
})();
|
|
894
429
|
return {
|
|
@@ -1042,8 +577,9 @@ export function createExecTool(defaults) {
|
|
|
1042
577
|
if (allowlistMatches.length > 0) {
|
|
1043
578
|
const seen = new Set();
|
|
1044
579
|
for (const match of allowlistMatches) {
|
|
1045
|
-
if (seen.has(match.pattern))
|
|
580
|
+
if (seen.has(match.pattern)) {
|
|
1046
581
|
continue;
|
|
582
|
+
}
|
|
1047
583
|
seen.add(match.pattern);
|
|
1048
584
|
recordAllowlistUse(approvals.file, agentId, match, commandText, resolvedPath ?? undefined);
|
|
1049
585
|
}
|
|
@@ -1061,6 +597,7 @@ export function createExecTool(defaults) {
|
|
|
1061
597
|
maxOutput,
|
|
1062
598
|
pendingMaxOutput,
|
|
1063
599
|
notifyOnExit: false,
|
|
600
|
+
notifyOnExitEmptySuccess: false,
|
|
1064
601
|
scopeKey: defaults?.scopeKey,
|
|
1065
602
|
sessionKey: notifySessionKey,
|
|
1066
603
|
timeoutSec: effectiveTimeout,
|
|
@@ -1078,8 +615,9 @@ export function createExecTool(defaults) {
|
|
|
1078
615
|
}, approvalRunningNoticeMs);
|
|
1079
616
|
}
|
|
1080
617
|
const outcome = await run.promise;
|
|
1081
|
-
if (runningTimer)
|
|
618
|
+
if (runningTimer) {
|
|
1082
619
|
clearTimeout(runningTimer);
|
|
620
|
+
}
|
|
1083
621
|
const output = normalizeNotifyOutput(tail(outcome.aggregated || "", DEFAULT_NOTIFY_TAIL_CHARS));
|
|
1084
622
|
const exitLabel = outcome.timedOut ? "timeout" : `code ${outcome.exitCode ?? "?"}`;
|
|
1085
623
|
const summary = output
|
|
@@ -1109,11 +647,41 @@ export function createExecTool(defaults) {
|
|
|
1109
647
|
if (hostSecurity === "allowlist" && (!analysisOk || !allowlistSatisfied)) {
|
|
1110
648
|
throw new Error("exec denied: allowlist miss");
|
|
1111
649
|
}
|
|
650
|
+
// If allowlist uses safeBins, sanitize only those stdin-only segments:
|
|
651
|
+
// disable glob/var expansion by forcing argv tokens to be literal via single-quoting.
|
|
652
|
+
if (hostSecurity === "allowlist" &&
|
|
653
|
+
analysisOk &&
|
|
654
|
+
allowlistSatisfied &&
|
|
655
|
+
allowlistEval.segmentSatisfiedBy.some((by) => by === "safeBins")) {
|
|
656
|
+
const safe = buildSafeBinsShellCommand({
|
|
657
|
+
command: params.command,
|
|
658
|
+
segments: allowlistEval.segments,
|
|
659
|
+
segmentSatisfiedBy: allowlistEval.segmentSatisfiedBy,
|
|
660
|
+
platform: process.platform,
|
|
661
|
+
});
|
|
662
|
+
if (!safe.ok || !safe.command) {
|
|
663
|
+
// Fallback: quote everything (safe, but may change glob behavior).
|
|
664
|
+
const fallback = buildSafeShellCommand({
|
|
665
|
+
command: params.command,
|
|
666
|
+
platform: process.platform,
|
|
667
|
+
});
|
|
668
|
+
if (!fallback.ok || !fallback.command) {
|
|
669
|
+
throw new Error(`exec denied: safeBins sanitize failed (${safe.reason ?? "unknown"})`);
|
|
670
|
+
}
|
|
671
|
+
warnings.push("Warning: safeBins hardening used fallback quoting due to parser mismatch.");
|
|
672
|
+
execCommandOverride = fallback.command;
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
warnings.push("Warning: safeBins hardening disabled glob/variable expansion for stdin-only segments.");
|
|
676
|
+
execCommandOverride = safe.command;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
1112
679
|
if (allowlistMatches.length > 0) {
|
|
1113
680
|
const seen = new Set();
|
|
1114
681
|
for (const match of allowlistMatches) {
|
|
1115
|
-
if (seen.has(match.pattern))
|
|
682
|
+
if (seen.has(match.pattern)) {
|
|
1116
683
|
continue;
|
|
684
|
+
}
|
|
1117
685
|
seen.add(match.pattern);
|
|
1118
686
|
recordAllowlistUse(approvals.file, agentId, match, params.command, allowlistEval.segments[0]?.resolution?.resolvedPath);
|
|
1119
687
|
}
|
|
@@ -1122,8 +690,12 @@ export function createExecTool(defaults) {
|
|
|
1122
690
|
const effectiveTimeout = typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec;
|
|
1123
691
|
const getWarningText = () => (warnings.length ? `${warnings.join("\n")}\n\n` : "");
|
|
1124
692
|
const usePty = params.pty === true && !sandbox;
|
|
693
|
+
// Preflight: catch a common model failure mode (shell syntax leaking into Python/JS sources)
|
|
694
|
+
// before we execute and burn tokens in cron loops.
|
|
695
|
+
await validateScriptFileForShellBleed({ command: params.command, workdir });
|
|
1125
696
|
const run = await runExecProcess({
|
|
1126
697
|
command: params.command,
|
|
698
|
+
execCommand: execCommandOverride,
|
|
1127
699
|
workdir,
|
|
1128
700
|
env,
|
|
1129
701
|
sandbox,
|
|
@@ -1133,6 +705,7 @@ export function createExecTool(defaults) {
|
|
|
1133
705
|
maxOutput,
|
|
1134
706
|
pendingMaxOutput,
|
|
1135
707
|
notifyOnExit,
|
|
708
|
+
notifyOnExitEmptySuccess,
|
|
1136
709
|
scopeKey: defaults?.scopeKey,
|
|
1137
710
|
sessionKey: notifySessionKey,
|
|
1138
711
|
timeoutSec: effectiveTimeout,
|
|
@@ -1142,12 +715,14 @@ export function createExecTool(defaults) {
|
|
|
1142
715
|
let yieldTimer = null;
|
|
1143
716
|
// Tool-call abort should not kill backgrounded sessions; timeouts still must.
|
|
1144
717
|
const onAbortSignal = () => {
|
|
1145
|
-
if (yielded || run.session.backgrounded)
|
|
718
|
+
if (yielded || run.session.backgrounded) {
|
|
1146
719
|
return;
|
|
720
|
+
}
|
|
1147
721
|
run.kill();
|
|
1148
722
|
};
|
|
1149
|
-
if (signal?.aborted)
|
|
723
|
+
if (signal?.aborted) {
|
|
1150
724
|
onAbortSignal();
|
|
725
|
+
}
|
|
1151
726
|
else if (signal) {
|
|
1152
727
|
signal.addEventListener("abort", onAbortSignal, { once: true });
|
|
1153
728
|
}
|
|
@@ -1169,10 +744,12 @@ export function createExecTool(defaults) {
|
|
|
1169
744
|
},
|
|
1170
745
|
});
|
|
1171
746
|
const onYieldNow = () => {
|
|
1172
|
-
if (yieldTimer)
|
|
747
|
+
if (yieldTimer) {
|
|
1173
748
|
clearTimeout(yieldTimer);
|
|
1174
|
-
|
|
749
|
+
}
|
|
750
|
+
if (yielded) {
|
|
1175
751
|
return;
|
|
752
|
+
}
|
|
1176
753
|
yielded = true;
|
|
1177
754
|
markBackgrounded(run.session);
|
|
1178
755
|
resolveRunning();
|
|
@@ -1183,8 +760,9 @@ export function createExecTool(defaults) {
|
|
|
1183
760
|
}
|
|
1184
761
|
else {
|
|
1185
762
|
yieldTimer = setTimeout(() => {
|
|
1186
|
-
if (yielded)
|
|
763
|
+
if (yielded) {
|
|
1187
764
|
return;
|
|
765
|
+
}
|
|
1188
766
|
yielded = true;
|
|
1189
767
|
markBackgrounded(run.session);
|
|
1190
768
|
resolveRunning();
|
|
@@ -1193,10 +771,12 @@ export function createExecTool(defaults) {
|
|
|
1193
771
|
}
|
|
1194
772
|
run.promise
|
|
1195
773
|
.then((outcome) => {
|
|
1196
|
-
if (yieldTimer)
|
|
774
|
+
if (yieldTimer) {
|
|
1197
775
|
clearTimeout(yieldTimer);
|
|
1198
|
-
|
|
776
|
+
}
|
|
777
|
+
if (yielded || run.session.backgrounded) {
|
|
1199
778
|
return;
|
|
779
|
+
}
|
|
1200
780
|
if (outcome.status === "failed") {
|
|
1201
781
|
reject(new Error(outcome.reason ?? "Command failed."));
|
|
1202
782
|
return;
|
|
@@ -1218,10 +798,12 @@ export function createExecTool(defaults) {
|
|
|
1218
798
|
});
|
|
1219
799
|
})
|
|
1220
800
|
.catch((err) => {
|
|
1221
|
-
if (yieldTimer)
|
|
801
|
+
if (yieldTimer) {
|
|
1222
802
|
clearTimeout(yieldTimer);
|
|
1223
|
-
|
|
803
|
+
}
|
|
804
|
+
if (yielded || run.session.backgrounded) {
|
|
1224
805
|
return;
|
|
806
|
+
}
|
|
1225
807
|
reject(err);
|
|
1226
808
|
});
|
|
1227
809
|
});
|