@poolzin/pool-bot 2026.2.24 → 2026.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/sessions.test-helpers.js +61 -0
  63. package/dist/config/commands.js +3 -0
  64. package/dist/config/config.js +1 -1
  65. package/dist/config/env-substitution.js +62 -34
  66. package/dist/config/env-vars.js +9 -0
  67. package/dist/config/io.js +571 -171
  68. package/dist/config/merge-patch.js +50 -4
  69. package/dist/config/redact-snapshot.js +404 -76
  70. package/dist/config/schema.js +58 -570
  71. package/dist/config/validation.js +140 -85
  72. package/dist/config/zod-schema.hooks.js +40 -11
  73. package/dist/config/zod-schema.installs.js +20 -0
  74. package/dist/config/zod-schema.js +8 -7
  75. package/dist/daemon/cmd-argv.js +21 -0
  76. package/dist/daemon/cmd-set.js +58 -0
  77. package/dist/daemon/service-types.js +1 -0
  78. package/dist/discord/monitor/exec-approvals.js +357 -162
  79. package/dist/gateway/auth.js +38 -3
  80. package/dist/gateway/call.js +149 -68
  81. package/dist/gateway/canvas-capability.js +75 -0
  82. package/dist/gateway/control-plane-audit.js +28 -0
  83. package/dist/gateway/control-plane-rate-limit.js +53 -0
  84. package/dist/gateway/events.js +1 -0
  85. package/dist/gateway/hooks.js +109 -54
  86. package/dist/gateway/http-common.js +22 -0
  87. package/dist/gateway/method-scopes.js +169 -0
  88. package/dist/gateway/net.js +23 -0
  89. package/dist/gateway/openresponses-http.js +120 -110
  90. package/dist/gateway/probe-auth.js +2 -0
  91. package/dist/gateway/protocol/index.js +3 -2
  92. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  93. package/dist/gateway/protocol/schema/push.js +18 -0
  94. package/dist/gateway/protocol/schema.js +1 -0
  95. package/dist/gateway/server-http.js +236 -52
  96. package/dist/gateway/server-methods/agent.js +162 -24
  97. package/dist/gateway/server-methods/chat.js +461 -130
  98. package/dist/gateway/server-methods/config.js +193 -150
  99. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  100. package/dist/gateway/server-methods/nodes.js +251 -69
  101. package/dist/gateway/server-methods/push.js +53 -0
  102. package/dist/gateway/server-reload-handlers.js +2 -3
  103. package/dist/gateway/server-runtime-config.js +5 -0
  104. package/dist/gateway/server-runtime-state.js +2 -0
  105. package/dist/gateway/server-ws-runtime.js +1 -0
  106. package/dist/gateway/server.impl.js +296 -139
  107. package/dist/gateway/session-preview.test-helpers.js +11 -0
  108. package/dist/gateway/startup-auth.js +126 -0
  109. package/dist/gateway/test-helpers.agent-results.js +15 -0
  110. package/dist/gateway/test-helpers.mocks.js +37 -14
  111. package/dist/gateway/test-helpers.server.js +161 -77
  112. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  113. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  114. package/dist/infra/archive-path.js +49 -0
  115. package/dist/infra/device-pairing.js +148 -167
  116. package/dist/infra/exec-approvals-allowlist.js +19 -70
  117. package/dist/infra/exec-approvals-analysis.js +44 -17
  118. package/dist/infra/exec-safe-bin-policy.js +269 -0
  119. package/dist/infra/fixed-window-rate-limit.js +33 -0
  120. package/dist/infra/git-root.js +61 -0
  121. package/dist/infra/heartbeat-active-hours.js +2 -2
  122. package/dist/infra/heartbeat-reason.js +40 -0
  123. package/dist/infra/heartbeat-runner.js +72 -32
  124. package/dist/infra/install-source-utils.js +91 -7
  125. package/dist/infra/node-pairing.js +50 -105
  126. package/dist/infra/npm-integrity.js +45 -0
  127. package/dist/infra/npm-pack-install.js +40 -0
  128. package/dist/infra/outbound/channel-adapters.js +20 -7
  129. package/dist/infra/outbound/message-action-runner.js +107 -327
  130. package/dist/infra/outbound/message.js +59 -36
  131. package/dist/infra/outbound/outbound-policy.js +52 -25
  132. package/dist/infra/outbound/outbound-send-service.js +58 -71
  133. package/dist/infra/pairing-files.js +10 -0
  134. package/dist/infra/plain-object.js +9 -0
  135. package/dist/infra/push-apns.js +365 -0
  136. package/dist/infra/restart-sentinel.js +16 -1
  137. package/dist/infra/restart.js +229 -26
  138. package/dist/infra/scp-host.js +54 -0
  139. package/dist/infra/update-startup.js +86 -9
  140. package/dist/media/inbound-path-policy.js +114 -0
  141. package/dist/media/input-files.js +16 -0
  142. package/dist/memory/test-manager.js +8 -0
  143. package/dist/plugin-sdk/temp-path.js +47 -0
  144. package/dist/plugins/discovery.js +217 -23
  145. package/dist/plugins/hook-runner-global.js +16 -0
  146. package/dist/plugins/loader.js +192 -26
  147. package/dist/plugins/logger.js +8 -0
  148. package/dist/plugins/manifest-registry.js +3 -0
  149. package/dist/plugins/path-safety.js +34 -0
  150. package/dist/plugins/registry.js +5 -2
  151. package/dist/plugins/runtime/index.js +271 -206
  152. package/dist/providers/github-copilot-models.js +4 -1
  153. package/dist/security/audit-channel.js +8 -19
  154. package/dist/security/audit-extra.async.js +354 -182
  155. package/dist/security/audit-extra.js +11 -1
  156. package/dist/security/audit-extra.sync.js +340 -33
  157. package/dist/security/audit-fs.js +31 -13
  158. package/dist/security/audit.js +145 -371
  159. package/dist/security/dm-policy-shared.js +24 -0
  160. package/dist/security/external-content.js +20 -8
  161. package/dist/security/fix.js +49 -85
  162. package/dist/security/scan-paths.js +20 -0
  163. package/dist/security/secret-equal.js +3 -7
  164. package/dist/security/windows-acl.js +30 -15
  165. package/dist/shared/node-list-parse.js +13 -0
  166. package/dist/shared/operator-scope-compat.js +37 -0
  167. package/dist/shared/text-chunking.js +29 -0
  168. package/dist/slack/blocks.test-helpers.js +31 -0
  169. package/dist/slack/monitor/mrkdwn.js +8 -0
  170. package/dist/telegram/bot-message-dispatch.js +366 -164
  171. package/dist/telegram/draft-stream.js +30 -7
  172. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  173. package/dist/terminal/prompt-select-styled.js +9 -0
  174. package/dist/test-utils/command-runner.js +6 -0
  175. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  176. package/dist/test-utils/model-auth-mock.js +12 -0
  177. package/dist/test-utils/provider-usage-fetch.js +14 -0
  178. package/dist/test-utils/temp-home.js +33 -0
  179. package/dist/tui/components/chat-log.js +9 -0
  180. package/dist/tui/tui-command-handlers.js +36 -27
  181. package/dist/tui/tui-event-handlers.js +122 -32
  182. package/dist/tui/tui.js +181 -45
  183. package/dist/utils/mask-api-key.js +10 -0
  184. package/dist/utils/run-with-concurrency.js +39 -0
  185. package/dist/web/media.js +4 -0
  186. package/docs/tools/slash-commands.md +5 -1
  187. package/extensions/feishu/src/external-keys.ts +19 -0
  188. package/extensions/lobster/src/windows-spawn.ts +193 -0
  189. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  190. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  191. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { CHANNEL_IDS, normalizeChatChannelId } from "../channels/registry.js";
