@poolzin/pool-bot 2026.2.24 → 2026.2.26
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/event-mapper.js +87 -22
- package/dist/acp/meta.js +12 -6
- package/dist/acp/secret-file.js +22 -0
- package/dist/agents/agent-paths.js +8 -9
- package/dist/agents/agent-scope.js +17 -5
- package/dist/agents/auth-profiles/oauth.js +148 -64
- package/dist/agents/auth-profiles/session-override.js +13 -7
- 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 +240 -0
- package/dist/agents/bash-tools.exec-host-node.js +235 -0
- package/dist/agents/bash-tools.exec-runtime.js +2 -25
- package/dist/agents/bash-tools.exec-types.js +1 -0
- package/dist/agents/bash-tools.process.js +224 -218
- package/dist/agents/bedrock-discovery.js +3 -1
- package/dist/agents/byteplus-models.js +97 -0
- package/dist/agents/chutes-oauth.js +1 -0
- package/dist/agents/cli-runner/helpers.js +4 -0
- package/dist/agents/compaction.js +41 -14
- package/dist/agents/content-blocks.js +16 -0
- package/dist/agents/doubao-models.js +121 -0
- package/dist/agents/failover-error.js +2 -0
- package/dist/agents/huggingface-models.js +5 -3
- package/dist/agents/live-model-filter.js +5 -0
- package/dist/agents/minimax-vlm.js +10 -8
- package/dist/agents/model-auth.js +6 -0
- package/dist/agents/model-catalog.js +3 -1
- package/dist/agents/model-fallback.js +96 -101
- package/dist/agents/model-selection.js +7 -1
- package/dist/agents/models-config.providers.js +364 -165
- package/dist/agents/ollama-stream.js +117 -4
- package/dist/agents/opencode-zen-models.js +22 -11
- package/dist/agents/pi-embedded-helpers/errors.js +55 -33
- package/dist/agents/pi-embedded-helpers/messaging-dedupe.js +10 -5
- package/dist/agents/pi-embedded-helpers/thinking.js +10 -5
- package/dist/agents/pi-embedded-helpers.js +1 -1
- package/dist/agents/pi-embedded-payloads.js +1 -0
- package/dist/agents/pi-embedded-runner/compact.js +29 -7
- package/dist/agents/pi-embedded-runner/extensions.js +28 -26
- package/dist/agents/pi-embedded-runner/google.js +20 -8
- package/dist/agents/pi-embedded-runner/run/attempt.js +95 -36
- package/dist/agents/pi-embedded-runner/run.js +71 -12
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +11 -2
- package/dist/agents/pi-embedded-runner/session-manager-cache.js +11 -7
- package/dist/agents/pi-embedded-runner/system-prompt.js +2 -0
- package/dist/agents/pi-embedded-runner/thinking.js +42 -0
- package/dist/agents/pi-embedded-runner/tool-name-allowlist.js +19 -0
- package/dist/agents/pi-embedded-runner/utils.js +7 -10
- package/dist/agents/pi-embedded-subscribe.handlers.lifecycle.js +45 -56
- package/dist/agents/pi-embedded-subscribe.handlers.tools.js +2 -2
- package/dist/agents/pi-embedded-subscribe.js +9 -4
- package/dist/agents/pi-embedded-subscribe.tools.js +68 -14
- package/dist/agents/pi-embedded-utils.js +3 -0
- package/dist/agents/pi-extensions/compaction-safeguard-runtime.js +4 -20
- package/dist/agents/pi-extensions/compaction-safeguard.js +75 -33
- package/dist/agents/pi-settings.js +40 -0
- package/dist/agents/pi-tools.policy.js +2 -1
- package/dist/agents/provider/config-loader.js +1 -1
- package/dist/agents/sandbox/browser.js +170 -33
- package/dist/agents/sandbox/config-hash.js +14 -27
- package/dist/agents/sandbox/config.js +21 -2
- package/dist/agents/sandbox/constants.js +2 -0
- package/dist/agents/sandbox/docker.js +16 -2
- package/dist/agents/sandbox/novnc-auth.js +62 -0
- package/dist/agents/sandbox/sanitize-env-vars.js +1 -1
- package/dist/agents/sandbox/shared.js +10 -6
- package/dist/agents/sandbox-paths.js +24 -11
- package/dist/agents/schema/clean-for-gemini.js +132 -85
- package/dist/agents/session-slug.js +10 -5
- package/dist/agents/session-tool-result-guard-wrapper.js +1 -0
- package/dist/agents/session-tool-result-guard.js +3 -1
- package/dist/agents/session-transcript-repair.js +40 -6
- package/dist/agents/skills/bundled-dir.js +19 -5
- package/dist/agents/skills/env-overrides.js +124 -43
- package/dist/agents/skills/frontmatter.js +6 -6
- package/dist/agents/skills/plugin-skills.js +14 -7
- package/dist/agents/skills/workspace.js +1 -0
- package/dist/agents/skills.test-helpers.js +13 -0
- package/dist/agents/stable-stringify.js +12 -0
- package/dist/agents/subagent-announce.js +251 -49
- package/dist/agents/subagent-lifecycle-events.js +19 -0
- package/dist/agents/subagent-registry-cleanup.js +31 -0
- package/dist/agents/subagent-registry-completion.js +68 -0
- package/dist/agents/subagent-registry-queries.js +117 -0
- package/dist/agents/subagent-registry-state.js +46 -0
- package/dist/agents/subagent-registry.js +252 -221
- package/dist/agents/subagent-registry.mocks.shared.js +12 -0
- package/dist/agents/subagent-registry.store.js +1 -0
- package/dist/agents/subagent-registry.types.js +1 -0
- package/dist/agents/subagent-spawn.js +195 -7
- package/dist/agents/system-prompt.js +22 -6
- package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
- package/dist/agents/test-helpers/fast-coding-tools.js +1 -18
- package/dist/agents/test-helpers/fast-core-tools.js +1 -17
- package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
- package/dist/agents/timeout.js +18 -6
- package/dist/agents/tool-call-id.js +1 -1
- package/dist/agents/tool-display-common.js +162 -29
- package/dist/agents/tool-images.js +82 -9
- package/dist/agents/tool-policy-shared.js +108 -0
- package/dist/agents/tool-policy.js +51 -26
- package/dist/agents/tools/browser-tool.js +160 -54
- package/dist/agents/tools/canvas-tool.js +27 -1
- package/dist/agents/tools/common.js +45 -0
- package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
- package/dist/agents/tools/discord-actions-guild.js +4 -1
- package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
- package/dist/agents/tools/gateway-tool.js +3 -1
- package/dist/agents/tools/image-tool.js +214 -99
- package/dist/agents/tools/nodes-utils.js +1 -10
- package/dist/agents/tools/sessions-history-tool.js +140 -108
- package/dist/agents/tools/sessions-send-helpers.js +12 -6
- package/dist/agents/tools/sessions-spawn-tool.js +8 -2
- package/dist/agents/tools/subagents-tool.js +2 -1
- package/dist/agents/tools/whatsapp-actions.js +10 -2
- package/dist/agents/tools/whatsapp-target-auth.js +18 -0
- package/dist/agents/transcript-policy.js +22 -8
- package/dist/agents/venice-models.js +11 -3
- package/dist/agents/workspace.js +222 -46
- package/dist/auto-reply/commands-registry.data.js +51 -0
- package/dist/auto-reply/commands-registry.js +19 -21
- package/dist/auto-reply/fallback-state.js +114 -0
- package/dist/auto-reply/group-activation.js +10 -5
- package/dist/auto-reply/inbound-debounce.js +10 -5
- package/dist/auto-reply/model-runtime.js +68 -0
- package/dist/auto-reply/reply/abort.js +1 -1
- package/dist/auto-reply/reply/agent-runner-execution.js +40 -5
- package/dist/auto-reply/reply/agent-runner.js +165 -39
- package/dist/auto-reply/reply/bash-command.js +41 -39
- package/dist/auto-reply/reply/command-gates.js +25 -0
- package/dist/auto-reply/reply/commands-allowlist.js +111 -72
- package/dist/auto-reply/reply/commands-bash.js +6 -5
- package/dist/auto-reply/reply/commands-config.js +30 -28
- package/dist/auto-reply/reply/commands-core.js +2 -1
- package/dist/auto-reply/reply/commands-info.js +1 -0
- package/dist/auto-reply/reply/commands-models.js +65 -14
- package/dist/auto-reply/reply/commands-session.js +237 -82
- package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
- package/dist/auto-reply/reply/commands-setunset.js +45 -0
- package/dist/auto-reply/reply/commands-subagents/action-agents.js +44 -0
- package/dist/auto-reply/reply/commands-subagents/action-focus.js +64 -0
- package/dist/auto-reply/reply/commands-subagents/action-help.js +4 -0
- package/dist/auto-reply/reply/commands-subagents/action-info.js +45 -0
- package/dist/auto-reply/reply/commands-subagents/action-kill.js +60 -0
- package/dist/auto-reply/reply/commands-subagents/action-list.js +44 -0
- package/dist/auto-reply/reply/commands-subagents/action-log.js +29 -0
- package/dist/auto-reply/reply/commands-subagents/action-send.js +119 -0
- package/dist/auto-reply/reply/commands-subagents/action-spawn.js +52 -0
- package/dist/auto-reply/reply/commands-subagents/action-unfocus.js +30 -0
- package/dist/auto-reply/reply/commands-subagents/shared.js +303 -0
- package/dist/auto-reply/reply/commands-subagents.js +51 -587
- package/dist/auto-reply/reply/commands-tts.js +10 -5
- package/dist/auto-reply/reply/config-value.js +10 -5
- package/dist/auto-reply/reply/directive-handling.model-picker.js +12 -6
- package/dist/auto-reply/reply/directive-handling.persist.js +9 -21
- package/dist/auto-reply/reply/directive-handling.shared.js +24 -4
- package/dist/auto-reply/reply/followup-runner.js +1 -0
- package/dist/auto-reply/reply/get-reply-directives-utils.js +23 -14
- package/dist/auto-reply/reply/get-reply-directives.js +17 -28
- package/dist/auto-reply/reply/get-reply-inline-actions.js +1 -0
- package/dist/auto-reply/reply/get-reply.js +71 -12
- package/dist/auto-reply/reply/model-selection.js +80 -39
- package/dist/auto-reply/reply/queue/enqueue.js +10 -5
- package/dist/auto-reply/reply/queue/state.js +13 -12
- package/dist/auto-reply/reply/reply-payloads.js +67 -36
- package/dist/auto-reply/reply/reply-reference.js +9 -8
- package/dist/auto-reply/reply/route-reply.js +15 -8
- package/dist/auto-reply/reply/session-reset-prompt.js +1 -1
- package/dist/auto-reply/reply/session.js +22 -6
- package/dist/auto-reply/reply/strip-inbound-meta.js +147 -0
- package/dist/auto-reply/reply/subagents-utils.js +56 -30
- package/dist/auto-reply/reply/typing.js +46 -21
- package/dist/auto-reply/send-policy.js +14 -7
- package/dist/auto-reply/status.js +140 -16
- package/dist/auto-reply/templating.js +10 -5
- package/dist/auto-reply/thinking.js +7 -16
- package/dist/auto-reply/tokens.js +21 -5
- package/dist/browser/bridge-server.js +36 -20
- package/dist/browser/cdp.helpers.js +7 -14
- package/dist/browser/cdp.js +35 -15
- package/dist/browser/chrome.profile-decoration.js +7 -4
- package/dist/browser/config.js +30 -0
- package/dist/browser/extension-relay-auth.js +55 -0
- package/dist/browser/extension-relay.js +74 -29
- package/dist/browser/navigation-guard.js +39 -0
- package/dist/browser/paths.js +77 -0
- package/dist/browser/profiles.js +13 -8
- package/dist/browser/pw-ai-module.js +10 -5
- package/dist/browser/pw-session.js +76 -39
- package/dist/browser/pw-tools-core.interactions.js +14 -7
- package/dist/browser/pw-tools-core.state.js +12 -6
- 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-context.js +7 -0
- package/dist/browser/server-lifecycle.js +37 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/allow-from.js +26 -0
- package/dist/channels/allowlists/resolve-utils.js +43 -19
- package/dist/channels/channel-config.js +14 -7
- package/dist/channels/draft-stream-loop.js +7 -0
- package/dist/channels/model-overrides.js +82 -0
- package/dist/channels/plugins/account-action-gate.js +13 -0
- package/dist/channels/plugins/message-actions.js +10 -0
- package/dist/channels/plugins/normalize/imessage.js +14 -7
- package/dist/channels/plugins/normalize/slack.js +10 -5
- package/dist/channels/plugins/normalize/telegram.js +14 -7
- package/dist/channels/plugins/outbound/discord.js +80 -8
- package/dist/channels/plugins/outbound/signal.js +11 -11
- package/dist/channels/plugins/setup-helpers.js +10 -5
- package/dist/channels/sender-label.js +14 -7
- package/dist/channels/session.js +4 -2
- package/dist/channels/status-reactions.js +297 -0
- package/dist/channels/telegram/api.js +18 -0
- package/dist/cli/argv.js +84 -21
- package/dist/cli/banner.js +3 -2
- package/dist/cli/browser-cli-actions-input/register.files-downloads.js +65 -56
- package/dist/cli/cli-name.js +11 -11
- package/dist/cli/cli-utils.js +13 -3
- package/dist/cli/command-format.js +1 -1
- package/dist/cli/config-cli.js +1 -1
- package/dist/cli/daemon-cli/lifecycle-core.js +31 -19
- package/dist/cli/daemon-cli/lifecycle.js +64 -2
- package/dist/cli/daemon-cli/restart-health.js +126 -0
- package/dist/cli/daemon-cli/status.gather.js +9 -13
- package/dist/cli/daemon-cli/status.print.js +2 -10
- package/dist/cli/deps.js +27 -22
- package/dist/cli/exec-approvals-cli.js +92 -124
- package/dist/cli/gateway-cli/run-loop.js +23 -5
- package/dist/cli/memory-cli.js +158 -61
- package/dist/cli/node-cli/register.js +14 -5
- package/dist/cli/nodes-cli/register.push.js +63 -0
- package/dist/cli/nodes-media-utils.js +26 -0
- package/dist/cli/outbound-send-deps.js +2 -9
- package/dist/cli/outbound-send-mapping.js +11 -0
- package/dist/cli/pairing-cli.js +40 -14
- package/dist/cli/plugins-cli.js +250 -73
- package/dist/cli/ports.js +11 -10
- package/dist/cli/program/build-program.js +3 -1
- package/dist/cli/program/command-registry.js +214 -136
- package/dist/cli/program/command-tree.js +16 -0
- package/dist/cli/program/help.js +43 -12
- package/dist/cli/program/preaction.js +13 -9
- package/dist/cli/program/register.configure.js +3 -18
- package/dist/cli/program/register.maintenance.js +2 -2
- package/dist/cli/program/register.onboard.js +2 -0
- package/dist/cli/program/register.status-health-sessions.js +16 -17
- package/dist/cli/program/register.subclis.js +93 -52
- package/dist/cli/route.js +12 -8
- package/dist/cli/system-cli.js +36 -46
- package/dist/cli/test-runtime-capture.js +24 -0
- package/dist/cli/update-cli/shared.js +22 -9
- package/dist/cli/update-cli/update-command.js +89 -14
- package/dist/cli/update-cli/wizard.js +6 -12
- package/dist/commands/agent/run-context.js +18 -5
- package/dist/commands/agent/session-store.js +17 -4
- package/dist/commands/agent.js +185 -89
- package/dist/commands/agents.bindings.js +14 -7
- package/dist/commands/agents.commands.add.js +13 -9
- package/dist/commands/agents.commands.identity.js +12 -6
- package/dist/commands/agents.commands.list.js +11 -6
- package/dist/commands/agents.config.js +8 -10
- package/dist/commands/agents.providers.js +12 -6
- package/dist/commands/auth-choice-options.js +103 -75
- package/dist/commands/auth-choice.apply.byteplus.js +55 -0
- package/dist/commands/auth-choice.apply.js +4 -0
- package/dist/commands/auth-choice.apply.minimax.js +61 -13
- package/dist/commands/auth-choice.apply.openai.js +3 -1
- package/dist/commands/auth-choice.apply.volcengine.js +55 -0
- package/dist/commands/auth-choice.preferred-provider.js +2 -0
- package/dist/commands/channels/remove.js +13 -6
- package/dist/commands/channels/shared.js +4 -14
- package/dist/commands/channels.mock-harness.js +23 -0
- package/dist/commands/configure.commands.js +14 -0
- package/dist/commands/configure.gateway.js +2 -4
- package/dist/commands/configure.js +1 -1
- package/dist/commands/configure.shared.js +11 -0
- package/dist/commands/daemon-install-helpers.js +2 -2
- package/dist/commands/daemon-install-runtime-warning.js +11 -0
- package/dist/commands/dashboard.js +12 -10
- package/dist/commands/docs.js +14 -8
- package/dist/commands/doctor-config-flow.js +11 -9
- package/dist/commands/doctor-legacy-config.js +281 -0
- package/dist/commands/doctor-state-integrity.js +99 -23
- package/dist/commands/doctor-update.js +12 -9
- package/dist/commands/models/list.list-command.js +7 -5
- package/dist/commands/models/set-image.js +2 -21
- package/dist/commands/node-daemon-install-helpers.js +10 -8
- package/dist/commands/onboard-auth.config-minimax.js +54 -80
- package/dist/commands/onboard-auth.config-opencode.js +2 -18
- package/dist/commands/onboard-auth.credentials.js +90 -13
- package/dist/commands/onboard-auth.js +1 -1
- package/dist/commands/onboard-auth.models.js +6 -5
- package/dist/commands/onboard-hooks.js +1 -1
- package/dist/commands/onboard-non-interactive/api-keys.js +14 -7
- package/dist/commands/onboard-non-interactive/local/auth-choice.js +64 -49
- package/dist/commands/onboard-provider-auth-flags.js +14 -0
- package/dist/commands/onboard-remote.js +14 -7
- package/dist/commands/onboard.js +11 -13
- package/dist/commands/sandbox-display.js +6 -5
- package/dist/commands/sessions.test-helpers.js +61 -0
- package/dist/commands/status-all/diagnosis.js +14 -10
- package/dist/commands/status-all/format.js +1 -0
- package/dist/commands/status.gateway-probe.js +1 -16
- package/dist/commands/systemd-linger.js +12 -6
- package/dist/config/agent-limits.js +2 -0
- package/dist/config/commands.js +32 -15
- package/dist/config/config-paths.js +9 -11
- package/dist/config/config.js +1 -1
- package/dist/config/defaults.js +22 -2
- package/dist/config/discord-preview-streaming.js +104 -0
- package/dist/config/env-substitution.js +62 -34
- package/dist/config/env-vars.js +45 -7
- package/dist/config/includes.js +4 -0
- package/dist/config/io.js +656 -171
- package/dist/config/legacy.migrations.part-1.js +189 -78
- package/dist/config/legacy.shared.js +3 -1
- package/dist/config/merge-patch.js +54 -4
- package/dist/config/prototype-keys.js +4 -0
- package/dist/config/redact-snapshot.js +404 -76
- package/dist/config/schema.help.js +44 -7
- package/dist/config/schema.js +58 -570
- package/dist/config/schema.labels.js +38 -6
- package/dist/config/sessions/delivery-info.js +10 -3
- package/dist/config/sessions/main-session.js +10 -5
- package/dist/config/sessions/session-file.js +33 -0
- package/dist/config/sessions/session-key.js +10 -5
- package/dist/config/sessions/store.js +1 -1
- package/dist/config/sessions.js +1 -0
- package/dist/config/validation.js +140 -85
- package/dist/config/zod-schema.agent-runtime.js +11 -0
- package/dist/config/zod-schema.hooks.js +40 -11
- package/dist/config/zod-schema.installs.js +20 -0
- package/dist/config/zod-schema.js +156 -20
- package/dist/config/zod-schema.providers-core.js +78 -4
- package/dist/config/zod-schema.providers.js +6 -1
- package/dist/config/zod-schema.session.js +41 -2
- package/dist/cron/run-log.js +3 -0
- package/dist/cron/schedule.js +21 -10
- package/dist/cron/service/ops.js +35 -21
- package/dist/cron/service/timer.js +116 -16
- package/dist/cron/stagger.js +3 -1
- 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/api.js +12 -6
- package/dist/discord/draft-chunking.js +22 -0
- package/dist/discord/draft-stream.js +124 -0
- package/dist/discord/monitor/agent-components.js +1 -1
- package/dist/discord/monitor/commands.js +5 -0
- package/dist/discord/monitor/exec-approvals.js +357 -162
- package/dist/discord/monitor/gateway-plugin.js +2 -1
- package/dist/discord/monitor/listeners.js +37 -27
- package/dist/discord/monitor/message-handler.js +4 -1
- package/dist/discord/monitor/message-handler.preflight.js +65 -8
- package/dist/discord/monitor/message-handler.process.js +246 -217
- package/dist/discord/monitor/message-utils.js +143 -6
- package/dist/discord/monitor/model-picker-preferences.js +143 -0
- package/dist/discord/monitor/model-picker.js +651 -0
- package/dist/discord/monitor/native-command.js +573 -16
- package/dist/discord/monitor/provider.allowlist.js +223 -0
- package/dist/discord/monitor/provider.js +275 -347
- package/dist/discord/monitor/provider.lifecycle.js +100 -0
- package/dist/discord/monitor/reply-delivery.js +123 -16
- package/dist/discord/monitor/thread-bindings.discord-api.js +215 -0
- package/dist/discord/monitor/thread-bindings.js +4 -0
- package/dist/discord/monitor/thread-bindings.lifecycle.js +177 -0
- package/dist/discord/monitor/thread-bindings.manager.js +423 -0
- package/dist/discord/monitor/thread-bindings.messages.js +55 -0
- package/dist/discord/monitor/thread-bindings.state.js +358 -0
- package/dist/discord/monitor/thread-bindings.types.js +6 -0
- package/dist/discord/resolve-users.js +33 -21
- package/dist/discord/send.channels.js +15 -0
- package/dist/discord/send.js +3 -2
- package/dist/discord/send.outbound.js +82 -26
- package/dist/discord/send.permissions.js +83 -30
- package/dist/discord/send.reactions.js +8 -4
- package/dist/discord/token.js +10 -5
- package/dist/discord/voice/command.js +263 -0
- package/dist/discord/voice/manager.js +531 -0
- package/dist/gateway/auth.js +72 -13
- package/dist/gateway/call.js +152 -83
- package/dist/gateway/canvas-capability.js +75 -0
- package/dist/gateway/client.js +28 -4
- package/dist/gateway/config-reload.js +3 -4
- package/dist/gateway/control-plane-audit.js +28 -0
- package/dist/gateway/control-plane-rate-limit.js +53 -0
- package/dist/gateway/control-ui.js +219 -96
- package/dist/gateway/events.js +1 -0
- package/dist/gateway/hooks-mapping.js +88 -38
- package/dist/gateway/hooks.js +109 -54
- package/dist/gateway/http-auth-helpers.js +3 -2
- package/dist/gateway/http-common.js +22 -0
- package/dist/gateway/http-endpoint-helpers.js +1 -0
- package/dist/gateway/method-scopes.js +169 -0
- package/dist/gateway/net.js +74 -9
- package/dist/gateway/node-invoke-system-run-approval.js +14 -35
- package/dist/gateway/node-registry.js +10 -5
- package/dist/gateway/openai-http.js +1 -0
- package/dist/gateway/openresponses-http.js +121 -110
- package/dist/gateway/origin-check.js +1 -18
- package/dist/gateway/probe-auth.js +2 -0
- package/dist/gateway/protocol/index.js +4 -2
- package/dist/gateway/protocol/schema/cron.js +1 -0
- package/dist/gateway/protocol/schema/devices.js +1 -0
- package/dist/gateway/protocol/schema/protocol-schemas.js +4 -1
- package/dist/gateway/protocol/schema/push.js +18 -0
- package/dist/gateway/protocol/schema/sessions.js +6 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/role-policy.js +17 -0
- package/dist/gateway/server/ws-connection/connect-policy.js +37 -0
- package/dist/gateway/server/ws-connection/message-handler.js +175 -148
- package/dist/gateway/server-chat.js +83 -25
- package/dist/gateway/server-constants.js +10 -9
- package/dist/gateway/server-cron.js +1 -0
- package/dist/gateway/server-http.js +247 -54
- package/dist/gateway/server-maintenance.js +20 -5
- package/dist/gateway/server-methods/agent.js +162 -24
- package/dist/gateway/server-methods/chat.js +465 -130
- package/dist/gateway/server-methods/config.js +193 -152
- package/dist/gateway/server-methods/devices.js +17 -3
- package/dist/gateway/server-methods/models.js +11 -1
- 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-methods/sessions.js +64 -8
- package/dist/gateway/server-methods/usage.js +162 -75
- package/dist/gateway/server-node-events.js +29 -0
- package/dist/gateway/server-reload-handlers.js +2 -3
- package/dist/gateway/server-runtime-config.js +39 -13
- package/dist/gateway/server-runtime-state.js +2 -0
- package/dist/gateway/server-startup-memory.js +17 -11
- 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/session-utils.fs.js +32 -34
- package/dist/gateway/sessions-resolve.js +17 -5
- 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.openai-mock.js +14 -7
- package/dist/gateway/test-helpers.server.js +161 -77
- package/dist/gateway/tools-invoke-http.js +21 -10
- package/dist/hooks/bundled/bootstrap-extra-files/handler.js +3 -1
- package/dist/hooks/bundled/command-logger/handler.js +7 -2
- package/dist/hooks/bundled/session-memory/handler.js +170 -38
- package/dist/hooks/frontmatter.js +6 -6
- package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
- package/dist/hooks/gmail-watcher.js +11 -6
- package/dist/hooks/internal-hooks.js +11 -1
- package/dist/hooks/llm-slug-generator.js +4 -1
- package/dist/hooks/workspace.js +47 -17
- package/dist/imessage/accounts.js +9 -20
- package/dist/imessage/monitor/inbound-processing.js +2 -1
- package/dist/infra/archive-path.js +49 -0
- package/dist/infra/archive.js +174 -73
- package/dist/infra/control-ui-assets.js +14 -6
- package/dist/infra/device-pairing.js +204 -144
- package/dist/infra/env.js +10 -5
- package/dist/infra/exec-approvals-allowlist.js +141 -70
- package/dist/infra/exec-approvals-analysis.js +78 -20
- package/dist/infra/exec-approvals.js +5 -17
- package/dist/infra/exec-safe-bin-policy.js +277 -0
- package/dist/infra/fixed-window-rate-limit.js +33 -0
- package/dist/infra/fs-safe.js +71 -39
- package/dist/infra/gateway-lock.js +6 -2
- 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/heartbeat-wake.js +6 -12
- package/dist/infra/host-env-security-policy.json +19 -0
- package/dist/infra/host-env-security.js +66 -0
- package/dist/infra/install-source-utils.js +91 -7
- package/dist/infra/net/ssrf.js +131 -38
- 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/bound-delivery-router.js +88 -0
- package/dist/infra/outbound/channel-adapters.js +20 -7
- package/dist/infra/outbound/channel-selection.js +12 -6
- package/dist/infra/outbound/envelope.js +1 -1
- package/dist/infra/outbound/format.js +12 -6
- 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/outbound/payloads.js +14 -7
- package/dist/infra/outbound/session-binding-service.js +123 -0
- package/dist/infra/pairing-files.js +10 -0
- package/dist/infra/path-guards.js +25 -0
- package/dist/infra/plain-object.js +9 -0
- package/dist/infra/provider-usage.fetch.codex.js +7 -15
- package/dist/infra/provider-usage.fetch.gemini.js +14 -11
- package/dist/infra/provider-usage.fetch.shared.js +30 -1
- package/dist/infra/provider-usage.fetch.zai.js +10 -9
- 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/retry-policy.js +4 -2
- package/dist/infra/retry.js +9 -5
- package/dist/infra/scp-host.js +54 -0
- package/dist/infra/session-cost-usage.js +107 -59
- package/dist/infra/session-maintenance-warning.js +3 -1
- package/dist/infra/shell-env.js +98 -34
- package/dist/infra/ssh-config.js +12 -6
- package/dist/infra/system-run-command.js +49 -4
- package/dist/infra/update-channels.js +10 -5
- package/dist/infra/update-startup.js +86 -9
- package/dist/line/accounts.js +5 -7
- package/dist/line/bot-access.js +8 -20
- package/dist/line/bot-handlers.js +3 -1
- package/dist/link-understanding/detect.js +15 -7
- package/dist/media/constants.js +15 -6
- package/dist/media/image-ops.js +7 -0
- package/dist/media/inbound-path-policy.js +114 -0
- package/dist/media/input-files.js +16 -0
- package/dist/media/local-roots.js +3 -2
- package/dist/media-understanding/apply.js +4 -1
- package/dist/media-understanding/concurrency.js +8 -20
- package/dist/memory/backend-config.js +45 -6
- package/dist/memory/embeddings.js +10 -4
- package/dist/memory/fs-utils.js +23 -0
- package/dist/memory/manager-search.js +12 -6
- package/dist/memory/manager-sync-ops.js +12 -2
- package/dist/memory/qmd-manager.js +466 -53
- package/dist/memory/query-expansion.js +167 -3
- package/dist/memory/status-format.js +10 -5
- package/dist/memory/sync-memory-files.js +1 -1
- package/dist/memory/test-manager.js +8 -0
- package/dist/node-host/invoke-system-run.js +281 -0
- package/dist/node-host/invoke.js +55 -337
- package/dist/pairing/pairing-store.js +22 -0
- package/dist/plugin-sdk/allow-from.js +1 -1
- package/dist/plugin-sdk/command-auth.js +3 -1
- package/dist/plugin-sdk/index.js +6 -3
- package/dist/plugin-sdk/temp-path.js +47 -0
- package/dist/plugin-sdk/webhook-targets.js +32 -0
- package/dist/plugins/bundled-dir.js +9 -6
- package/dist/plugins/discovery.js +217 -23
- package/dist/plugins/hook-runner-global.js +16 -0
- package/dist/plugins/hooks.js +50 -0
- package/dist/plugins/install.js +28 -16
- 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/plugins/runtime.js +3 -17
- package/dist/plugins/update.js +78 -12
- package/dist/process/spawn-utils.js +14 -7
- package/dist/providers/github-copilot-models.js +4 -1
- package/dist/providers/github-copilot-token.js +11 -6
- package/dist/providers/qwen-portal-oauth.js +14 -6
- package/dist/routing/account-id.js +30 -0
- package/dist/routing/resolve-route.js +3 -7
- package/dist/routing/session-key.js +2 -16
- package/dist/security/audit-channel.js +100 -20
- package/dist/security/audit-extra.async.js +505 -179
- package/dist/security/audit-extra.js +12 -2
- package/dist/security/audit-extra.sync.js +421 -35
- package/dist/security/audit-fs.js +31 -13
- package/dist/security/audit.js +180 -370
- package/dist/security/dm-policy-shared.js +68 -0
- package/dist/security/external-content.js +46 -14
- 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/entry-status.js +6 -0
- package/dist/shared/frontmatter.js +5 -5
- package/dist/shared/node-list-parse.js +13 -0
- package/dist/shared/node-match.js +11 -4
- package/dist/shared/operator-scope-compat.js +42 -0
- package/dist/shared/text-chunking.js +29 -0
- package/dist/signal/accounts.js +7 -20
- package/dist/signal/monitor/event-handler.js +3 -1
- package/dist/slack/accounts.js +6 -19
- package/dist/slack/actions.js +11 -3
- package/dist/slack/blocks.test-helpers.js +31 -0
- package/dist/slack/monitor/auth.js +1 -1
- package/dist/slack/monitor/message-handler/dispatch.js +50 -29
- package/dist/slack/monitor/mrkdwn.js +8 -0
- package/dist/slack/monitor/replies.js +15 -7
- package/dist/slack/monitor/slash.js +22 -13
- package/dist/slack/resolve-channels.js +10 -5
- package/dist/slack/send.js +102 -12
- package/dist/slack/stream-mode.js +10 -0
- package/dist/slack/streaming.js +4 -2
- package/dist/telegram/accounts.js +19 -14
- package/dist/telegram/bot/helpers.js +3 -5
- package/dist/telegram/bot-access.js +35 -36
- package/dist/telegram/bot-handlers.js +120 -148
- package/dist/telegram/bot-message-context.js +68 -9
- package/dist/telegram/bot-message-dispatch.js +477 -210
- package/dist/telegram/bot-native-commands.js +16 -0
- package/dist/telegram/draft-stream.js +44 -8
- package/dist/telegram/inline-buttons.js +5 -15
- package/dist/telegram/monitor.js +11 -7
- package/dist/telegram/network-config.js +19 -7
- package/dist/telegram/reasoning-lane-coordinator.js +128 -0
- package/dist/telegram/send.js +3 -2
- package/dist/telegram/sent-message-cache.js +5 -6
- package/dist/telegram/status-reaction-variants.js +208 -0
- package/dist/telegram/sticker-cache.js +11 -9
- package/dist/terminal/prompt-select-styled.js +9 -0
- package/dist/terminal/theme.js +12 -12
- 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/tts/tts.js +80 -567
- package/dist/tui/components/chat-log.js +50 -8
- package/dist/tui/theme/theme.js +10 -12
- package/dist/tui/tui-command-handlers.js +36 -27
- package/dist/tui/tui-event-handlers.js +122 -32
- package/dist/tui/tui-local-shell.js +16 -6
- package/dist/tui/tui.js +236 -48
- package/dist/utils/account-id.js +2 -4
- package/dist/utils/boolean.js +10 -5
- package/dist/utils/directive-tags.js +11 -0
- package/dist/utils/mask-api-key.js +10 -0
- package/dist/utils/queue-helpers.js +67 -12
- package/dist/utils/run-with-concurrency.js +39 -0
- package/dist/web/auto-reply/deliver-reply.js +8 -4
- package/dist/web/auto-reply/mentions.js +10 -5
- package/dist/web/auto-reply/monitor/group-members.js +14 -7
- package/dist/web/auto-reply/monitor/process-message.js +45 -24
- package/dist/web/inbound/access-control.js +5 -2
- package/dist/web/login-qr.js +12 -6
- package/dist/web/media.js +126 -15
- package/docs/tools/slash-commands.md +5 -1
- package/extensions/bluebubbles/src/monitor-processing.ts +580 -139
- package/extensions/bluebubbles/src/monitor.ts +208 -1950
- 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/dist/config/io.js
CHANGED
|
@@ -2,19 +2,25 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
6
|
import JSON5 from "json5";
|
|
7
|
+
import { loadDotEnv } from "../infra/dotenv.js";
|
|
8
|
+
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
|
|
6
9
|
import { loadShellEnvFallback, resolveShellEnvFallbackTimeoutMs, shouldDeferShellEnvFallback, shouldEnableShellEnvFallback, } from "../infra/shell-env.js";
|
|
10
|
+
import { VERSION } from "../version.js";
|
|
7
11
|
import { DuplicateAgentDirError, findDuplicateAgentDirs } from "./agent-dirs.js";
|
|
12
|
+
import { rotateConfigBackups } from "./backup-rotation.js";
|
|
8
13
|
import { applyCompactionDefaults, applyContextPruningDefaults, applyAgentDefaults, applyLoggingDefaults, applyMessageDefaults, applyModelDefaults, applySessionDefaults, applyTalkApiKey, } from "./defaults.js";
|
|
9
|
-
import {
|
|
10
|
-
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
|
|
11
|
-
import {
|
|
14
|
+
import { restoreEnvVarRefs } from "./env-preserve.js";
|
|
15
|
+
import { MissingEnvVarError, containsEnvVarReference, resolveConfigEnvVars, } from "./env-substitution.js";
|
|
16
|
+
import { applyConfigEnvVars } from "./env-vars.js";
|
|
12
17
|
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
|
|
13
18
|
import { findLegacyConfigIssues } from "./legacy.js";
|
|
19
|
+
import { applyMergePatch } from "./merge-patch.js";
|
|
14
20
|
import { normalizeConfigPaths } from "./normalize-paths.js";
|
|
15
21
|
import { resolveConfigPath, resolveDefaultConfigCandidates, resolveStateDir } from "./paths.js";
|
|
16
22
|
import { applyConfigOverrides } from "./runtime-overrides.js";
|
|
17
|
-
import { validateConfigObjectWithPlugins } from "./validation.js";
|
|
23
|
+
import { validateConfigObjectRawWithPlugins, validateConfigObjectWithPlugins, } from "./validation.js";
|
|
18
24
|
import { comparePoolbotVersions } from "./version.js";
|
|
19
25
|
// Re-export for backwards compatibility
|
|
20
26
|
export { CircularIncludeError, ConfigIncludeError } from "./includes.js";
|
|
@@ -36,10 +42,8 @@ const SHELL_ENV_EXPECTED_KEYS = [
|
|
|
36
42
|
"SLACK_APP_TOKEN",
|
|
37
43
|
"POOLBOT_GATEWAY_TOKEN",
|
|
38
44
|
"POOLBOT_GATEWAY_PASSWORD",
|
|
39
|
-
"CLAWDBOT_GATEWAY_TOKEN",
|
|
40
|
-
"CLAWDBOT_GATEWAY_PASSWORD",
|
|
41
45
|
];
|
|
42
|
-
const
|
|
46
|
+
const CONFIG_AUDIT_LOG_FILENAME = "config-audit.jsonl";
|
|
43
47
|
const loggedInvalidConfigs = new Set();
|
|
44
48
|
function hashConfigRaw(raw) {
|
|
45
49
|
return crypto
|
|
@@ -47,14 +51,92 @@ function hashConfigRaw(raw) {
|
|
|
47
51
|
.update(raw ?? "")
|
|
48
52
|
.digest("hex");
|
|
49
53
|
}
|
|
54
|
+
function isNumericPathSegment(raw) {
|
|
55
|
+
return /^[0-9]+$/.test(raw);
|
|
56
|
+
}
|
|
57
|
+
function isWritePlainObject(value) {
|
|
58
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
function unsetPathForWrite(root, pathSegments) {
|
|
61
|
+
if (pathSegments.length === 0) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const traversal = [];
|
|
65
|
+
let cursor = root;
|
|
66
|
+
for (let i = 0; i < pathSegments.length - 1; i += 1) {
|
|
67
|
+
const segment = pathSegments[i];
|
|
68
|
+
if (Array.isArray(cursor)) {
|
|
69
|
+
if (!isNumericPathSegment(segment)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const index = Number.parseInt(segment, 10);
|
|
73
|
+
if (!Number.isFinite(index) || index < 0 || index >= cursor.length) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
traversal.push({ container: cursor, key: index });
|
|
77
|
+
cursor = cursor[index];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!isWritePlainObject(cursor) || !(segment in cursor)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
traversal.push({ container: cursor, key: segment });
|
|
84
|
+
cursor = cursor[segment];
|
|
85
|
+
}
|
|
86
|
+
const leaf = pathSegments[pathSegments.length - 1];
|
|
87
|
+
if (Array.isArray(cursor)) {
|
|
88
|
+
if (!isNumericPathSegment(leaf)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const index = Number.parseInt(leaf, 10);
|
|
92
|
+
if (!Number.isFinite(index) || index < 0 || index >= cursor.length) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
cursor.splice(index, 1);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
if (!isWritePlainObject(cursor) || !(leaf in cursor)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
delete cursor[leaf];
|
|
102
|
+
}
|
|
103
|
+
// Prune now-empty object branches after unsetting to avoid dead config scaffolding.
|
|
104
|
+
for (let i = traversal.length - 1; i >= 0; i -= 1) {
|
|
105
|
+
const { container, key } = traversal[i];
|
|
106
|
+
let child;
|
|
107
|
+
if (Array.isArray(container)) {
|
|
108
|
+
child = typeof key === "number" ? container[key] : undefined;
|
|
109
|
+
}
|
|
110
|
+
else if (isWritePlainObject(container)) {
|
|
111
|
+
child = container[String(key)];
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (!isWritePlainObject(child) || Object.keys(child).length > 0) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(container) && typeof key === "number") {
|
|
120
|
+
if (key >= 0 && key < container.length) {
|
|
121
|
+
container.splice(key, 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (isWritePlainObject(container)) {
|
|
125
|
+
delete container[String(key)];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
50
130
|
export function resolveConfigSnapshotHash(snapshot) {
|
|
51
131
|
if (typeof snapshot.hash === "string") {
|
|
52
132
|
const trimmed = snapshot.hash.trim();
|
|
53
|
-
if (trimmed)
|
|
133
|
+
if (trimmed) {
|
|
54
134
|
return trimmed;
|
|
135
|
+
}
|
|
55
136
|
}
|
|
56
|
-
if (typeof snapshot.raw !== "string")
|
|
137
|
+
if (typeof snapshot.raw !== "string") {
|
|
57
138
|
return null;
|
|
139
|
+
}
|
|
58
140
|
return hashConfigRaw(snapshot.raw);
|
|
59
141
|
}
|
|
60
142
|
function coerceConfig(value) {
|
|
@@ -63,29 +145,217 @@ function coerceConfig(value) {
|
|
|
63
145
|
}
|
|
64
146
|
return value;
|
|
65
147
|
}
|
|
66
|
-
|
|
67
|
-
|
|
148
|
+
function isPlainObject(value) {
|
|
149
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
150
|
+
}
|
|
151
|
+
function hasConfigMeta(value) {
|
|
152
|
+
if (!isPlainObject(value)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const meta = value.meta;
|
|
156
|
+
return isPlainObject(meta);
|
|
157
|
+
}
|
|
158
|
+
function resolveGatewayMode(value) {
|
|
159
|
+
if (!isPlainObject(value)) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const gateway = value.gateway;
|
|
163
|
+
if (!isPlainObject(gateway) || typeof gateway.mode !== "string") {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const trimmed = gateway.mode.trim();
|
|
167
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
168
|
+
}
|
|
169
|
+
function cloneUnknown(value) {
|
|
170
|
+
return structuredClone(value);
|
|
171
|
+
}
|
|
172
|
+
function createMergePatch(base, target) {
|
|
173
|
+
if (!isPlainObject(base) || !isPlainObject(target)) {
|
|
174
|
+
return cloneUnknown(target);
|
|
175
|
+
}
|
|
176
|
+
const patch = {};
|
|
177
|
+
const keys = new Set([...Object.keys(base), ...Object.keys(target)]);
|
|
178
|
+
for (const key of keys) {
|
|
179
|
+
const hasBase = key in base;
|
|
180
|
+
const hasTarget = key in target;
|
|
181
|
+
if (!hasTarget) {
|
|
182
|
+
patch[key] = null;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const targetValue = target[key];
|
|
186
|
+
if (!hasBase) {
|
|
187
|
+
patch[key] = cloneUnknown(targetValue);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const baseValue = base[key];
|
|
191
|
+
if (isPlainObject(baseValue) && isPlainObject(targetValue)) {
|
|
192
|
+
const childPatch = createMergePatch(baseValue, targetValue);
|
|
193
|
+
if (isPlainObject(childPatch) && Object.keys(childPatch).length === 0) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
patch[key] = childPatch;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!isDeepStrictEqual(baseValue, targetValue)) {
|
|
200
|
+
patch[key] = cloneUnknown(targetValue);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return patch;
|
|
204
|
+
}
|
|
205
|
+
function collectEnvRefPaths(value, path, output) {
|
|
206
|
+
if (typeof value === "string") {
|
|
207
|
+
if (containsEnvVarReference(value)) {
|
|
208
|
+
output.set(path, value);
|
|
209
|
+
}
|
|
68
210
|
return;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
74
|
-
for (let index = maxIndex - 1; index >= 1; index -= 1) {
|
|
75
|
-
await ioFs.rename(`${backupBase}.${index}`, `${backupBase}.${index + 1}`).catch(() => {
|
|
76
|
-
// best-effort
|
|
211
|
+
}
|
|
212
|
+
if (Array.isArray(value)) {
|
|
213
|
+
value.forEach((item, index) => {
|
|
214
|
+
collectEnvRefPaths(item, `${path}[${index}]`, output);
|
|
77
215
|
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (isPlainObject(value)) {
|
|
219
|
+
for (const [key, child] of Object.entries(value)) {
|
|
220
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
221
|
+
collectEnvRefPaths(child, childPath, output);
|
|
222
|
+
}
|
|
78
223
|
}
|
|
79
|
-
|
|
224
|
+
}
|
|
225
|
+
function collectChangedPaths(base, target, path, output) {
|
|
226
|
+
if (Array.isArray(base) && Array.isArray(target)) {
|
|
227
|
+
const max = Math.max(base.length, target.length);
|
|
228
|
+
for (let index = 0; index < max; index += 1) {
|
|
229
|
+
const childPath = path ? `${path}[${index}]` : `[${index}]`;
|
|
230
|
+
if (index >= base.length || index >= target.length) {
|
|
231
|
+
output.add(childPath);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
collectChangedPaths(base[index], target[index], childPath, output);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (isPlainObject(base) && isPlainObject(target)) {
|
|
239
|
+
const keys = new Set([...Object.keys(base), ...Object.keys(target)]);
|
|
240
|
+
for (const key of keys) {
|
|
241
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
242
|
+
const hasBase = key in base;
|
|
243
|
+
const hasTarget = key in target;
|
|
244
|
+
if (!hasTarget || !hasBase) {
|
|
245
|
+
output.add(childPath);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
collectChangedPaths(base[key], target[key], childPath, output);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!isDeepStrictEqual(base, target)) {
|
|
253
|
+
output.add(path);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function parentPath(value) {
|
|
257
|
+
if (!value) {
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
if (value.endsWith("]")) {
|
|
261
|
+
const index = value.lastIndexOf("[");
|
|
262
|
+
return index > 0 ? value.slice(0, index) : "";
|
|
263
|
+
}
|
|
264
|
+
const index = value.lastIndexOf(".");
|
|
265
|
+
return index >= 0 ? value.slice(0, index) : "";
|
|
266
|
+
}
|
|
267
|
+
function isPathChanged(path, changedPaths) {
|
|
268
|
+
if (changedPaths.has(path)) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
let current = parentPath(path);
|
|
272
|
+
while (current) {
|
|
273
|
+
if (changedPaths.has(current)) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
current = parentPath(current);
|
|
277
|
+
}
|
|
278
|
+
return changedPaths.has("");
|
|
279
|
+
}
|
|
280
|
+
function restoreEnvRefsFromMap(value, path, envRefMap, changedPaths) {
|
|
281
|
+
if (typeof value === "string") {
|
|
282
|
+
if (!isPathChanged(path, changedPaths)) {
|
|
283
|
+
const original = envRefMap.get(path);
|
|
284
|
+
if (original !== undefined) {
|
|
285
|
+
return original;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
if (Array.isArray(value)) {
|
|
291
|
+
let changed = false;
|
|
292
|
+
const next = value.map((item, index) => {
|
|
293
|
+
const updated = restoreEnvRefsFromMap(item, `${path}[${index}]`, envRefMap, changedPaths);
|
|
294
|
+
if (updated !== item) {
|
|
295
|
+
changed = true;
|
|
296
|
+
}
|
|
297
|
+
return updated;
|
|
298
|
+
});
|
|
299
|
+
return changed ? next : value;
|
|
300
|
+
}
|
|
301
|
+
if (isPlainObject(value)) {
|
|
302
|
+
let changed = false;
|
|
303
|
+
const next = {};
|
|
304
|
+
for (const [key, child] of Object.entries(value)) {
|
|
305
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
306
|
+
const updated = restoreEnvRefsFromMap(child, childPath, envRefMap, changedPaths);
|
|
307
|
+
if (updated !== child) {
|
|
308
|
+
changed = true;
|
|
309
|
+
}
|
|
310
|
+
next[key] = updated;
|
|
311
|
+
}
|
|
312
|
+
return changed ? next : value;
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
function resolveConfigAuditLogPath(env, homedir) {
|
|
317
|
+
return path.join(resolveStateDir(env, homedir), "logs", CONFIG_AUDIT_LOG_FILENAME);
|
|
318
|
+
}
|
|
319
|
+
function resolveConfigWriteSuspiciousReasons(params) {
|
|
320
|
+
const reasons = [];
|
|
321
|
+
if (!params.existsBefore) {
|
|
322
|
+
return reasons;
|
|
323
|
+
}
|
|
324
|
+
if (typeof params.previousBytes === "number" &&
|
|
325
|
+
typeof params.nextBytes === "number" &&
|
|
326
|
+
params.previousBytes >= 512 &&
|
|
327
|
+
params.nextBytes < Math.floor(params.previousBytes * 0.5)) {
|
|
328
|
+
reasons.push(`size-drop:${params.previousBytes}->${params.nextBytes}`);
|
|
329
|
+
}
|
|
330
|
+
if (!params.hasMetaBefore) {
|
|
331
|
+
reasons.push("missing-meta-before-write");
|
|
332
|
+
}
|
|
333
|
+
if (params.gatewayModeBefore && !params.gatewayModeAfter) {
|
|
334
|
+
reasons.push("gateway-mode-removed");
|
|
335
|
+
}
|
|
336
|
+
return reasons;
|
|
337
|
+
}
|
|
338
|
+
async function appendConfigWriteAuditRecord(deps, record) {
|
|
339
|
+
try {
|
|
340
|
+
const auditPath = resolveConfigAuditLogPath(deps.env, deps.homedir);
|
|
341
|
+
await deps.fs.promises.mkdir(path.dirname(auditPath), { recursive: true, mode: 0o700 });
|
|
342
|
+
await deps.fs.promises.appendFile(auditPath, `${JSON.stringify(record)}\n`, {
|
|
343
|
+
encoding: "utf-8",
|
|
344
|
+
mode: 0o600,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
80
348
|
// best-effort
|
|
81
|
-
}
|
|
349
|
+
}
|
|
82
350
|
}
|
|
83
351
|
function warnOnConfigMiskeys(raw, logger) {
|
|
84
|
-
if (!raw || typeof raw !== "object")
|
|
352
|
+
if (!raw || typeof raw !== "object") {
|
|
85
353
|
return;
|
|
354
|
+
}
|
|
86
355
|
const gateway = raw.gateway;
|
|
87
|
-
if (!gateway || typeof gateway !== "object")
|
|
356
|
+
if (!gateway || typeof gateway !== "object") {
|
|
88
357
|
return;
|
|
358
|
+
}
|
|
89
359
|
if ("token" in gateway) {
|
|
90
360
|
logger.warn('Config uses "gateway.token". This key is ignored; use "gateway.auth.token" instead.');
|
|
91
361
|
}
|
|
@@ -103,26 +373,21 @@ function stampConfigVersion(cfg) {
|
|
|
103
373
|
}
|
|
104
374
|
function warnIfConfigFromFuture(cfg, logger) {
|
|
105
375
|
const touched = cfg.meta?.lastTouchedVersion;
|
|
106
|
-
if (!touched)
|
|
376
|
+
if (!touched) {
|
|
107
377
|
return;
|
|
378
|
+
}
|
|
108
379
|
const cmp = comparePoolbotVersions(VERSION, touched);
|
|
109
|
-
if (cmp === null)
|
|
380
|
+
if (cmp === null) {
|
|
110
381
|
return;
|
|
111
|
-
if (cmp < 0) {
|
|
112
|
-
logger.warn(`Config was last written by a newer Poolbot (${touched}); current version is ${VERSION}.`);
|
|
113
382
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const entries = collectConfigEnvVars(cfg);
|
|
117
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
118
|
-
if (env[key]?.trim())
|
|
119
|
-
continue;
|
|
120
|
-
env[key] = value;
|
|
383
|
+
if (cmp < 0) {
|
|
384
|
+
logger.warn(`Config was last written by a newer Pool Bot (${touched}); current version is ${VERSION}.`);
|
|
121
385
|
}
|
|
122
386
|
}
|
|
123
387
|
function resolveConfigPathForDeps(deps) {
|
|
124
|
-
if (deps.configPath)
|
|
388
|
+
if (deps.configPath) {
|
|
125
389
|
return deps.configPath;
|
|
390
|
+
}
|
|
126
391
|
return resolveConfigPath(deps.env, resolveStateDir(deps.env, deps.homedir));
|
|
127
392
|
}
|
|
128
393
|
function normalizeDeps(overrides = {}) {
|
|
@@ -130,11 +395,19 @@ function normalizeDeps(overrides = {}) {
|
|
|
130
395
|
fs: overrides.fs ?? fs,
|
|
131
396
|
json5: overrides.json5 ?? JSON5,
|
|
132
397
|
env: overrides.env ?? process.env,
|
|
133
|
-
homedir: overrides.homedir ?? os.homedir,
|
|
398
|
+
homedir: overrides.homedir ?? (() => resolveRequiredHomeDir(overrides.env ?? process.env, os.homedir)),
|
|
134
399
|
configPath: overrides.configPath ?? "",
|
|
135
400
|
logger: overrides.logger ?? console,
|
|
136
401
|
};
|
|
137
402
|
}
|
|
403
|
+
function maybeLoadDotEnvForConfig(env) {
|
|
404
|
+
// Only hydrate dotenv for the real process env. Callers using injected env
|
|
405
|
+
// objects (tests/diagnostics) should stay isolated.
|
|
406
|
+
if (env !== process.env) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
loadDotEnv({ quiet: true });
|
|
410
|
+
}
|
|
138
411
|
export function parseConfigJson5(raw, json5 = JSON5) {
|
|
139
412
|
try {
|
|
140
413
|
return { ok: true, parsed: json5.parse(raw) };
|
|
@@ -143,6 +416,23 @@ export function parseConfigJson5(raw, json5 = JSON5) {
|
|
|
143
416
|
return { ok: false, error: String(err) };
|
|
144
417
|
}
|
|
145
418
|
}
|
|
419
|
+
function resolveConfigIncludesForRead(parsed, configPath, deps) {
|
|
420
|
+
return resolveConfigIncludes(parsed, configPath, {
|
|
421
|
+
readFile: (candidate) => deps.fs.readFileSync(candidate, "utf-8"),
|
|
422
|
+
parseJson: (raw) => deps.json5.parse(raw),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function resolveConfigForRead(resolvedIncludes, env) {
|
|
426
|
+
// Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars.
|
|
427
|
+
if (resolvedIncludes && typeof resolvedIncludes === "object" && "env" in resolvedIncludes) {
|
|
428
|
+
applyConfigEnvVars(resolvedIncludes, env);
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
resolvedConfigRaw: resolveConfigEnvVars(resolvedIncludes, env),
|
|
432
|
+
// Capture env snapshot after substitution for write-time ${VAR} restoration.
|
|
433
|
+
envSnapshotForRestore: { ...env },
|
|
434
|
+
};
|
|
435
|
+
}
|
|
146
436
|
export function createConfigIO(overrides = {}) {
|
|
147
437
|
const deps = normalizeDeps(overrides);
|
|
148
438
|
const requestedConfigPath = resolveConfigPathForDeps(deps);
|
|
@@ -152,6 +442,7 @@ export function createConfigIO(overrides = {}) {
|
|
|
152
442
|
const configPath = candidatePaths.find((candidate) => deps.fs.existsSync(candidate)) ?? requestedConfigPath;
|
|
153
443
|
function loadConfig() {
|
|
154
444
|
try {
|
|
445
|
+
maybeLoadDotEnvForConfig(deps.env);
|
|
155
446
|
if (!deps.fs.existsSync(configPath)) {
|
|
156
447
|
if (shouldEnableShellEnvFallback(deps.env) && !shouldDeferShellEnvFallback(deps.env)) {
|
|
157
448
|
loadShellEnvFallback({
|
|
@@ -166,21 +457,11 @@ export function createConfigIO(overrides = {}) {
|
|
|
166
457
|
}
|
|
167
458
|
const raw = deps.fs.readFileSync(configPath, "utf-8");
|
|
168
459
|
const parsed = deps.json5.parse(raw);
|
|
169
|
-
|
|
170
|
-
const resolved = resolveConfigIncludes(parsed, configPath, {
|
|
171
|
-
readFile: (p) => deps.fs.readFileSync(p, "utf-8"),
|
|
172
|
-
parseJson: (raw) => deps.json5.parse(raw),
|
|
173
|
-
});
|
|
174
|
-
// Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars
|
|
175
|
-
if (resolved && typeof resolved === "object" && "env" in resolved) {
|
|
176
|
-
applyConfigEnv(resolved, deps.env);
|
|
177
|
-
}
|
|
178
|
-
// Substitute ${VAR} env var references
|
|
179
|
-
const substituted = resolveConfigEnvVars(resolved, deps.env);
|
|
180
|
-
const resolvedConfig = substituted;
|
|
460
|
+
const { resolvedConfigRaw: resolvedConfig } = resolveConfigForRead(resolveConfigIncludesForRead(parsed, configPath, deps), deps.env);
|
|
181
461
|
warnOnConfigMiskeys(resolvedConfig, deps.logger);
|
|
182
|
-
if (typeof resolvedConfig !== "object" || resolvedConfig === null)
|
|
462
|
+
if (typeof resolvedConfig !== "object" || resolvedConfig === null) {
|
|
183
463
|
return {};
|
|
464
|
+
}
|
|
184
465
|
const preValidationDuplicates = findDuplicateAgentDirs(resolvedConfig, {
|
|
185
466
|
env: deps.env,
|
|
186
467
|
homedir: deps.homedir,
|
|
@@ -218,7 +499,7 @@ export function createConfigIO(overrides = {}) {
|
|
|
218
499
|
if (duplicates.length > 0) {
|
|
219
500
|
throw new DuplicateAgentDirError(duplicates);
|
|
220
501
|
}
|
|
221
|
-
|
|
502
|
+
applyConfigEnvVars(cfg, deps.env);
|
|
222
503
|
const enabled = shouldEnableShellEnvFallback(deps.env) || cfg.env?.shellEnv?.enabled === true;
|
|
223
504
|
if (enabled && !shouldDeferShellEnvFallback(deps.env)) {
|
|
224
505
|
loadShellEnvFallback({
|
|
@@ -244,23 +525,27 @@ export function createConfigIO(overrides = {}) {
|
|
|
244
525
|
return {};
|
|
245
526
|
}
|
|
246
527
|
}
|
|
247
|
-
async function
|
|
528
|
+
async function readConfigFileSnapshotInternal() {
|
|
529
|
+
maybeLoadDotEnvForConfig(deps.env);
|
|
248
530
|
const exists = deps.fs.existsSync(configPath);
|
|
249
531
|
if (!exists) {
|
|
250
532
|
const hash = hashConfigRaw(null);
|
|
251
533
|
const config = applyTalkApiKey(applyModelDefaults(applyCompactionDefaults(applyContextPruningDefaults(applyAgentDefaults(applySessionDefaults(applyMessageDefaults({})))))));
|
|
252
534
|
const legacyIssues = [];
|
|
253
535
|
return {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
536
|
+
snapshot: {
|
|
537
|
+
path: configPath,
|
|
538
|
+
exists: false,
|
|
539
|
+
raw: null,
|
|
540
|
+
parsed: {},
|
|
541
|
+
resolved: {},
|
|
542
|
+
valid: true,
|
|
543
|
+
config,
|
|
544
|
+
hash,
|
|
545
|
+
issues: [],
|
|
546
|
+
warnings: [],
|
|
547
|
+
legacyIssues,
|
|
548
|
+
},
|
|
264
549
|
};
|
|
265
550
|
}
|
|
266
551
|
try {
|
|
@@ -269,118 +554,169 @@ export function createConfigIO(overrides = {}) {
|
|
|
269
554
|
const parsedRes = parseConfigJson5(raw, deps.json5);
|
|
270
555
|
if (!parsedRes.ok) {
|
|
271
556
|
return {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
557
|
+
snapshot: {
|
|
558
|
+
path: configPath,
|
|
559
|
+
exists: true,
|
|
560
|
+
raw,
|
|
561
|
+
parsed: {},
|
|
562
|
+
resolved: {},
|
|
563
|
+
valid: false,
|
|
564
|
+
config: {},
|
|
565
|
+
hash,
|
|
566
|
+
issues: [{ path: "", message: `JSON5 parse failed: ${parsedRes.error}` }],
|
|
567
|
+
warnings: [],
|
|
568
|
+
legacyIssues: [],
|
|
569
|
+
},
|
|
282
570
|
};
|
|
283
571
|
}
|
|
284
572
|
// Resolve $include directives
|
|
285
573
|
let resolved;
|
|
286
574
|
try {
|
|
287
|
-
resolved =
|
|
288
|
-
readFile: (p) => deps.fs.readFileSync(p, "utf-8"),
|
|
289
|
-
parseJson: (raw) => deps.json5.parse(raw),
|
|
290
|
-
});
|
|
575
|
+
resolved = resolveConfigIncludesForRead(parsedRes.parsed, configPath, deps);
|
|
291
576
|
}
|
|
292
577
|
catch (err) {
|
|
293
578
|
const message = err instanceof ConfigIncludeError
|
|
294
579
|
? err.message
|
|
295
580
|
: `Include resolution failed: ${String(err)}`;
|
|
296
581
|
return {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
582
|
+
snapshot: {
|
|
583
|
+
path: configPath,
|
|
584
|
+
exists: true,
|
|
585
|
+
raw,
|
|
586
|
+
parsed: parsedRes.parsed,
|
|
587
|
+
resolved: coerceConfig(parsedRes.parsed),
|
|
588
|
+
valid: false,
|
|
589
|
+
config: coerceConfig(parsedRes.parsed),
|
|
590
|
+
hash,
|
|
591
|
+
issues: [{ path: "", message }],
|
|
592
|
+
warnings: [],
|
|
593
|
+
legacyIssues: [],
|
|
594
|
+
},
|
|
307
595
|
};
|
|
308
596
|
}
|
|
309
|
-
|
|
310
|
-
if (resolved && typeof resolved === "object" && "env" in resolved) {
|
|
311
|
-
applyConfigEnv(resolved, deps.env);
|
|
312
|
-
}
|
|
313
|
-
// Substitute ${VAR} env var references
|
|
314
|
-
let substituted;
|
|
597
|
+
let readResolution;
|
|
315
598
|
try {
|
|
316
|
-
|
|
599
|
+
readResolution = resolveConfigForRead(resolved, deps.env);
|
|
317
600
|
}
|
|
318
601
|
catch (err) {
|
|
319
602
|
const message = err instanceof MissingEnvVarError
|
|
320
603
|
? err.message
|
|
321
604
|
: `Env var substitution failed: ${String(err)}`;
|
|
322
605
|
return {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
606
|
+
snapshot: {
|
|
607
|
+
path: configPath,
|
|
608
|
+
exists: true,
|
|
609
|
+
raw,
|
|
610
|
+
parsed: parsedRes.parsed,
|
|
611
|
+
resolved: coerceConfig(resolved),
|
|
612
|
+
valid: false,
|
|
613
|
+
config: coerceConfig(resolved),
|
|
614
|
+
hash,
|
|
615
|
+
issues: [{ path: "", message }],
|
|
616
|
+
warnings: [],
|
|
617
|
+
legacyIssues: [],
|
|
618
|
+
},
|
|
333
619
|
};
|
|
334
620
|
}
|
|
335
|
-
const resolvedConfigRaw =
|
|
621
|
+
const resolvedConfigRaw = readResolution.resolvedConfigRaw;
|
|
336
622
|
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw);
|
|
337
623
|
const validated = validateConfigObjectWithPlugins(resolvedConfigRaw);
|
|
338
624
|
if (!validated.ok) {
|
|
339
625
|
return {
|
|
626
|
+
snapshot: {
|
|
627
|
+
path: configPath,
|
|
628
|
+
exists: true,
|
|
629
|
+
raw,
|
|
630
|
+
parsed: parsedRes.parsed,
|
|
631
|
+
resolved: coerceConfig(resolvedConfigRaw),
|
|
632
|
+
valid: false,
|
|
633
|
+
config: coerceConfig(resolvedConfigRaw),
|
|
634
|
+
hash,
|
|
635
|
+
issues: validated.issues,
|
|
636
|
+
warnings: validated.warnings,
|
|
637
|
+
legacyIssues,
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
warnIfConfigFromFuture(validated.config, deps.logger);
|
|
642
|
+
return {
|
|
643
|
+
snapshot: {
|
|
340
644
|
path: configPath,
|
|
341
645
|
exists: true,
|
|
342
646
|
raw,
|
|
343
647
|
parsed: parsedRes.parsed,
|
|
344
|
-
|
|
345
|
-
config
|
|
648
|
+
// Use resolvedConfigRaw (after $include and ${ENV} substitution but BEFORE runtime defaults)
|
|
649
|
+
// for config set/unset operations (issue #6070)
|
|
650
|
+
resolved: coerceConfig(resolvedConfigRaw),
|
|
651
|
+
valid: true,
|
|
652
|
+
config: normalizeConfigPaths(applyTalkApiKey(applyModelDefaults(applyAgentDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config))))))),
|
|
346
653
|
hash,
|
|
347
|
-
issues:
|
|
654
|
+
issues: [],
|
|
348
655
|
warnings: validated.warnings,
|
|
349
656
|
legacyIssues,
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
warnIfConfigFromFuture(validated.config, deps.logger);
|
|
353
|
-
return {
|
|
354
|
-
path: configPath,
|
|
355
|
-
exists: true,
|
|
356
|
-
raw,
|
|
357
|
-
parsed: parsedRes.parsed,
|
|
358
|
-
valid: true,
|
|
359
|
-
config: normalizeConfigPaths(applyTalkApiKey(applyModelDefaults(applyAgentDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config))))))),
|
|
360
|
-
hash,
|
|
361
|
-
issues: [],
|
|
362
|
-
warnings: validated.warnings,
|
|
363
|
-
legacyIssues,
|
|
657
|
+
},
|
|
658
|
+
envSnapshotForRestore: readResolution.envSnapshotForRestore,
|
|
364
659
|
};
|
|
365
660
|
}
|
|
366
661
|
catch (err) {
|
|
367
662
|
return {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
663
|
+
snapshot: {
|
|
664
|
+
path: configPath,
|
|
665
|
+
exists: true,
|
|
666
|
+
raw: null,
|
|
667
|
+
parsed: {},
|
|
668
|
+
resolved: {},
|
|
669
|
+
valid: false,
|
|
670
|
+
config: {},
|
|
671
|
+
hash: hashConfigRaw(null),
|
|
672
|
+
issues: [{ path: "", message: `read failed: ${String(err)}` }],
|
|
673
|
+
warnings: [],
|
|
674
|
+
legacyIssues: [],
|
|
675
|
+
},
|
|
378
676
|
};
|
|
379
677
|
}
|
|
380
678
|
}
|
|
381
|
-
async function
|
|
679
|
+
async function readConfigFileSnapshot() {
|
|
680
|
+
const result = await readConfigFileSnapshotInternal();
|
|
681
|
+
return result.snapshot;
|
|
682
|
+
}
|
|
683
|
+
async function readConfigFileSnapshotForWrite() {
|
|
684
|
+
const result = await readConfigFileSnapshotInternal();
|
|
685
|
+
return {
|
|
686
|
+
snapshot: result.snapshot,
|
|
687
|
+
writeOptions: {
|
|
688
|
+
envSnapshotForRestore: result.envSnapshotForRestore,
|
|
689
|
+
expectedConfigPath: configPath,
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
async function writeConfigFile(cfg, options = {}) {
|
|
382
694
|
clearConfigCache();
|
|
383
|
-
|
|
695
|
+
let persistCandidate = cfg;
|
|
696
|
+
const { snapshot } = await readConfigFileSnapshotInternal();
|
|
697
|
+
let envRefMap = null;
|
|
698
|
+
let changedPaths = null;
|
|
699
|
+
if (snapshot.valid && snapshot.exists) {
|
|
700
|
+
const patch = createMergePatch(snapshot.config, cfg);
|
|
701
|
+
persistCandidate = applyMergePatch(snapshot.resolved, patch);
|
|
702
|
+
try {
|
|
703
|
+
const resolvedIncludes = resolveConfigIncludes(snapshot.parsed, configPath, {
|
|
704
|
+
readFile: (candidate) => deps.fs.readFileSync(candidate, "utf-8"),
|
|
705
|
+
parseJson: (raw) => deps.json5.parse(raw),
|
|
706
|
+
});
|
|
707
|
+
const collected = new Map();
|
|
708
|
+
collectEnvRefPaths(resolvedIncludes, "", collected);
|
|
709
|
+
if (collected.size > 0) {
|
|
710
|
+
envRefMap = collected;
|
|
711
|
+
changedPaths = new Set();
|
|
712
|
+
collectChangedPaths(snapshot.config, cfg, "", changedPaths);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
envRefMap = null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const validated = validateConfigObjectRawWithPlugins(persistCandidate);
|
|
384
720
|
if (!validated.ok) {
|
|
385
721
|
const issue = validated.issues[0];
|
|
386
722
|
const pathLabel = issue?.path ? issue.path : "<root>";
|
|
@@ -392,41 +728,179 @@ export function createConfigIO(overrides = {}) {
|
|
|
392
728
|
.join("\n");
|
|
393
729
|
deps.logger.warn(`Config warnings:\n${details}`);
|
|
394
730
|
}
|
|
731
|
+
// Restore ${VAR} env var references that were resolved during config loading.
|
|
732
|
+
// Read the current file (pre-substitution) and restore any references whose
|
|
733
|
+
// resolved values match the incoming config — so we don't overwrite
|
|
734
|
+
// "${ANTHROPIC_API_KEY}" with "sk-ant-..." when the caller didn't change it.
|
|
735
|
+
//
|
|
736
|
+
// We use only the root file's parsed content (no $include resolution) to avoid
|
|
737
|
+
// pulling values from included files into the root config on write-back.
|
|
738
|
+
// Apply env restoration to validated.config (which has runtime defaults stripped
|
|
739
|
+
// per issue #6070) rather than the raw caller input.
|
|
740
|
+
let cfgToWrite = validated.config;
|
|
741
|
+
try {
|
|
742
|
+
if (deps.fs.existsSync(configPath)) {
|
|
743
|
+
const currentRaw = await deps.fs.promises.readFile(configPath, "utf-8");
|
|
744
|
+
const parsedRes = parseConfigJson5(currentRaw, deps.json5);
|
|
745
|
+
if (parsedRes.ok) {
|
|
746
|
+
// Use env snapshot from when config was loaded (if available) to avoid
|
|
747
|
+
// TOCTOU issues where env changes between load and write. Falls back to
|
|
748
|
+
// live env if no snapshot exists (e.g., first write before any load).
|
|
749
|
+
const envForRestore = options.envSnapshotForRestore ?? deps.env;
|
|
750
|
+
cfgToWrite = restoreEnvVarRefs(cfgToWrite, parsedRes.parsed, envForRestore);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
// If reading the current file fails, write cfg as-is (no env restoration)
|
|
756
|
+
}
|
|
395
757
|
const dir = path.dirname(configPath);
|
|
396
758
|
await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
759
|
+
const outputConfig = envRefMap && changedPaths
|
|
760
|
+
? restoreEnvRefsFromMap(cfgToWrite, "", envRefMap, changedPaths)
|
|
761
|
+
: cfgToWrite;
|
|
762
|
+
if (options.unsetPaths?.length) {
|
|
763
|
+
for (const unsetPath of options.unsetPaths) {
|
|
764
|
+
if (!Array.isArray(unsetPath) || unsetPath.length === 0) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
unsetPathForWrite(outputConfig, unsetPath);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// Do NOT apply runtime defaults when writing — user config should only contain
|
|
771
|
+
// explicitly set values. Runtime defaults are applied when loading (issue #6070).
|
|
772
|
+
const stampedOutputConfig = stampConfigVersion(outputConfig);
|
|
773
|
+
const json = JSON.stringify(stampedOutputConfig, null, 2).trimEnd().concat("\n");
|
|
774
|
+
const nextHash = hashConfigRaw(json);
|
|
775
|
+
const previousHash = resolveConfigSnapshotHash(snapshot);
|
|
776
|
+
const changedPathCount = changedPaths?.size;
|
|
777
|
+
const previousBytes = typeof snapshot.raw === "string" ? Buffer.byteLength(snapshot.raw, "utf-8") : null;
|
|
778
|
+
const nextBytes = Buffer.byteLength(json, "utf-8");
|
|
779
|
+
const hasMetaBefore = hasConfigMeta(snapshot.parsed);
|
|
780
|
+
const hasMetaAfter = hasConfigMeta(stampedOutputConfig);
|
|
781
|
+
const gatewayModeBefore = resolveGatewayMode(snapshot.resolved);
|
|
782
|
+
const gatewayModeAfter = resolveGatewayMode(stampedOutputConfig);
|
|
783
|
+
const suspiciousReasons = resolveConfigWriteSuspiciousReasons({
|
|
784
|
+
existsBefore: snapshot.exists,
|
|
785
|
+
previousBytes,
|
|
786
|
+
nextBytes,
|
|
787
|
+
hasMetaBefore,
|
|
788
|
+
gatewayModeBefore,
|
|
789
|
+
gatewayModeAfter,
|
|
404
790
|
});
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
791
|
+
const logConfigOverwrite = () => {
|
|
792
|
+
if (!snapshot.exists) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const isVitest = deps.env.VITEST === "true";
|
|
796
|
+
const shouldLogInVitest = deps.env.POOLBOT_TEST_CONFIG_OVERWRITE_LOG === "1";
|
|
797
|
+
if (isVitest && !shouldLogInVitest) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const changeSummary = typeof changedPathCount === "number" ? `, changedPaths=${changedPathCount}` : "";
|
|
801
|
+
deps.logger.warn(`Config overwrite: ${configPath} (sha256 ${previousHash ?? "unknown"} -> ${nextHash}, backup=${configPath}.bak${changeSummary})`);
|
|
802
|
+
};
|
|
803
|
+
const logConfigWriteAnomalies = () => {
|
|
804
|
+
if (suspiciousReasons.length === 0) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
// Tests often write minimal configs (missing meta, etc); keep output quiet unless requested.
|
|
808
|
+
const isVitest = deps.env.VITEST === "true";
|
|
809
|
+
const shouldLogInVitest = deps.env.POOLBOT_TEST_CONFIG_WRITE_ANOMALY_LOG === "1";
|
|
810
|
+
if (isVitest && !shouldLogInVitest) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
deps.logger.warn(`Config write anomaly: ${configPath} (${suspiciousReasons.join(", ")})`);
|
|
814
|
+
};
|
|
815
|
+
const auditRecordBase = {
|
|
816
|
+
ts: new Date().toISOString(),
|
|
817
|
+
source: "config-io",
|
|
818
|
+
event: "config.write",
|
|
819
|
+
configPath,
|
|
820
|
+
pid: process.pid,
|
|
821
|
+
ppid: process.ppid,
|
|
822
|
+
cwd: process.cwd(),
|
|
823
|
+
argv: process.argv.slice(0, 8),
|
|
824
|
+
execArgv: process.execArgv.slice(0, 8),
|
|
825
|
+
watchMode: deps.env.POOLBOT_WATCH_MODE === "1",
|
|
826
|
+
watchSession: typeof deps.env.POOLBOT_WATCH_SESSION === "string" &&
|
|
827
|
+
deps.env.POOLBOT_WATCH_SESSION.trim().length > 0
|
|
828
|
+
? deps.env.POOLBOT_WATCH_SESSION.trim()
|
|
829
|
+
: null,
|
|
830
|
+
watchCommand: typeof deps.env.POOLBOT_WATCH_COMMAND === "string" &&
|
|
831
|
+
deps.env.POOLBOT_WATCH_COMMAND.trim().length > 0
|
|
832
|
+
? deps.env.POOLBOT_WATCH_COMMAND.trim()
|
|
833
|
+
: null,
|
|
834
|
+
existsBefore: snapshot.exists,
|
|
835
|
+
previousHash: previousHash ?? null,
|
|
836
|
+
nextHash,
|
|
837
|
+
previousBytes,
|
|
838
|
+
nextBytes,
|
|
839
|
+
changedPathCount: typeof changedPathCount === "number" ? changedPathCount : null,
|
|
840
|
+
hasMetaBefore,
|
|
841
|
+
hasMetaAfter,
|
|
842
|
+
gatewayModeBefore,
|
|
843
|
+
gatewayModeAfter,
|
|
844
|
+
suspicious: suspiciousReasons,
|
|
845
|
+
};
|
|
846
|
+
const appendWriteAudit = async (result, err) => {
|
|
847
|
+
const errorCode = err && typeof err === "object" && "code" in err && typeof err.code === "string"
|
|
848
|
+
? err.code
|
|
849
|
+
: undefined;
|
|
850
|
+
const errorMessage = err && typeof err === "object" && "message" in err && typeof err.message === "string"
|
|
851
|
+
? err.message
|
|
852
|
+
: undefined;
|
|
853
|
+
await appendConfigWriteAuditRecord(deps, {
|
|
854
|
+
...auditRecordBase,
|
|
855
|
+
result,
|
|
856
|
+
nextHash: result === "failed" ? null : auditRecordBase.nextHash,
|
|
857
|
+
nextBytes: result === "failed" ? null : auditRecordBase.nextBytes,
|
|
858
|
+
errorCode,
|
|
859
|
+
errorMessage,
|
|
409
860
|
});
|
|
410
|
-
}
|
|
861
|
+
};
|
|
862
|
+
const tmp = path.join(dir, `${path.basename(configPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
411
863
|
try {
|
|
412
|
-
await deps.fs.promises.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
await deps.fs.promises.copyFile(
|
|
419
|
-
await deps.fs.promises.chmod(configPath, 0o600).catch(() => {
|
|
864
|
+
await deps.fs.promises.writeFile(tmp, json, {
|
|
865
|
+
encoding: "utf-8",
|
|
866
|
+
mode: 0o600,
|
|
867
|
+
});
|
|
868
|
+
if (deps.fs.existsSync(configPath)) {
|
|
869
|
+
await rotateConfigBackups(configPath, deps.fs.promises);
|
|
870
|
+
await deps.fs.promises.copyFile(configPath, `${configPath}.bak`).catch(() => {
|
|
420
871
|
// best-effort
|
|
421
872
|
});
|
|
873
|
+
}
|
|
874
|
+
try {
|
|
875
|
+
await deps.fs.promises.rename(tmp, configPath);
|
|
876
|
+
}
|
|
877
|
+
catch (err) {
|
|
878
|
+
const code = err.code;
|
|
879
|
+
// Windows doesn't reliably support atomic replace via rename when dest exists.
|
|
880
|
+
if (code === "EPERM" || code === "EEXIST") {
|
|
881
|
+
await deps.fs.promises.copyFile(tmp, configPath);
|
|
882
|
+
await deps.fs.promises.chmod(configPath, 0o600).catch(() => {
|
|
883
|
+
// best-effort
|
|
884
|
+
});
|
|
885
|
+
await deps.fs.promises.unlink(tmp).catch(() => {
|
|
886
|
+
// best-effort
|
|
887
|
+
});
|
|
888
|
+
logConfigOverwrite();
|
|
889
|
+
logConfigWriteAnomalies();
|
|
890
|
+
await appendWriteAudit("copy-fallback");
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
422
893
|
await deps.fs.promises.unlink(tmp).catch(() => {
|
|
423
894
|
// best-effort
|
|
424
895
|
});
|
|
425
|
-
|
|
896
|
+
throw err;
|
|
426
897
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
898
|
+
logConfigOverwrite();
|
|
899
|
+
logConfigWriteAnomalies();
|
|
900
|
+
await appendWriteAudit("rename");
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
await appendWriteAudit("failed", err);
|
|
430
904
|
throw err;
|
|
431
905
|
}
|
|
432
906
|
}
|
|
@@ -434,35 +908,41 @@ export function createConfigIO(overrides = {}) {
|
|
|
434
908
|
configPath,
|
|
435
909
|
loadConfig,
|
|
436
910
|
readConfigFileSnapshot,
|
|
911
|
+
readConfigFileSnapshotForWrite,
|
|
437
912
|
writeConfigFile,
|
|
438
913
|
};
|
|
439
914
|
}
|
|
440
915
|
// NOTE: These wrappers intentionally do *not* cache the resolved config path at
|
|
441
|
-
// module scope. `POOLBOT_CONFIG_PATH`
|
|
916
|
+
// module scope. `POOLBOT_CONFIG_PATH` (and friends) are expected to work even
|
|
442
917
|
// when set after the module has been imported (tests, one-off scripts, etc.).
|
|
443
918
|
const DEFAULT_CONFIG_CACHE_MS = 200;
|
|
444
919
|
let configCache = null;
|
|
445
920
|
function resolveConfigCacheMs(env) {
|
|
446
|
-
const raw = env.POOLBOT_CONFIG_CACHE_MS?.trim()
|
|
447
|
-
if (raw === "" || raw === "0")
|
|
921
|
+
const raw = env.POOLBOT_CONFIG_CACHE_MS?.trim();
|
|
922
|
+
if (raw === "" || raw === "0") {
|
|
448
923
|
return 0;
|
|
449
|
-
|
|
924
|
+
}
|
|
925
|
+
if (!raw) {
|
|
450
926
|
return DEFAULT_CONFIG_CACHE_MS;
|
|
927
|
+
}
|
|
451
928
|
const parsed = Number.parseInt(raw, 10);
|
|
452
|
-
if (!Number.isFinite(parsed))
|
|
929
|
+
if (!Number.isFinite(parsed)) {
|
|
453
930
|
return DEFAULT_CONFIG_CACHE_MS;
|
|
931
|
+
}
|
|
454
932
|
return Math.max(0, parsed);
|
|
455
933
|
}
|
|
456
934
|
function shouldUseConfigCache(env) {
|
|
457
|
-
if (env.POOLBOT_DISABLE_CONFIG_CACHE?.trim()
|
|
935
|
+
if (env.POOLBOT_DISABLE_CONFIG_CACHE?.trim()) {
|
|
458
936
|
return false;
|
|
937
|
+
}
|
|
459
938
|
return resolveConfigCacheMs(env) > 0;
|
|
460
939
|
}
|
|
461
|
-
function clearConfigCache() {
|
|
940
|
+
export function clearConfigCache() {
|
|
462
941
|
configCache = null;
|
|
463
942
|
}
|
|
464
943
|
export function loadConfig() {
|
|
465
|
-
const
|
|
944
|
+
const io = createConfigIO();
|
|
945
|
+
const configPath = io.configPath;
|
|
466
946
|
const now = Date.now();
|
|
467
947
|
if (shouldUseConfigCache(process.env)) {
|
|
468
948
|
const cached = configCache;
|
|
@@ -470,7 +950,7 @@ export function loadConfig() {
|
|
|
470
950
|
return cached.config;
|
|
471
951
|
}
|
|
472
952
|
}
|
|
473
|
-
const config =
|
|
953
|
+
const config = io.loadConfig();
|
|
474
954
|
if (shouldUseConfigCache(process.env)) {
|
|
475
955
|
const cacheMs = resolveConfigCacheMs(process.env);
|
|
476
956
|
if (cacheMs > 0) {
|
|
@@ -484,11 +964,16 @@ export function loadConfig() {
|
|
|
484
964
|
return config;
|
|
485
965
|
}
|
|
486
966
|
export async function readConfigFileSnapshot() {
|
|
487
|
-
return await createConfigIO(
|
|
488
|
-
|
|
489
|
-
|
|
967
|
+
return await createConfigIO().readConfigFileSnapshot();
|
|
968
|
+
}
|
|
969
|
+
export async function readConfigFileSnapshotForWrite() {
|
|
970
|
+
return await createConfigIO().readConfigFileSnapshotForWrite();
|
|
490
971
|
}
|
|
491
|
-
export async function writeConfigFile(cfg) {
|
|
492
|
-
|
|
493
|
-
|
|
972
|
+
export async function writeConfigFile(cfg, options = {}) {
|
|
973
|
+
const io = createConfigIO();
|
|
974
|
+
const sameConfigPath = options.expectedConfigPath === undefined || options.expectedConfigPath === io.configPath;
|
|
975
|
+
await io.writeConfigFile(cfg, {
|
|
976
|
+
envSnapshotForRestore: sameConfigPath ? options.envSnapshotForRestore : undefined,
|
|
977
|
+
unsetPaths: options.unsetPaths,
|
|
978
|
+
});
|
|
494
979
|
}
|