@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.
Files changed (646) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/event-mapper.js +87 -22
  4. package/dist/acp/meta.js +12 -6
  5. package/dist/acp/secret-file.js +22 -0
  6. package/dist/agents/agent-paths.js +8 -9
  7. package/dist/agents/agent-scope.js +17 -5
  8. package/dist/agents/auth-profiles/oauth.js +148 -64
  9. package/dist/agents/auth-profiles/session-override.js +13 -7
  10. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  11. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  12. package/dist/agents/bash-tools.exec-host-gateway.js +240 -0
  13. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  14. package/dist/agents/bash-tools.exec-runtime.js +2 -25
  15. package/dist/agents/bash-tools.exec-types.js +1 -0
  16. package/dist/agents/bash-tools.process.js +224 -218
  17. package/dist/agents/bedrock-discovery.js +3 -1
  18. package/dist/agents/byteplus-models.js +97 -0
  19. package/dist/agents/chutes-oauth.js +1 -0
  20. package/dist/agents/cli-runner/helpers.js +4 -0
  21. package/dist/agents/compaction.js +41 -14
  22. package/dist/agents/content-blocks.js +16 -0
  23. package/dist/agents/doubao-models.js +121 -0
  24. package/dist/agents/failover-error.js +2 -0
  25. package/dist/agents/huggingface-models.js +5 -3
  26. package/dist/agents/live-model-filter.js +5 -0
  27. package/dist/agents/minimax-vlm.js +10 -8
  28. package/dist/agents/model-auth.js +6 -0
  29. package/dist/agents/model-catalog.js +3 -1
  30. package/dist/agents/model-fallback.js +96 -101
  31. package/dist/agents/model-selection.js +7 -1
  32. package/dist/agents/models-config.providers.js +364 -165
  33. package/dist/agents/ollama-stream.js +117 -4
  34. package/dist/agents/opencode-zen-models.js +22 -11
  35. package/dist/agents/pi-embedded-helpers/errors.js +55 -33
  36. package/dist/agents/pi-embedded-helpers/messaging-dedupe.js +10 -5
  37. package/dist/agents/pi-embedded-helpers/thinking.js +10 -5
  38. package/dist/agents/pi-embedded-helpers.js +1 -1
  39. package/dist/agents/pi-embedded-payloads.js +1 -0
  40. package/dist/agents/pi-embedded-runner/compact.js +29 -7
  41. package/dist/agents/pi-embedded-runner/extensions.js +28 -26
  42. package/dist/agents/pi-embedded-runner/google.js +20 -8
  43. package/dist/agents/pi-embedded-runner/run/attempt.js +95 -36
  44. package/dist/agents/pi-embedded-runner/run.js +71 -12
  45. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  46. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +11 -2
  47. package/dist/agents/pi-embedded-runner/session-manager-cache.js +11 -7
  48. package/dist/agents/pi-embedded-runner/system-prompt.js +2 -0
  49. package/dist/agents/pi-embedded-runner/thinking.js +42 -0
  50. package/dist/agents/pi-embedded-runner/tool-name-allowlist.js +19 -0
  51. package/dist/agents/pi-embedded-runner/utils.js +7 -10
  52. package/dist/agents/pi-embedded-subscribe.handlers.lifecycle.js +45 -56
  53. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +2 -2
  54. package/dist/agents/pi-embedded-subscribe.js +9 -4
  55. package/dist/agents/pi-embedded-subscribe.tools.js +68 -14
  56. package/dist/agents/pi-embedded-utils.js +3 -0
  57. package/dist/agents/pi-extensions/compaction-safeguard-runtime.js +4 -20
  58. package/dist/agents/pi-extensions/compaction-safeguard.js +75 -33
  59. package/dist/agents/pi-settings.js +40 -0
  60. package/dist/agents/pi-tools.policy.js +2 -1
  61. package/dist/agents/provider/config-loader.js +1 -1
  62. package/dist/agents/sandbox/browser.js +170 -33
  63. package/dist/agents/sandbox/config-hash.js +14 -27
  64. package/dist/agents/sandbox/config.js +21 -2
  65. package/dist/agents/sandbox/constants.js +2 -0
  66. package/dist/agents/sandbox/docker.js +16 -2
  67. package/dist/agents/sandbox/novnc-auth.js +62 -0
  68. package/dist/agents/sandbox/sanitize-env-vars.js +1 -1
  69. package/dist/agents/sandbox/shared.js +10 -6
  70. package/dist/agents/sandbox-paths.js +24 -11
  71. package/dist/agents/schema/clean-for-gemini.js +132 -85
  72. package/dist/agents/session-slug.js +10 -5
  73. package/dist/agents/session-tool-result-guard-wrapper.js +1 -0
  74. package/dist/agents/session-tool-result-guard.js +3 -1
  75. package/dist/agents/session-transcript-repair.js +40 -6
  76. package/dist/agents/skills/bundled-dir.js +19 -5
  77. package/dist/agents/skills/env-overrides.js +124 -43
  78. package/dist/agents/skills/frontmatter.js +6 -6
  79. package/dist/agents/skills/plugin-skills.js +14 -7
  80. package/dist/agents/skills/workspace.js +1 -0
  81. package/dist/agents/skills.test-helpers.js +13 -0
  82. package/dist/agents/stable-stringify.js +12 -0
  83. package/dist/agents/subagent-announce.js +251 -49
  84. package/dist/agents/subagent-lifecycle-events.js +19 -0
  85. package/dist/agents/subagent-registry-cleanup.js +31 -0
  86. package/dist/agents/subagent-registry-completion.js +68 -0
  87. package/dist/agents/subagent-registry-queries.js +117 -0
  88. package/dist/agents/subagent-registry-state.js +46 -0
  89. package/dist/agents/subagent-registry.js +252 -221
  90. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  91. package/dist/agents/subagent-registry.store.js +1 -0
  92. package/dist/agents/subagent-registry.types.js +1 -0
  93. package/dist/agents/subagent-spawn.js +195 -7
  94. package/dist/agents/system-prompt.js +22 -6
  95. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  96. package/dist/agents/test-helpers/fast-coding-tools.js +1 -18
  97. package/dist/agents/test-helpers/fast-core-tools.js +1 -17
  98. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  99. package/dist/agents/timeout.js +18 -6
  100. package/dist/agents/tool-call-id.js +1 -1
  101. package/dist/agents/tool-display-common.js +162 -29
  102. package/dist/agents/tool-images.js +82 -9
  103. package/dist/agents/tool-policy-shared.js +108 -0
  104. package/dist/agents/tool-policy.js +51 -26
  105. package/dist/agents/tools/browser-tool.js +160 -54
  106. package/dist/agents/tools/canvas-tool.js +27 -1
  107. package/dist/agents/tools/common.js +45 -0
  108. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  109. package/dist/agents/tools/discord-actions-guild.js +4 -1
  110. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  111. package/dist/agents/tools/gateway-tool.js +3 -1
  112. package/dist/agents/tools/image-tool.js +214 -99
  113. package/dist/agents/tools/nodes-utils.js +1 -10
  114. package/dist/agents/tools/sessions-history-tool.js +140 -108
  115. package/dist/agents/tools/sessions-send-helpers.js +12 -6
  116. package/dist/agents/tools/sessions-spawn-tool.js +8 -2
  117. package/dist/agents/tools/subagents-tool.js +2 -1
  118. package/dist/agents/tools/whatsapp-actions.js +10 -2
  119. package/dist/agents/tools/whatsapp-target-auth.js +18 -0
  120. package/dist/agents/transcript-policy.js +22 -8
  121. package/dist/agents/venice-models.js +11 -3
  122. package/dist/agents/workspace.js +222 -46
  123. package/dist/auto-reply/commands-registry.data.js +51 -0
  124. package/dist/auto-reply/commands-registry.js +19 -21
  125. package/dist/auto-reply/fallback-state.js +114 -0
  126. package/dist/auto-reply/group-activation.js +10 -5
  127. package/dist/auto-reply/inbound-debounce.js +10 -5
  128. package/dist/auto-reply/model-runtime.js +68 -0
  129. package/dist/auto-reply/reply/abort.js +1 -1
  130. package/dist/auto-reply/reply/agent-runner-execution.js +40 -5
  131. package/dist/auto-reply/reply/agent-runner.js +165 -39
  132. package/dist/auto-reply/reply/bash-command.js +41 -39
  133. package/dist/auto-reply/reply/command-gates.js +25 -0
  134. package/dist/auto-reply/reply/commands-allowlist.js +111 -72
  135. package/dist/auto-reply/reply/commands-bash.js +6 -5
  136. package/dist/auto-reply/reply/commands-config.js +30 -28
  137. package/dist/auto-reply/reply/commands-core.js +2 -1
  138. package/dist/auto-reply/reply/commands-info.js +1 -0
  139. package/dist/auto-reply/reply/commands-models.js +65 -14
  140. package/dist/auto-reply/reply/commands-session.js +237 -82
  141. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  142. package/dist/auto-reply/reply/commands-setunset.js +45 -0
  143. package/dist/auto-reply/reply/commands-subagents/action-agents.js +44 -0
  144. package/dist/auto-reply/reply/commands-subagents/action-focus.js +64 -0
  145. package/dist/auto-reply/reply/commands-subagents/action-help.js +4 -0
  146. package/dist/auto-reply/reply/commands-subagents/action-info.js +45 -0
  147. package/dist/auto-reply/reply/commands-subagents/action-kill.js +60 -0
  148. package/dist/auto-reply/reply/commands-subagents/action-list.js +44 -0
  149. package/dist/auto-reply/reply/commands-subagents/action-log.js +29 -0
  150. package/dist/auto-reply/reply/commands-subagents/action-send.js +119 -0
  151. package/dist/auto-reply/reply/commands-subagents/action-spawn.js +52 -0
  152. package/dist/auto-reply/reply/commands-subagents/action-unfocus.js +30 -0
  153. package/dist/auto-reply/reply/commands-subagents/shared.js +303 -0
  154. package/dist/auto-reply/reply/commands-subagents.js +51 -587
  155. package/dist/auto-reply/reply/commands-tts.js +10 -5
  156. package/dist/auto-reply/reply/config-value.js +10 -5
  157. package/dist/auto-reply/reply/directive-handling.model-picker.js +12 -6
  158. package/dist/auto-reply/reply/directive-handling.persist.js +9 -21
  159. package/dist/auto-reply/reply/directive-handling.shared.js +24 -4
  160. package/dist/auto-reply/reply/followup-runner.js +1 -0
  161. package/dist/auto-reply/reply/get-reply-directives-utils.js +23 -14
  162. package/dist/auto-reply/reply/get-reply-directives.js +17 -28
  163. package/dist/auto-reply/reply/get-reply-inline-actions.js +1 -0
  164. package/dist/auto-reply/reply/get-reply.js +71 -12
  165. package/dist/auto-reply/reply/model-selection.js +80 -39
  166. package/dist/auto-reply/reply/queue/enqueue.js +10 -5
  167. package/dist/auto-reply/reply/queue/state.js +13 -12
  168. package/dist/auto-reply/reply/reply-payloads.js +67 -36
  169. package/dist/auto-reply/reply/reply-reference.js +9 -8
  170. package/dist/auto-reply/reply/route-reply.js +15 -8
  171. package/dist/auto-reply/reply/session-reset-prompt.js +1 -1
  172. package/dist/auto-reply/reply/session.js +22 -6
  173. package/dist/auto-reply/reply/strip-inbound-meta.js +147 -0
  174. package/dist/auto-reply/reply/subagents-utils.js +56 -30
  175. package/dist/auto-reply/reply/typing.js +46 -21
  176. package/dist/auto-reply/send-policy.js +14 -7
  177. package/dist/auto-reply/status.js +140 -16
  178. package/dist/auto-reply/templating.js +10 -5
  179. package/dist/auto-reply/thinking.js +7 -16
  180. package/dist/auto-reply/tokens.js +21 -5
  181. package/dist/browser/bridge-server.js +36 -20
  182. package/dist/browser/cdp.helpers.js +7 -14
  183. package/dist/browser/cdp.js +35 -15
  184. package/dist/browser/chrome.profile-decoration.js +7 -4
  185. package/dist/browser/config.js +30 -0
  186. package/dist/browser/extension-relay-auth.js +55 -0
  187. package/dist/browser/extension-relay.js +74 -29
  188. package/dist/browser/navigation-guard.js +39 -0
  189. package/dist/browser/paths.js +77 -0
  190. package/dist/browser/profiles.js +13 -8
  191. package/dist/browser/pw-ai-module.js +10 -5
  192. package/dist/browser/pw-session.js +76 -39
  193. package/dist/browser/pw-tools-core.interactions.js +14 -7
  194. package/dist/browser/pw-tools-core.state.js +12 -6
  195. package/dist/browser/routes/agent.act.js +431 -424
  196. package/dist/browser/routes/agent.shared.js +47 -3
  197. package/dist/browser/routes/agent.snapshot.js +122 -116
  198. package/dist/browser/routes/agent.storage.js +303 -297
  199. package/dist/browser/routes/tabs.js +154 -100
  200. package/dist/browser/server-context.js +7 -0
  201. package/dist/browser/server-lifecycle.js +37 -0
  202. package/dist/build-info.json +3 -3
  203. package/dist/channels/allow-from.js +26 -0
  204. package/dist/channels/allowlists/resolve-utils.js +43 -19
  205. package/dist/channels/channel-config.js +14 -7
  206. package/dist/channels/draft-stream-loop.js +7 -0
  207. package/dist/channels/model-overrides.js +82 -0
  208. package/dist/channels/plugins/account-action-gate.js +13 -0
  209. package/dist/channels/plugins/message-actions.js +10 -0
  210. package/dist/channels/plugins/normalize/imessage.js +14 -7
  211. package/dist/channels/plugins/normalize/slack.js +10 -5
  212. package/dist/channels/plugins/normalize/telegram.js +14 -7
  213. package/dist/channels/plugins/outbound/discord.js +80 -8
  214. package/dist/channels/plugins/outbound/signal.js +11 -11
  215. package/dist/channels/plugins/setup-helpers.js +10 -5
  216. package/dist/channels/sender-label.js +14 -7
  217. package/dist/channels/session.js +4 -2
  218. package/dist/channels/status-reactions.js +297 -0
  219. package/dist/channels/telegram/api.js +18 -0
  220. package/dist/cli/argv.js +84 -21
  221. package/dist/cli/banner.js +3 -2
  222. package/dist/cli/browser-cli-actions-input/register.files-downloads.js +65 -56
  223. package/dist/cli/cli-name.js +11 -11
  224. package/dist/cli/cli-utils.js +13 -3
  225. package/dist/cli/command-format.js +1 -1
  226. package/dist/cli/config-cli.js +1 -1
  227. package/dist/cli/daemon-cli/lifecycle-core.js +31 -19
  228. package/dist/cli/daemon-cli/lifecycle.js +64 -2
  229. package/dist/cli/daemon-cli/restart-health.js +126 -0
  230. package/dist/cli/daemon-cli/status.gather.js +9 -13
  231. package/dist/cli/daemon-cli/status.print.js +2 -10
  232. package/dist/cli/deps.js +27 -22
  233. package/dist/cli/exec-approvals-cli.js +92 -124
  234. package/dist/cli/gateway-cli/run-loop.js +23 -5
  235. package/dist/cli/memory-cli.js +158 -61
  236. package/dist/cli/node-cli/register.js +14 -5
  237. package/dist/cli/nodes-cli/register.push.js +63 -0
  238. package/dist/cli/nodes-media-utils.js +26 -0
  239. package/dist/cli/outbound-send-deps.js +2 -9
  240. package/dist/cli/outbound-send-mapping.js +11 -0
  241. package/dist/cli/pairing-cli.js +40 -14
  242. package/dist/cli/plugins-cli.js +250 -73
  243. package/dist/cli/ports.js +11 -10
  244. package/dist/cli/program/build-program.js +3 -1
  245. package/dist/cli/program/command-registry.js +214 -136
  246. package/dist/cli/program/command-tree.js +16 -0
  247. package/dist/cli/program/help.js +43 -12
  248. package/dist/cli/program/preaction.js +13 -9
  249. package/dist/cli/program/register.configure.js +3 -18
  250. package/dist/cli/program/register.maintenance.js +2 -2
  251. package/dist/cli/program/register.onboard.js +2 -0
  252. package/dist/cli/program/register.status-health-sessions.js +16 -17
  253. package/dist/cli/program/register.subclis.js +93 -52
  254. package/dist/cli/route.js +12 -8
  255. package/dist/cli/system-cli.js +36 -46
  256. package/dist/cli/test-runtime-capture.js +24 -0
  257. package/dist/cli/update-cli/shared.js +22 -9
  258. package/dist/cli/update-cli/update-command.js +89 -14
  259. package/dist/cli/update-cli/wizard.js +6 -12
  260. package/dist/commands/agent/run-context.js +18 -5
  261. package/dist/commands/agent/session-store.js +17 -4
  262. package/dist/commands/agent.js +185 -89
  263. package/dist/commands/agents.bindings.js +14 -7
  264. package/dist/commands/agents.commands.add.js +13 -9
  265. package/dist/commands/agents.commands.identity.js +12 -6
  266. package/dist/commands/agents.commands.list.js +11 -6
  267. package/dist/commands/agents.config.js +8 -10
  268. package/dist/commands/agents.providers.js +12 -6
  269. package/dist/commands/auth-choice-options.js +103 -75
  270. package/dist/commands/auth-choice.apply.byteplus.js +55 -0
  271. package/dist/commands/auth-choice.apply.js +4 -0
  272. package/dist/commands/auth-choice.apply.minimax.js +61 -13
  273. package/dist/commands/auth-choice.apply.openai.js +3 -1
  274. package/dist/commands/auth-choice.apply.volcengine.js +55 -0
  275. package/dist/commands/auth-choice.preferred-provider.js +2 -0
  276. package/dist/commands/channels/remove.js +13 -6
  277. package/dist/commands/channels/shared.js +4 -14
  278. package/dist/commands/channels.mock-harness.js +23 -0
  279. package/dist/commands/configure.commands.js +14 -0
  280. package/dist/commands/configure.gateway.js +2 -4
  281. package/dist/commands/configure.js +1 -1
  282. package/dist/commands/configure.shared.js +11 -0
  283. package/dist/commands/daemon-install-helpers.js +2 -2
  284. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  285. package/dist/commands/dashboard.js +12 -10
  286. package/dist/commands/docs.js +14 -8
  287. package/dist/commands/doctor-config-flow.js +11 -9
  288. package/dist/commands/doctor-legacy-config.js +281 -0
  289. package/dist/commands/doctor-state-integrity.js +99 -23
  290. package/dist/commands/doctor-update.js +12 -9
  291. package/dist/commands/models/list.list-command.js +7 -5
  292. package/dist/commands/models/set-image.js +2 -21
  293. package/dist/commands/node-daemon-install-helpers.js +10 -8
  294. package/dist/commands/onboard-auth.config-minimax.js +54 -80
  295. package/dist/commands/onboard-auth.config-opencode.js +2 -18
  296. package/dist/commands/onboard-auth.credentials.js +90 -13
  297. package/dist/commands/onboard-auth.js +1 -1
  298. package/dist/commands/onboard-auth.models.js +6 -5
  299. package/dist/commands/onboard-hooks.js +1 -1
  300. package/dist/commands/onboard-non-interactive/api-keys.js +14 -7
  301. package/dist/commands/onboard-non-interactive/local/auth-choice.js +64 -49
  302. package/dist/commands/onboard-provider-auth-flags.js +14 -0
  303. package/dist/commands/onboard-remote.js +14 -7
  304. package/dist/commands/onboard.js +11 -13
  305. package/dist/commands/sandbox-display.js +6 -5
  306. package/dist/commands/sessions.test-helpers.js +61 -0
  307. package/dist/commands/status-all/diagnosis.js +14 -10
  308. package/dist/commands/status-all/format.js +1 -0
  309. package/dist/commands/status.gateway-probe.js +1 -16
  310. package/dist/commands/systemd-linger.js +12 -6
  311. package/dist/config/agent-limits.js +2 -0
  312. package/dist/config/commands.js +32 -15
  313. package/dist/config/config-paths.js +9 -11
  314. package/dist/config/config.js +1 -1
  315. package/dist/config/defaults.js +22 -2
  316. package/dist/config/discord-preview-streaming.js +104 -0
  317. package/dist/config/env-substitution.js +62 -34
  318. package/dist/config/env-vars.js +45 -7
  319. package/dist/config/includes.js +4 -0
  320. package/dist/config/io.js +656 -171
  321. package/dist/config/legacy.migrations.part-1.js +189 -78
  322. package/dist/config/legacy.shared.js +3 -1
  323. package/dist/config/merge-patch.js +54 -4
  324. package/dist/config/prototype-keys.js +4 -0
  325. package/dist/config/redact-snapshot.js +404 -76
  326. package/dist/config/schema.help.js +44 -7
  327. package/dist/config/schema.js +58 -570
  328. package/dist/config/schema.labels.js +38 -6
  329. package/dist/config/sessions/delivery-info.js +10 -3
  330. package/dist/config/sessions/main-session.js +10 -5
  331. package/dist/config/sessions/session-file.js +33 -0
  332. package/dist/config/sessions/session-key.js +10 -5
  333. package/dist/config/sessions/store.js +1 -1
  334. package/dist/config/sessions.js +1 -0
  335. package/dist/config/validation.js +140 -85
  336. package/dist/config/zod-schema.agent-runtime.js +11 -0
  337. package/dist/config/zod-schema.hooks.js +40 -11
  338. package/dist/config/zod-schema.installs.js +20 -0
  339. package/dist/config/zod-schema.js +156 -20
  340. package/dist/config/zod-schema.providers-core.js +78 -4
  341. package/dist/config/zod-schema.providers.js +6 -1
  342. package/dist/config/zod-schema.session.js +41 -2
  343. package/dist/cron/run-log.js +3 -0
  344. package/dist/cron/schedule.js +21 -10
  345. package/dist/cron/service/ops.js +35 -21
  346. package/dist/cron/service/timer.js +116 -16
  347. package/dist/cron/stagger.js +3 -1
  348. package/dist/daemon/cmd-argv.js +21 -0
  349. package/dist/daemon/cmd-set.js +58 -0
  350. package/dist/daemon/service-types.js +1 -0
  351. package/dist/discord/api.js +12 -6
  352. package/dist/discord/draft-chunking.js +22 -0
  353. package/dist/discord/draft-stream.js +124 -0
  354. package/dist/discord/monitor/agent-components.js +1 -1
  355. package/dist/discord/monitor/commands.js +5 -0
  356. package/dist/discord/monitor/exec-approvals.js +357 -162
  357. package/dist/discord/monitor/gateway-plugin.js +2 -1
  358. package/dist/discord/monitor/listeners.js +37 -27
  359. package/dist/discord/monitor/message-handler.js +4 -1
  360. package/dist/discord/monitor/message-handler.preflight.js +65 -8
  361. package/dist/discord/monitor/message-handler.process.js +246 -217
  362. package/dist/discord/monitor/message-utils.js +143 -6
  363. package/dist/discord/monitor/model-picker-preferences.js +143 -0
  364. package/dist/discord/monitor/model-picker.js +651 -0
  365. package/dist/discord/monitor/native-command.js +573 -16
  366. package/dist/discord/monitor/provider.allowlist.js +223 -0
  367. package/dist/discord/monitor/provider.js +275 -347
  368. package/dist/discord/monitor/provider.lifecycle.js +100 -0
  369. package/dist/discord/monitor/reply-delivery.js +123 -16
  370. package/dist/discord/monitor/thread-bindings.discord-api.js +215 -0
  371. package/dist/discord/monitor/thread-bindings.js +4 -0
  372. package/dist/discord/monitor/thread-bindings.lifecycle.js +177 -0
  373. package/dist/discord/monitor/thread-bindings.manager.js +423 -0
  374. package/dist/discord/monitor/thread-bindings.messages.js +55 -0
  375. package/dist/discord/monitor/thread-bindings.state.js +358 -0
  376. package/dist/discord/monitor/thread-bindings.types.js +6 -0
  377. package/dist/discord/resolve-users.js +33 -21
  378. package/dist/discord/send.channels.js +15 -0
  379. package/dist/discord/send.js +3 -2
  380. package/dist/discord/send.outbound.js +82 -26
  381. package/dist/discord/send.permissions.js +83 -30
  382. package/dist/discord/send.reactions.js +8 -4
  383. package/dist/discord/token.js +10 -5
  384. package/dist/discord/voice/command.js +263 -0
  385. package/dist/discord/voice/manager.js +531 -0
  386. package/dist/gateway/auth.js +72 -13
  387. package/dist/gateway/call.js +152 -83
  388. package/dist/gateway/canvas-capability.js +75 -0
  389. package/dist/gateway/client.js +28 -4
  390. package/dist/gateway/config-reload.js +3 -4
  391. package/dist/gateway/control-plane-audit.js +28 -0
  392. package/dist/gateway/control-plane-rate-limit.js +53 -0
  393. package/dist/gateway/control-ui.js +219 -96
  394. package/dist/gateway/events.js +1 -0
  395. package/dist/gateway/hooks-mapping.js +88 -38
  396. package/dist/gateway/hooks.js +109 -54
  397. package/dist/gateway/http-auth-helpers.js +3 -2
  398. package/dist/gateway/http-common.js +22 -0
  399. package/dist/gateway/http-endpoint-helpers.js +1 -0
  400. package/dist/gateway/method-scopes.js +169 -0
  401. package/dist/gateway/net.js +74 -9
  402. package/dist/gateway/node-invoke-system-run-approval.js +14 -35
  403. package/dist/gateway/node-registry.js +10 -5
  404. package/dist/gateway/openai-http.js +1 -0
  405. package/dist/gateway/openresponses-http.js +121 -110
  406. package/dist/gateway/origin-check.js +1 -18
  407. package/dist/gateway/probe-auth.js +2 -0
  408. package/dist/gateway/protocol/index.js +4 -2
  409. package/dist/gateway/protocol/schema/cron.js +1 -0
  410. package/dist/gateway/protocol/schema/devices.js +1 -0
  411. package/dist/gateway/protocol/schema/protocol-schemas.js +4 -1
  412. package/dist/gateway/protocol/schema/push.js +18 -0
  413. package/dist/gateway/protocol/schema/sessions.js +6 -0
  414. package/dist/gateway/protocol/schema.js +1 -0
  415. package/dist/gateway/role-policy.js +17 -0
  416. package/dist/gateway/server/ws-connection/connect-policy.js +37 -0
  417. package/dist/gateway/server/ws-connection/message-handler.js +175 -148
  418. package/dist/gateway/server-chat.js +83 -25
  419. package/dist/gateway/server-constants.js +10 -9
  420. package/dist/gateway/server-cron.js +1 -0
  421. package/dist/gateway/server-http.js +247 -54
  422. package/dist/gateway/server-maintenance.js +20 -5
  423. package/dist/gateway/server-methods/agent.js +162 -24
  424. package/dist/gateway/server-methods/chat.js +465 -130
  425. package/dist/gateway/server-methods/config.js +193 -152
  426. package/dist/gateway/server-methods/devices.js +17 -3
  427. package/dist/gateway/server-methods/models.js +11 -1
  428. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  429. package/dist/gateway/server-methods/nodes.js +251 -69
  430. package/dist/gateway/server-methods/push.js +53 -0
  431. package/dist/gateway/server-methods/sessions.js +64 -8
  432. package/dist/gateway/server-methods/usage.js +162 -75
  433. package/dist/gateway/server-node-events.js +29 -0
  434. package/dist/gateway/server-reload-handlers.js +2 -3
  435. package/dist/gateway/server-runtime-config.js +39 -13
  436. package/dist/gateway/server-runtime-state.js +2 -0
  437. package/dist/gateway/server-startup-memory.js +17 -11
  438. package/dist/gateway/server-ws-runtime.js +1 -0
  439. package/dist/gateway/server.impl.js +296 -139
  440. package/dist/gateway/session-preview.test-helpers.js +11 -0
  441. package/dist/gateway/session-utils.fs.js +32 -34
  442. package/dist/gateway/sessions-resolve.js +17 -5
  443. package/dist/gateway/startup-auth.js +126 -0
  444. package/dist/gateway/test-helpers.agent-results.js +15 -0
  445. package/dist/gateway/test-helpers.mocks.js +37 -14
  446. package/dist/gateway/test-helpers.openai-mock.js +14 -7
  447. package/dist/gateway/test-helpers.server.js +161 -77
  448. package/dist/gateway/tools-invoke-http.js +21 -10
  449. package/dist/hooks/bundled/bootstrap-extra-files/handler.js +3 -1
  450. package/dist/hooks/bundled/command-logger/handler.js +7 -2
  451. package/dist/hooks/bundled/session-memory/handler.js +170 -38
  452. package/dist/hooks/frontmatter.js +6 -6
  453. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  454. package/dist/hooks/gmail-watcher.js +11 -6
  455. package/dist/hooks/internal-hooks.js +11 -1
  456. package/dist/hooks/llm-slug-generator.js +4 -1
  457. package/dist/hooks/workspace.js +47 -17
  458. package/dist/imessage/accounts.js +9 -20
  459. package/dist/imessage/monitor/inbound-processing.js +2 -1
  460. package/dist/infra/archive-path.js +49 -0
  461. package/dist/infra/archive.js +174 -73
  462. package/dist/infra/control-ui-assets.js +14 -6
  463. package/dist/infra/device-pairing.js +204 -144
  464. package/dist/infra/env.js +10 -5
  465. package/dist/infra/exec-approvals-allowlist.js +141 -70
  466. package/dist/infra/exec-approvals-analysis.js +78 -20
  467. package/dist/infra/exec-approvals.js +5 -17
  468. package/dist/infra/exec-safe-bin-policy.js +277 -0
  469. package/dist/infra/fixed-window-rate-limit.js +33 -0
  470. package/dist/infra/fs-safe.js +71 -39
  471. package/dist/infra/gateway-lock.js +6 -2
  472. package/dist/infra/git-root.js +61 -0
  473. package/dist/infra/heartbeat-active-hours.js +2 -2
  474. package/dist/infra/heartbeat-reason.js +40 -0
  475. package/dist/infra/heartbeat-runner.js +72 -32
  476. package/dist/infra/heartbeat-wake.js +6 -12
  477. package/dist/infra/host-env-security-policy.json +19 -0
  478. package/dist/infra/host-env-security.js +66 -0
  479. package/dist/infra/install-source-utils.js +91 -7
  480. package/dist/infra/net/ssrf.js +131 -38
  481. package/dist/infra/node-pairing.js +50 -105
  482. package/dist/infra/npm-integrity.js +45 -0
  483. package/dist/infra/npm-pack-install.js +40 -0
  484. package/dist/infra/outbound/bound-delivery-router.js +88 -0
  485. package/dist/infra/outbound/channel-adapters.js +20 -7
  486. package/dist/infra/outbound/channel-selection.js +12 -6
  487. package/dist/infra/outbound/envelope.js +1 -1
  488. package/dist/infra/outbound/format.js +12 -6
  489. package/dist/infra/outbound/message-action-runner.js +107 -327
  490. package/dist/infra/outbound/message.js +59 -36
  491. package/dist/infra/outbound/outbound-policy.js +52 -25
  492. package/dist/infra/outbound/outbound-send-service.js +58 -71
  493. package/dist/infra/outbound/payloads.js +14 -7
  494. package/dist/infra/outbound/session-binding-service.js +123 -0
  495. package/dist/infra/pairing-files.js +10 -0
  496. package/dist/infra/path-guards.js +25 -0
  497. package/dist/infra/plain-object.js +9 -0
  498. package/dist/infra/provider-usage.fetch.codex.js +7 -15
  499. package/dist/infra/provider-usage.fetch.gemini.js +14 -11
  500. package/dist/infra/provider-usage.fetch.shared.js +30 -1
  501. package/dist/infra/provider-usage.fetch.zai.js +10 -9
  502. package/dist/infra/push-apns.js +365 -0
  503. package/dist/infra/restart-sentinel.js +16 -1
  504. package/dist/infra/restart.js +229 -26
  505. package/dist/infra/retry-policy.js +4 -2
  506. package/dist/infra/retry.js +9 -5
  507. package/dist/infra/scp-host.js +54 -0
  508. package/dist/infra/session-cost-usage.js +107 -59
  509. package/dist/infra/session-maintenance-warning.js +3 -1
  510. package/dist/infra/shell-env.js +98 -34
  511. package/dist/infra/ssh-config.js +12 -6
  512. package/dist/infra/system-run-command.js +49 -4
  513. package/dist/infra/update-channels.js +10 -5
  514. package/dist/infra/update-startup.js +86 -9
  515. package/dist/line/accounts.js +5 -7
  516. package/dist/line/bot-access.js +8 -20
  517. package/dist/line/bot-handlers.js +3 -1
  518. package/dist/link-understanding/detect.js +15 -7
  519. package/dist/media/constants.js +15 -6
  520. package/dist/media/image-ops.js +7 -0
  521. package/dist/media/inbound-path-policy.js +114 -0
  522. package/dist/media/input-files.js +16 -0
  523. package/dist/media/local-roots.js +3 -2
  524. package/dist/media-understanding/apply.js +4 -1
  525. package/dist/media-understanding/concurrency.js +8 -20
  526. package/dist/memory/backend-config.js +45 -6
  527. package/dist/memory/embeddings.js +10 -4
  528. package/dist/memory/fs-utils.js +23 -0
  529. package/dist/memory/manager-search.js +12 -6
  530. package/dist/memory/manager-sync-ops.js +12 -2
  531. package/dist/memory/qmd-manager.js +466 -53
  532. package/dist/memory/query-expansion.js +167 -3
  533. package/dist/memory/status-format.js +10 -5
  534. package/dist/memory/sync-memory-files.js +1 -1
  535. package/dist/memory/test-manager.js +8 -0
  536. package/dist/node-host/invoke-system-run.js +281 -0
  537. package/dist/node-host/invoke.js +55 -337
  538. package/dist/pairing/pairing-store.js +22 -0
  539. package/dist/plugin-sdk/allow-from.js +1 -1
  540. package/dist/plugin-sdk/command-auth.js +3 -1
  541. package/dist/plugin-sdk/index.js +6 -3
  542. package/dist/plugin-sdk/temp-path.js +47 -0
  543. package/dist/plugin-sdk/webhook-targets.js +32 -0
  544. package/dist/plugins/bundled-dir.js +9 -6
  545. package/dist/plugins/discovery.js +217 -23
  546. package/dist/plugins/hook-runner-global.js +16 -0
  547. package/dist/plugins/hooks.js +50 -0
  548. package/dist/plugins/install.js +28 -16
  549. package/dist/plugins/loader.js +192 -26
  550. package/dist/plugins/logger.js +8 -0
  551. package/dist/plugins/manifest-registry.js +3 -0
  552. package/dist/plugins/path-safety.js +34 -0
  553. package/dist/plugins/registry.js +5 -2
  554. package/dist/plugins/runtime/index.js +271 -206
  555. package/dist/plugins/runtime.js +3 -17
  556. package/dist/plugins/update.js +78 -12
  557. package/dist/process/spawn-utils.js +14 -7
  558. package/dist/providers/github-copilot-models.js +4 -1
  559. package/dist/providers/github-copilot-token.js +11 -6
  560. package/dist/providers/qwen-portal-oauth.js +14 -6
  561. package/dist/routing/account-id.js +30 -0
  562. package/dist/routing/resolve-route.js +3 -7
  563. package/dist/routing/session-key.js +2 -16
  564. package/dist/security/audit-channel.js +100 -20
  565. package/dist/security/audit-extra.async.js +505 -179
  566. package/dist/security/audit-extra.js +12 -2
  567. package/dist/security/audit-extra.sync.js +421 -35
  568. package/dist/security/audit-fs.js +31 -13
  569. package/dist/security/audit.js +180 -370
  570. package/dist/security/dm-policy-shared.js +68 -0
  571. package/dist/security/external-content.js +46 -14
  572. package/dist/security/fix.js +49 -85
  573. package/dist/security/scan-paths.js +20 -0
  574. package/dist/security/secret-equal.js +3 -7
  575. package/dist/security/windows-acl.js +30 -15
  576. package/dist/shared/entry-status.js +6 -0
  577. package/dist/shared/frontmatter.js +5 -5
  578. package/dist/shared/node-list-parse.js +13 -0
  579. package/dist/shared/node-match.js +11 -4
  580. package/dist/shared/operator-scope-compat.js +42 -0
  581. package/dist/shared/text-chunking.js +29 -0
  582. package/dist/signal/accounts.js +7 -20
  583. package/dist/signal/monitor/event-handler.js +3 -1
  584. package/dist/slack/accounts.js +6 -19
  585. package/dist/slack/actions.js +11 -3
  586. package/dist/slack/blocks.test-helpers.js +31 -0
  587. package/dist/slack/monitor/auth.js +1 -1
  588. package/dist/slack/monitor/message-handler/dispatch.js +50 -29
  589. package/dist/slack/monitor/mrkdwn.js +8 -0
  590. package/dist/slack/monitor/replies.js +15 -7
  591. package/dist/slack/monitor/slash.js +22 -13
  592. package/dist/slack/resolve-channels.js +10 -5
  593. package/dist/slack/send.js +102 -12
  594. package/dist/slack/stream-mode.js +10 -0
  595. package/dist/slack/streaming.js +4 -2
  596. package/dist/telegram/accounts.js +19 -14
  597. package/dist/telegram/bot/helpers.js +3 -5
  598. package/dist/telegram/bot-access.js +35 -36
  599. package/dist/telegram/bot-handlers.js +120 -148
  600. package/dist/telegram/bot-message-context.js +68 -9
  601. package/dist/telegram/bot-message-dispatch.js +477 -210
  602. package/dist/telegram/bot-native-commands.js +16 -0
  603. package/dist/telegram/draft-stream.js +44 -8
  604. package/dist/telegram/inline-buttons.js +5 -15
  605. package/dist/telegram/monitor.js +11 -7
  606. package/dist/telegram/network-config.js +19 -7
  607. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  608. package/dist/telegram/send.js +3 -2
  609. package/dist/telegram/sent-message-cache.js +5 -6
  610. package/dist/telegram/status-reaction-variants.js +208 -0
  611. package/dist/telegram/sticker-cache.js +11 -9
  612. package/dist/terminal/prompt-select-styled.js +9 -0
  613. package/dist/terminal/theme.js +12 -12
  614. package/dist/test-utils/command-runner.js +6 -0
  615. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  616. package/dist/test-utils/model-auth-mock.js +12 -0
  617. package/dist/test-utils/provider-usage-fetch.js +14 -0
  618. package/dist/test-utils/temp-home.js +33 -0
  619. package/dist/tts/tts.js +80 -567
  620. package/dist/tui/components/chat-log.js +50 -8
  621. package/dist/tui/theme/theme.js +10 -12
  622. package/dist/tui/tui-command-handlers.js +36 -27
  623. package/dist/tui/tui-event-handlers.js +122 -32
  624. package/dist/tui/tui-local-shell.js +16 -6
  625. package/dist/tui/tui.js +236 -48
  626. package/dist/utils/account-id.js +2 -4
  627. package/dist/utils/boolean.js +10 -5
  628. package/dist/utils/directive-tags.js +11 -0
  629. package/dist/utils/mask-api-key.js +10 -0
  630. package/dist/utils/queue-helpers.js +67 -12
  631. package/dist/utils/run-with-concurrency.js +39 -0
  632. package/dist/web/auto-reply/deliver-reply.js +8 -4
  633. package/dist/web/auto-reply/mentions.js +10 -5
  634. package/dist/web/auto-reply/monitor/group-members.js +14 -7
  635. package/dist/web/auto-reply/monitor/process-message.js +45 -24
  636. package/dist/web/inbound/access-control.js +5 -2
  637. package/dist/web/login-qr.js +12 -6
  638. package/dist/web/media.js +126 -15
  639. package/docs/tools/slash-commands.md +5 -1
  640. package/extensions/bluebubbles/src/monitor-processing.ts +580 -139
  641. package/extensions/bluebubbles/src/monitor.ts +208 -1950
  642. package/extensions/feishu/src/external-keys.ts +19 -0
  643. package/extensions/lobster/src/windows-spawn.ts +193 -0
  644. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  645. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  646. 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 { VERSION } from "../version.js";