4
4
  import { normalizePluginsConfig, resolveEnableState, resolveMemorySlotDecision, } from "../plugins/config-state.js";
5
5
  import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
6
6
  import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
7
+ import { isRecord } from "../utils.js";
7
8
  import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js";
8
9
  import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js";
9
10
  import { findLegacyConfigIssues } from "./legacy.js";
@@ -16,28 +17,35 @@ function isWorkspaceAvatarPath(value, workspaceDir) {
16
17
  const workspaceRoot = path.resolve(workspaceDir);
17
18
  const resolved = path.resolve(workspaceRoot, value);
18
19
  const relative = path.relative(workspaceRoot, resolved);
19
- if (relative === "")
20
+ if (relative === "") {
20
21
  return true;
21
- if (relative.startsWith(".."))
22
+ }
23
+ if (relative.startsWith("..")) {
22
24
  return false;
25
+ }
23
26
  return !path.isAbsolute(relative);
24
27
  }
25
28
  function validateIdentityAvatar(config) {
26
29
  const agents = config.agents?.list;
27
- if (!Array.isArray(agents) || agents.length === 0)
30
+ if (!Array.isArray(agents) || agents.length === 0) {
28
31
  return [];
32
+ }
29
33
  const issues = [];
30
34
  for (const [index, entry] of agents.entries()) {
31
- if (!entry || typeof entry !== "object")
35
+ if (!entry || typeof entry !== "object") {
32
36
  continue;
37
+ }
33
38
  const avatarRaw = entry.identity?.avatar;
34
- if (typeof avatarRaw !== "string")
39
+ if (typeof avatarRaw !== "string") {
35
40
  continue;
41
+ }
36
42
  const avatar = avatarRaw.trim();
37
- if (!avatar)
43
+ if (!avatar) {
38
44
  continue;
39
- if (AVATAR_DATA_RE.test(avatar) || AVATAR_HTTP_RE.test(avatar))
45
+ }
46
+ if (AVATAR_DATA_RE.test(avatar) || AVATAR_HTTP_RE.test(avatar)) {
40
47
  continue;
48
+ }
41
49
  if (avatar.startsWith("~")) {
42
50
  issues.push({
43
51
  path: `agents.list.${index}.identity.avatar`,
@@ -63,7 +71,11 @@ function validateIdentityAvatar(config) {
63
71
  }
64
72
  return issues;
65
73
  }
66
- export function validateConfigObject(raw) {
74
+ /**
75
+ * Validates config without applying runtime defaults.
76
+ * Use this when you need the raw validated config (e.g., for writing back to file).
77
+ */
78
+ export function validateConfigObjectRaw(raw) {
67
79
  const legacyIssues = findLegacyConfigIssues(raw);
68
80
  if (legacyIssues.length > 0) {
69
81
  return {
@@ -102,42 +114,136 @@ export function validateConfigObject(raw) {
102
114
  }
103
115
  return {
104
116
  ok: true,
105
- config: applyModelDefaults(applyAgentDefaults(applySessionDefaults(validated.data))),
117
+ config: validated.data,
106
118
  };
107
119
  }
108
- function isRecord(value) {
109
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
120
+ export function validateConfigObject(raw) {
121
+ const result = validateConfigObjectRaw(raw);
122
+ if (!result.ok) {
123
+ return result;
124
+ }
125
+ return {
126
+ ok: true,
127
+ config: applyModelDefaults(applyAgentDefaults(applySessionDefaults(result.config))),
128
+ };
110
129
  }
111
130
  export function validateConfigObjectWithPlugins(raw) {
112
- const base = validateConfigObject(raw);
131
+ return validateConfigObjectWithPluginsBase(raw, { applyDefaults: true });
132
+ }
133
+ export function validateConfigObjectRawWithPlugins(raw) {
134
+ return validateConfigObjectWithPluginsBase(raw, { applyDefaults: false });
135
+ }
136
+ function validateConfigObjectWithPluginsBase(raw, opts) {
137
+ const base = opts.applyDefaults ? validateConfigObject(raw) : validateConfigObjectRaw(raw);
113
138
  if (!base.ok) {
114
139
  return { ok: false, issues: base.issues, warnings: [] };
115
140
  }
116
141
  const config = base.config;
117
142
  const issues = [];
118
143
  const warnings = [];
119
- const pluginsConfig = config.plugins;
120
- const normalizedPlugins = normalizePluginsConfig(pluginsConfig);
121
- const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
122
- const registry = loadPluginManifestRegistry({
123
- config,
124
- workspaceDir: workspaceDir ?? undefined,
125
- });
126
- const knownIds = new Set(registry.plugins.map((record) => record.id));
127
- for (const diag of registry.diagnostics) {
128
- let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins";
129
- if (!diag.pluginId && diag.message.includes("plugin path not found")) {
130
- path = "plugins.load.paths";
144
+ const hasExplicitPluginsConfig = isRecord(raw) && Object.prototype.hasOwnProperty.call(raw, "plugins");
145
+ let registryInfo = null;
146
+ const ensureRegistry = () => {
147
+ if (registryInfo) {
148
+ return registryInfo;
149
+ }
150
+ const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
151
+ const registry = loadPluginManifestRegistry({
152
+ config,
153
+ workspaceDir: workspaceDir ?? undefined,
154
+ });
155
+ const knownIds = new Set(registry.plugins.map((record) => record.id));
156
+ const normalizedPlugins = normalizePluginsConfig(config.plugins);
157
+ for (const diag of registry.diagnostics) {
158
+ let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins";
159
+ if (!diag.pluginId && diag.message.includes("plugin path not found")) {
160
+ path = "plugins.load.paths";
161
+ }
162
+ const pluginLabel = diag.pluginId ? `plugin ${diag.pluginId}` : "plugin";
163
+ const message = `${pluginLabel}: ${diag.message}`;
164
+ if (diag.level === "error") {
165
+ issues.push({ path, message });
166
+ }
167
+ else {
168
+ warnings.push({ path, message });
169
+ }
170
+ }
171
+ registryInfo = { registry, knownIds, normalizedPlugins };
172
+ return registryInfo;
173
+ };
174
+ const allowedChannels = new Set(["defaults", ...CHANNEL_IDS]);
175
+ if (config.channels && isRecord(config.channels)) {
176
+ for (const key of Object.keys(config.channels)) {
177
+ const trimmed = key.trim();
178
+ if (!trimmed) {
179
+ continue;
180
+ }
181
+ if (!allowedChannels.has(trimmed)) {
182
+ const { registry } = ensureRegistry();
183
+ for (const record of registry.plugins) {
184
+ for (const channelId of record.channels) {
185
+ allowedChannels.add(channelId);
186
+ }
187
+ }
188
+ }
189
+ if (!allowedChannels.has(trimmed)) {
190
+ issues.push({
191
+ path: `channels.${trimmed}`,
192
+ message: `unknown channel id: ${trimmed}`,
193
+ });
194
+ }
195
+ }
196
+ }
197
+ const heartbeatChannelIds = new Set();
198
+ for (const channelId of CHANNEL_IDS) {
199
+ heartbeatChannelIds.add(channelId.toLowerCase());
200
+ }
201
+ const validateHeartbeatTarget = (target, path) => {
202
+ if (typeof target !== "string") {
203
+ return;
204
+ }
205
+ const trimmed = target.trim();
206
+ if (!trimmed) {
207
+ issues.push({ path, message: "heartbeat target must not be empty" });
208
+ return;
209
+ }
210
+ const normalized = trimmed.toLowerCase();
211
+ if (normalized === "last" || normalized === "none") {
212
+ return;
213
+ }
214
+ if (normalizeChatChannelId(trimmed)) {
215
+ return;
131
216
  }
132
- const pluginLabel = diag.pluginId ? `plugin ${diag.pluginId}` : "plugin";
133
- const message = `${pluginLabel}: ${diag.message}`;
134
- if (diag.level === "error") {
135
- issues.push({ path, message });
217
+ if (!heartbeatChannelIds.has(normalized)) {
218
+ const { registry } = ensureRegistry();
219
+ for (const record of registry.plugins) {
220
+ for (const channelId of record.channels) {
221
+ const pluginChannel = channelId.trim();
222
+ if (pluginChannel) {
223
+ heartbeatChannelIds.add(pluginChannel.toLowerCase());
224
+ }
225
+ }
226
+ }
136
227
  }
137
- else {
138
- warnings.push({ path, message });
228
+ if (heartbeatChannelIds.has(normalized)) {
229
+ return;
230
+ }
231
+ issues.push({ path, message: `unknown heartbeat target: ${target}` });
232
+ };
233
+ validateHeartbeatTarget(config.agents?.defaults?.heartbeat?.target, "agents.defaults.heartbeat.target");
234
+ if (Array.isArray(config.agents?.list)) {
235
+ for (const [index, entry] of config.agents.list.entries()) {
236
+ validateHeartbeatTarget(entry?.heartbeat?.target, `agents.list.${index}.heartbeat.target`);
139
237
  }
140
238
  }
239
+ if (!hasExplicitPluginsConfig) {
240
+ if (issues.length > 0) {
241
+ return { ok: false, issues, warnings };
242
+ }
243
+ return { ok: true, config, warnings };
244
+ }
245
+ const { registry, knownIds, normalizedPlugins } = ensureRegistry();
246
+ const pluginsConfig = config.plugins;
141
247
  const entries = pluginsConfig?.entries;
142
248
  if (entries && isRecord(entries)) {
143
249
  for (const pluginId of Object.keys(entries)) {
@@ -151,8 +257,9 @@ export function validateConfigObjectWithPlugins(raw) {
151
257
  }
152
258
  const allow = pluginsConfig?.allow ?? [];
153
259
  for (const pluginId of allow) {
154
- if (typeof pluginId !== "string" || !pluginId.trim())
260
+ if (typeof pluginId !== "string" || !pluginId.trim()) {
155
261
  continue;
262
+ }
156
263
  if (!knownIds.has(pluginId)) {
157
264
  issues.push({
158
265
  path: "plugins.allow",
@@ -162,8 +269,9 @@ export function validateConfigObjectWithPlugins(raw) {
162
269
  }
163
270
  const deny = pluginsConfig?.deny ?? [];
164
271
  for (const pluginId of deny) {
165
- if (typeof pluginId !== "string" || !pluginId.trim())
272
+ if (typeof pluginId !== "string" || !pluginId.trim()) {
166
273
  continue;
274
+ }
167
275
  if (!knownIds.has(pluginId)) {
168
276
  issues.push({
169
277
  path: "plugins.deny",
@@ -178,59 +286,6 @@ export function validateConfigObjectWithPlugins(raw) {
178
286
  message: `plugin not found: ${memorySlot}`,
179
287
  });
180
288
  }
181
- const allowedChannels = new Set(["defaults", ...CHANNEL_IDS]);
182
- for (const record of registry.plugins) {
183
- for (const channelId of record.channels) {
184
- allowedChannels.add(channelId);
185
- }
186
- }
187
- if (config.channels && isRecord(config.channels)) {
188
- for (const key of Object.keys(config.channels)) {
189
- const trimmed = key.trim();
190
- if (!trimmed)
191
- continue;
192
- if (!allowedChannels.has(trimmed)) {
193
- issues.push({
194
- path: `channels.${trimmed}`,
195
- message: `unknown channel id: ${trimmed}`,
196
- });
197
- }
198
- }
199
- }
200
- const heartbeatChannelIds = new Set();
201
- for (const channelId of CHANNEL_IDS) {
202
- heartbeatChannelIds.add(channelId.toLowerCase());
203
- }
204
- for (const record of registry.plugins) {
205
- for (const channelId of record.channels) {
206
- const trimmed = channelId.trim();
207
- if (trimmed)
208
- heartbeatChannelIds.add(trimmed.toLowerCase());
209
- }
210
- }
211
- const validateHeartbeatTarget = (target, path) => {
212
- if (typeof target !== "string")
213
- return;
214
- const trimmed = target.trim();
215
- if (!trimmed) {
216
- issues.push({ path, message: "heartbeat target must not be empty" });
217
- return;
218
- }
219
- const normalized = trimmed.toLowerCase();
220
- if (normalized === "last" || normalized === "none")
221
- return;
222
- if (normalizeChatChannelId(trimmed))
223
- return;
224
- if (heartbeatChannelIds.has(normalized))
225
- return;
226
- issues.push({ path, message: `unknown heartbeat target: ${target}` });
227
- };
228
- validateHeartbeatTarget(config.agents?.defaults?.heartbeat?.target, "agents.defaults.heartbeat.target");
229
- if (Array.isArray(config.agents?.list)) {
230
- for (const [index, entry] of config.agents.list.entries()) {
231
- validateHeartbeatTarget(entry?.heartbeat?.target, `agents.list.${index}.heartbeat.target`);
232
- }
233
- }
234
289
  let selectedMemoryPluginId = null;
235
290
  const seenPlugins = new Set();
236
291
  for (const record of registry.plugins) {
@@ -1,4 +1,33 @@
1
+ import path from "node:path";
1
2
  import { z } from "zod";
3
+ import { InstallRecordShape } from "./zod-schema.installs.js";
4
+ import { sensitive } from "./zod-schema.sensitive.js";
5
+ function isSafeRelativeModulePath(raw) {
6
+ const value = raw.trim();
7
+ if (!value) {
8
+ return false;
9
+ }
10
+ // Hook modules are loaded via file-path resolution + dynamic import().
11
+ // Keep this strictly relative to a configured base dir to avoid path traversal and surprises.
12
+ if (path.isAbsolute(value)) {
13
+ return false;
14
+ }
15
+ if (value.startsWith("~")) {
16
+ return false;
17
+ }
18
+ // Disallow URL-ish and drive-relative forms (e.g. "file:...", "C:foo").
19
+ if (value.includes(":")) {
20
+ return false;
21
+ }
22
+ const parts = value.split(/[\\/]+/g);
23
+ if (parts.some((part) => part === "..")) {
24
+ return false;
25
+ }
26
+ return true;
27
+ }
28
+ const SafeRelativeModulePathSchema = z
29
+ .string()
30
+ .refine(isSafeRelativeModulePath, "module must be a safe relative path (no absolute paths)");
2
31
  export const HookMappingSchema = z
3
32
  .object({
4
33
  id: z.string().optional(),
@@ -11,7 +40,8 @@ export const HookMappingSchema = z
11
40
  action: z.union([z.literal("wake"), z.literal("agent")]).optional(),
12
41
  wakeMode: z.union([z.literal("now"), z.literal("next-heartbeat")]).optional(),
13
42
  name: z.string().optional(),
14
- sessionKey: z.string().optional(),
43
+ agentId: z.string().optional(),
44
+ sessionKey: z.string().optional().register(sensitive),
15
45
  messageTemplate: z.string().optional(),
16
46
  textTemplate: z.string().optional(),
17
47
  deliver: z.boolean().optional(),
@@ -22,6 +52,7 @@ export const HookMappingSchema = z
22
52
  z.literal("whatsapp"),
23
53
  z.literal("telegram"),
24
54
  z.literal("discord"),
55
+ z.literal("irc"),
25
56
  z.literal("slack"),
26
57
  z.literal("signal"),
27
58
  z.literal("imessage"),
@@ -34,7 +65,7 @@ export const HookMappingSchema = z
34
65
  timeoutSeconds: z.number().int().positive().optional(),
35
66
  transform: z
36
67
  .object({
37
- module: z.string(),
68
+ module: SafeRelativeModulePathSchema,
38
69
  export: z.string().optional(),
39
70
  })
40
71
  .strict()
@@ -45,7 +76,7 @@ export const HookMappingSchema = z
45
76
  export const InternalHookHandlerSchema = z
46
77
  .object({
47
78
  event: z.string(),
48
- module: z.string(),
79
+ module: SafeRelativeModulePathSchema,
49
80
  export: z.string().optional(),
50
81
  })
51
82
  .strict();
@@ -54,15 +85,13 @@ const HookConfigSchema = z
54
85
  enabled: z.boolean().optional(),
55
86
  env: z.record(z.string(), z.string()).optional(),
56
87
  })
57
- .strict();
88
+ // Hook configs are intentionally open-ended (handlers can define their own keys).
89
+ // Keep enabled/env typed, but allow additional per-hook keys without marking the
90
+ // whole config invalid (which triggers doctor/best-effort loads).
91
+ .passthrough();
58
92
  const HookInstallRecordSchema = z
59
93
  .object({
60
- source: z.union([z.literal("npm"), z.literal("archive"), z.literal("path")]),
61
- spec: z.string().optional(),
62
- sourcePath: z.string().optional(),
63
- installPath: z.string().optional(),
64
- version: z.string().optional(),
65
- installedAt: z.string().optional(),
94
+ ...InstallRecordShape,
66
95
  hooks: z.array(z.string()).optional(),
67
96
  })
68
97
  .strict();
@@ -87,7 +116,7 @@ export const HooksGmailSchema = z
87
116
  label: z.string().optional(),
88
117
  topic: z.string().optional(),
89
118
  subscription: z.string().optional(),
90
- pushToken: z.string().optional(),
119
+ pushToken: z.string().optional().register(sensitive),
91
120
  hookUrl: z.string().optional(),
92
121
  includeBody: z.boolean().optional(),
93
122
  maxBytes: z.number().int().positive().optional(),
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ export const InstallSourceSchema = z.union([
3
+ z.literal("npm"),
4
+ z.literal("archive"),
5
+ z.literal("path"),
6
+ ]);
7
+ export const InstallRecordShape = {
8
+ source: InstallSourceSchema,
9
+ spec: z.string().optional(),
10
+ sourcePath: z.string().optional(),
11
+ installPath: z.string().optional(),
12
+ version: z.string().optional(),
13
+ resolvedName: z.string().optional(),
14
+ resolvedVersion: z.string().optional(),
15
+ resolvedSpec: z.string().optional(),
16
+ integrity: z.string().optional(),
17
+ shasum: z.string().optional(),
18
+ resolvedAt: z.string().optional(),
19
+ installedAt: z.string().optional(),
20
+ };
@@ -6,6 +6,7 @@ import { HexColorSchema, ModelsConfigSchema } from "./zod-schema.core.js";
6
6
  import { HookMappingSchema, HooksGmailSchema, InternalHooksSchema } from "./zod-schema.hooks.js";
7
7
  import { ChannelsSchema } from "./zod-schema.providers.js";
8
8
  import { CommandsSchema, MessagesSchema, SessionSchema } from "./zod-schema.session.js";
9
+ import { sensitive } from "./zod-schema.sensitive.js";
9
10
  const BrowserSnapshotDefaultsSchema = z
10
11
  .object({
11
12
  mode: z.literal("efficient").optional(),
@@ -226,7 +227,7 @@ export const PoolBotSchema = z
226
227
  .object({
227
228
  enabled: z.boolean().optional(),
228
229
  path: z.string().optional(),
229
- token: z.string().optional(),
230
+ token: z.string().optional().register(sensitive),
230
231
  maxBodyBytes: z.number().int().positive().optional(),
231
232
  presets: z.array(z.string()).optional(),
232
233
  transformsDir: z.string().optional(),
@@ -286,7 +287,7 @@ export const PoolBotSchema = z
286
287
  voiceAliases: z.record(z.string(), z.string()).optional(),
287
288
  modelId: z.string().optional(),
288
289
  outputFormat: z.string().optional(),
289
- apiKey: z.string().optional(),
290
+ apiKey: z.string().optional().register(sensitive),
290
291
  interruptOnSpeech: z.boolean().optional(),
291
292
  })
292
293
  .strict()
@@ -316,8 +317,8 @@ export const PoolBotSchema = z
316
317
  auth: z
317
318
  .object({
318
319
  mode: z.union([z.literal("token"), z.literal("password")]).optional(),
319
- token: z.string().optional(),
320
- password: z.string().optional(),
320
+ token: z.string().optional().register(sensitive),
321
+ password: z.string().optional().register(sensitive),
321
322
  allowTailscale: z.boolean().optional(),
322
323
  })
323
324
  .strict()
@@ -334,8 +335,8 @@ export const PoolBotSchema = z
334
335
  .object({
335
336
  url: z.string().optional(),
336
337
  transport: z.union([z.literal("ssh"), z.literal("direct")]).optional(),
337
- token: z.string().optional(),
338
- password: z.string().optional(),
338
+ token: z.string().optional().register(sensitive),
339
+ password: z.string().optional().register(sensitive),
339
340
  tlsFingerprint: z.string().optional(),
340
341
  sshTarget: z.string().optional(),
341
342
  sshIdentity: z.string().optional(),
@@ -460,7 +461,7 @@ export const PoolBotSchema = z
460
461
  .record(z.string(), z
461
462
  .object({
462
463
  enabled: z.boolean().optional(),
463
- apiKey: z.string().optional(),
464
+ apiKey: z.string().optional().register(sensitive),
464
465
  env: z.record(z.string(), z.string()).optional(),
465
466
  config: z.record(z.string(), z.unknown()).optional(),
466
467
  })
@@ -0,0 +1,21 @@
1
+ import { splitArgsPreservingQuotes } from "./arg-split.js";
2
+ import { assertNoCmdLineBreak } from "./cmd-set.js";
3
+ export function quoteCmdScriptArg(value) {
4
+ assertNoCmdLineBreak(value, "Command argument");
5
+ if (!value) {
6
+ return '""';
7
+ }
8
+ const escaped = value.replace(/"/g, '\\"').replace(/%/g, "%%").replace(/!/g, "^!");
9
+ if (!/[ \t"&|<>^()%!]/g.test(value)) {
10
+ return escaped;
11
+ }
12
+ return `"${escaped}"`;
13
+ }
14
+ export function unescapeCmdScriptArg(value) {
15
+ return value.replace(/\^!/g, "!").replace(/%%/g, "%");
16
+ }
17
+ export function parseCmdScriptCommandLine(value) {
18
+ // Script renderer escapes quotes (`\"`) and cmd expansions (`%%`, `^!`).
19
+ // Keep all other backslashes literal so Windows drive/UNC paths survive.
20
+ return splitArgsPreservingQuotes(value, { escapeMode: "backslash-quote-only" }).map(unescapeCmdScriptArg);
21
+ }
@@ -0,0 +1,58 @@
1
+ export function assertNoCmdLineBreak(value, field) {
2
+ if (/[\r\n]/.test(value)) {
3
+ throw new Error(`${field} cannot contain CR or LF in Windows task scripts.`);
4
+ }
5
+ }
6
+ function escapeCmdSetAssignmentComponent(value) {
7
+ return value.replace(/\^/g, "^^").replace(/%/g, "%%").replace(/!/g, "^!").replace(/"/g, '^"');
8
+ }
9
+ function unescapeCmdSetAssignmentComponent(value) {
10
+ let out = "";
11
+ for (let i = 0; i < value.length; i += 1) {
12
+ const ch = value[i];
13
+ const next = value[i + 1];
14
+ if (ch === "^" && (next === "^" || next === '"' || next === "!")) {
15
+ out += next;
16
+ i += 1;
17
+ continue;
18
+ }
19
+ if (ch === "%" && next === "%") {
20
+ out += "%";
21
+ i += 1;
22
+ continue;
23
+ }
24
+ out += ch;
25
+ }
26
+ return out;
27
+ }
28
+ export function parseCmdSetAssignment(line) {
29
+ const raw = line.trim();
30
+ if (!raw) {
31
+ return null;
32
+ }
33
+ const quoted = raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2;
34
+ const assignment = quoted ? raw.slice(1, -1) : raw;
35
+ const index = assignment.indexOf("=");
36
+ if (index <= 0) {
37
+ return null;
38
+ }
39
+ const key = assignment.slice(0, index).trim();
40
+ const value = assignment.slice(index + 1).trim();
41
+ if (!key) {
42
+ return null;
43
+ }
44
+ if (!quoted) {
45
+ return { key, value };
46
+ }
47
+ return {
48
+ key: unescapeCmdSetAssignmentComponent(key),
49
+ value: unescapeCmdSetAssignmentComponent(value),
50
+ };
51
+ }
52
+ export function renderCmdSetAssignment(key, value) {
53
+ assertNoCmdLineBreak(key, "Environment variable name");
54
+ assertNoCmdLineBreak(value, "Environment variable value");
55
+ const escapedKey = escapeCmdSetAssignmentComponent(key);
56
+ const escapedValue = escapeCmdSetAssignmentComponent(value);
57
+ return `set "${escapedKey}=${escapedValue}"`;
58
+ }
@@ -0,0 +1 @@
1
+ export {};