10
- import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
11
- import { collectConfigEnvVars } from "./env-vars.js";
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 CONFIG_BACKUP_COUNT = 5;
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
- async function rotateConfigBackups(configPath, ioFs) {
67
- if (CONFIG_BACKUP_COUNT <= 1)
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
- const backupBase = `${configPath}.bak`;
70
- const maxIndex = CONFIG_BACKUP_COUNT - 1;
71
- await ioFs.unlink(`${backupBase}.${maxIndex}`).catch(() => {
72
- // best-effort
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
- await ioFs.rename(backupBase, `${backupBase}.1`).catch(() => {
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
- function applyConfigEnv(cfg, env) {
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
- // Resolve $include directives before validation
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
- applyConfigEnv(cfg, deps.env);
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 readConfigFileSnapshot() {
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
- path: configPath,
255
- exists: false,
256
- raw: null,
257
- parsed: {},
258
- valid: true,
259
- config,
260
- hash,
261
- issues: [],
262
- warnings: [],
263
- legacyIssues,
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
- path: configPath,
273
- exists: true,
274
- raw,
275
- parsed: {},
276
- valid: false,
277
- config: {},
278
- hash,
279
- issues: [{ path: "", message: `JSON5 parse failed: ${parsedRes.error}` }],
280
- warnings: [],
281
- legacyIssues: [],
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 = resolveConfigIncludes(parsedRes.parsed, configPath, {
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
- path: configPath,
298
- exists: true,
299
- raw,
300
- parsed: parsedRes.parsed,
301
- valid: false,
302
- config: coerceConfig(parsedRes.parsed),
303
- hash,
304
- issues: [{ path: "", message }],
305
- warnings: [],
306
- legacyIssues: [],
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
- // Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars
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
- substituted = resolveConfigEnvVars(resolved, deps.env);
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
- path: configPath,
324
- exists: true,
325
- raw,
326
- parsed: parsedRes.parsed,
327
- valid: false,
328
- config: coerceConfig(resolved),
329
- hash,
330
- issues: [{ path: "", message }],
331
- warnings: [],
332
- legacyIssues: [],
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 = substituted;
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
- valid: false,
345
- config: coerceConfig(resolvedConfigRaw),
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: validated.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
- path: configPath,
369
- exists: true,
370
- raw: null,
371
- parsed: {},
372
- valid: false,
373
- config: {},
374
- hash: hashConfigRaw(null),
375
- issues: [{ path: "", message: `read failed: ${String(err)}` }],
376
- warnings: [],
377
- legacyIssues: [],
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 writeConfigFile(cfg) {
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
- const validated = validateConfigObjectWithPlugins(cfg);
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 json = JSON.stringify(applyModelDefaults(stampConfigVersion(cfg)), null, 2)
398
- .trimEnd()
399
- .concat("\n");
400
- const tmp = path.join(dir, `${path.basename(configPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
401
- await deps.fs.promises.writeFile(tmp, json, {
402
- encoding: "utf-8",
403
- mode: 0o600,
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
- if (deps.fs.existsSync(configPath)) {
406
- await rotateConfigBackups(configPath, deps.fs.promises);
407
- await deps.fs.promises.copyFile(configPath, `${configPath}.bak`).catch(() => {
408
- // best-effort
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.rename(tmp, configPath);
413
- }
414
- catch (err) {
415
- const code = err.code;
416
- // Windows doesn't reliably support atomic replace via rename when dest exists.
417
- if (code === "EPERM" || code === "EEXIST") {
418
- await deps.fs.promises.copyFile(tmp, configPath);
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
- return;
896
+ throw err;
426
897
  }
427
- await deps.fs.promises.unlink(tmp).catch(() => {
428
- // best-effort
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` / `CLAWDBOT_CONFIG_PATH` (and friends) are expected to work even
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() || env.CLAWDBOT_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
- if (!raw)
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() || env.CLAWDBOT_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 configPath = resolveConfigPath();
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 = createConfigIO({ configPath }).loadConfig();
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
- configPath: resolveConfigPath(),
489
- }).readConfigFileSnapshot();
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
- clearConfigCache();
493
- await createConfigIO({ configPath: resolveConfigPath() }).writeConfigFile(cfg);
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
  }