@poolzin/pool-bot 2026.2.23 → 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 (235) hide show
  1. package/CHANGELOG.md +29 -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/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -1,29 +1,30 @@
1
1
  import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
2
- import { CONFIG_PATH, loadConfig, parseConfigJson5, readConfigFileSnapshot, resolveConfigSnapshotHash, validateConfigObjectWithPlugins, writeConfigFile, } from "../../config/config.js";
2
+ import { listChannelPlugins } from "../../channels/plugins/index.js";
3
+ import { CONFIG_PATH, loadConfig, parseConfigJson5, readConfigFileSnapshot, readConfigFileSnapshotForWrite, resolveConfigSnapshotHash, validateConfigObjectWithPlugins, writeConfigFile, } from "../../config/config.js";
3
4
  import { applyLegacyMigrations } from "../../config/legacy.js";
4
5
  import { applyMergePatch } from "../../config/merge-patch.js";
6
+ import { redactConfigObject, redactConfigSnapshot, restoreRedactedValues, } from "../../config/redact-snapshot.js";
5
7
  import { buildConfigSchema } from "../../config/schema.js";
6
- import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
8
+ import { extractDeliveryInfo } from "../../config/sessions.js";
7
9
  import { formatDoctorNonInteractiveHint, writeRestartSentinel, } from "../../infra/restart-sentinel.js";
8
- import { listChannelPlugins } from "../../channels/plugins/index.js";
10
+ import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
9
11
  import { loadPoolBotPlugins } from "../../plugins/loader.js";
10
- import { ErrorCodes, errorShape, formatValidationErrors, validateConfigApplyParams, validateConfigGetParams, validateConfigPatchParams, validateConfigSchemaParams, validateConfigSetParams, } from "../protocol/index.js";
11
- function resolveBaseHash(params) {
12
- const raw = params?.baseHash;
13
- if (typeof raw !== "string")
14
- return null;
15
- const trimmed = raw.trim();
16
- return trimmed ? trimmed : null;
17
- }
12
+ import { diffConfigPaths } from "../config-reload.js";
13
+ import { formatControlPlaneActor, resolveControlPlaneActor, summarizeChangedPaths, } from "../control-plane-audit.js";
14
+ import { ErrorCodes, errorShape, validateConfigApplyParams, validateConfigGetParams, validateConfigPatchParams, validateConfigSchemaParams, validateConfigSetParams, } from "../protocol/index.js";
15
+ import { resolveBaseHashParam } from "./base-hash.js";
16
+ import { parseRestartRequestParams } from "./restart-request.js";
17
+ import { assertValidParams } from "./validation.js";
18
18
  function requireConfigBaseHash(params, snapshot, respond) {
19
- if (!snapshot.exists)
19
+ if (!snapshot.exists) {
20
20
  return true;
21
+ }
21
22
  const snapshotHash = resolveConfigSnapshotHash(snapshot);
22
23
  if (!snapshotHash) {
23
24
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "config base hash unavailable; re-run config.get and retry"));
24
25
  return false;
25
26
  }
26
- const baseHash = resolveBaseHash(params);
27
+ const baseHash = resolveBaseHashParam(params);
27
28
  if (!baseHash) {
28
29
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "config base hash required; re-run config.get and retry"));
29
30
  return false;
@@ -34,89 +35,149 @@ function requireConfigBaseHash(params, snapshot, respond) {
34
35
  }
35
36
  return true;
36
37
  }
38
+ function parseRawConfigOrRespond(params, requestName, respond) {
39
+ const rawValue = params.raw;
40
+ if (typeof rawValue !== "string") {
41
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid ${requestName} params: raw (string) required`));
42
+ return null;
43
+ }
44
+ return rawValue;
45
+ }
46
+ function parseValidateConfigFromRawOrRespond(params, requestName, snapshot, respond) {
47
+ const rawValue = parseRawConfigOrRespond(params, requestName, respond);
48
+ if (!rawValue) {
49
+ return null;
50
+ }
51
+ const parsedRes = parseConfigJson5(rawValue);
52
+ if (!parsedRes.ok) {
53
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, parsedRes.error));
54
+ return null;
55
+ }
56
+ const schema = loadSchemaWithPlugins();
57
+ const restored = restoreRedactedValues(parsedRes.parsed, snapshot.config, schema.uiHints);
58
+ if (!restored.ok) {
59
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, restored.humanReadableMessage ?? "invalid config"));
60
+ return null;
61
+ }
62
+ const validated = validateConfigObjectWithPlugins(restored.result);
63
+ if (!validated.ok) {
64
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", {
65
+ details: { issues: validated.issues },
66
+ }));
67
+ return null;
68
+ }
69
+ return { config: validated.config, schema };
70
+ }
71
+ function resolveConfigRestartRequest(params) {
72
+ const { sessionKey, note, restartDelayMs } = parseRestartRequestParams(params);
73
+ // Extract deliveryContext + threadId for routing after restart
74
+ // Supports both :thread: (most channels) and :topic: (Telegram)
75
+ const { deliveryContext, threadId } = extractDeliveryInfo(sessionKey);
76
+ return {
77
+ sessionKey,
78
+ note,
79
+ restartDelayMs,
80
+ deliveryContext,
81
+ threadId,
82
+ };
83
+ }
84
+ function buildConfigRestartSentinelPayload(params) {
85
+ return {
86
+ kind: params.kind,
87
+ status: "ok",
88
+ ts: Date.now(),
89
+ sessionKey: params.sessionKey,
90
+ deliveryContext: params.deliveryContext,
91
+ threadId: params.threadId,
92
+ message: params.note ?? null,
93
+ doctorHint: formatDoctorNonInteractiveHint(),
94
+ stats: {
95
+ mode: params.mode,
96
+ root: CONFIG_PATH,
97
+ },
98
+ };
99
+ }
100
+ async function tryWriteRestartSentinelPayload(payload) {
101
+ try {
102
+ return await writeRestartSentinel(payload);
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ function loadSchemaWithPlugins() {
109
+ const cfg = loadConfig();
110
+ const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
111
+ const pluginRegistry = loadPoolBotPlugins({
112
+ config: cfg,
113
+ cache: true,
114
+ workspaceDir,
115
+ logger: {
116
+ info: () => { },
117
+ warn: () => { },
118
+ error: () => { },
119
+ debug: () => { },
120
+ },
121
+ });
122
+ // Note: We can't easily cache this, as there are no callback that can invalidate
123
+ // our cache. However, both loadConfig() and loadPoolBotPlugins() already cache
124
+ // their results, and buildConfigSchema() is just a cheap transformation.
125
+ return buildConfigSchema({
126
+ plugins: pluginRegistry.plugins.map((plugin) => ({
127
+ id: plugin.id,
128
+ name: plugin.name,
129
+ description: plugin.description,
130
+ configUiHints: plugin.configUiHints,
131
+ configSchema: plugin.configJsonSchema,
132
+ })),
133
+ channels: listChannelPlugins().map((entry) => ({
134
+ id: entry.id,
135
+ label: entry.meta.label,
136
+ description: entry.meta.blurb,
137
+ configSchema: entry.configSchema?.schema,
138
+ configUiHints: entry.configSchema?.uiHints,
139
+ })),
140
+ });
141
+ }
37
142
  export const configHandlers = {
38
143
  "config.get": async ({ params, respond }) => {
39
- if (!validateConfigGetParams(params)) {
40
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid config.get params: ${formatValidationErrors(validateConfigGetParams.errors)}`));
144
+ if (!assertValidParams(params, validateConfigGetParams, "config.get", respond)) {
41
145
  return;
42
146
  }
43
147
  const snapshot = await readConfigFileSnapshot();
44
- respond(true, snapshot, undefined);
148
+ const schema = loadSchemaWithPlugins();
149
+ respond(true, redactConfigSnapshot(snapshot, schema.uiHints), undefined);
45
150
  },
46
151
  "config.schema": ({ params, respond }) => {
47
- if (!validateConfigSchemaParams(params)) {
48
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid config.schema params: ${formatValidationErrors(validateConfigSchemaParams.errors)}`));
152
+ if (!assertValidParams(params, validateConfigSchemaParams, "config.schema", respond)) {
49
153
  return;
50
154
  }
51
- const cfg = loadConfig();
52
- const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
53
- const pluginRegistry = loadPoolBotPlugins({
54
- config: cfg,
55
- workspaceDir,
56
- logger: {
57
- info: () => { },
58
- warn: () => { },
59
- error: () => { },
60
- debug: () => { },
61
- },
62
- });
63
- const schema = buildConfigSchema({
64
- plugins: pluginRegistry.plugins.map((plugin) => ({
65
- id: plugin.id,
66
- name: plugin.name,
67
- description: plugin.description,
68
- configUiHints: plugin.configUiHints,
69
- configSchema: plugin.configJsonSchema,
70
- })),
71
- channels: listChannelPlugins().map((entry) => ({
72
- id: entry.id,
73
- label: entry.meta.label,
74
- description: entry.meta.blurb,
75
- configSchema: entry.configSchema?.schema,
76
- configUiHints: entry.configSchema?.uiHints,
77
- })),
78
- });
79
- respond(true, schema, undefined);
155
+ respond(true, loadSchemaWithPlugins(), undefined);
80
156
  },
81
157
  "config.set": async ({ params, respond }) => {
82
- if (!validateConfigSetParams(params)) {
83
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid config.set params: ${formatValidationErrors(validateConfigSetParams.errors)}`));
158
+ if (!assertValidParams(params, validateConfigSetParams, "config.set", respond)) {
84
159
  return;
85
160
  }
86
- const snapshot = await readConfigFileSnapshot();
161
+ const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
87
162
  if (!requireConfigBaseHash(params, snapshot, respond)) {
88
163
  return;
89
164
  }
90
- const rawValue = params.raw;
91
- if (typeof rawValue !== "string") {
92
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid config.set params: raw (string) required"));
93
- return;
94
- }
95
- const parsedRes = parseConfigJson5(rawValue);
96
- if (!parsedRes.ok) {
97
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, parsedRes.error));
98
- return;
99
- }
100
- const validated = validateConfigObjectWithPlugins(parsedRes.parsed);
101
- if (!validated.ok) {
102
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", {
103
- details: { issues: validated.issues },
104
- }));
165
+ const parsed = parseValidateConfigFromRawOrRespond(params, "config.set", snapshot, respond);
166
+ if (!parsed) {
105
167
  return;
106
168
  }
107
- await writeConfigFile(validated.config);
169
+ await writeConfigFile(parsed.config, writeOptions);
108
170
  respond(true, {
109
171
  ok: true,
110
172
  path: CONFIG_PATH,
111
- config: validated.config,
173
+ config: redactConfigObject(parsed.config, parsed.schema.uiHints),
112
174
  }, undefined);
113
175
  },
114
- "config.patch": async ({ params, respond }) => {
115
- if (!validateConfigPatchParams(params)) {
116
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid config.patch params: ${formatValidationErrors(validateConfigPatchParams.errors)}`));
176
+ "config.patch": async ({ params, respond, client, context }) => {
177
+ if (!assertValidParams(params, validateConfigPatchParams, "config.patch", respond)) {
117
178
  return;
118
179
  }
119
- const snapshot = await readConfigFileSnapshot();
180
+ const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
120
181
  if (!requireConfigBaseHash(params, snapshot, respond)) {
121
182
  return;
122
183
  }
@@ -140,9 +201,17 @@ export const configHandlers = {
140
201
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "config.patch raw must be an object"));
141
202
  return;
142
203
  }
143
- const merged = applyMergePatch(snapshot.config, parsedRes.parsed);
144
- const migrated = applyLegacyMigrations(merged);
145
- const resolved = migrated.next ?? merged;
204
+ const merged = applyMergePatch(snapshot.config, parsedRes.parsed, {
205
+ mergeObjectArraysById: true,
206
+ });
207
+ const schemaPatch = loadSchemaWithPlugins();
208
+ const restoredMerge = restoreRedactedValues(merged, snapshot.config, schemaPatch.uiHints);
209
+ if (!restoredMerge.ok) {
210
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, restoredMerge.humanReadableMessage ?? "invalid config"));
211
+ return;
212
+ }
213
+ const migrated = applyLegacyMigrations(restoredMerge.result);
214
+ const resolved = migrated.next ?? restoredMerge.result;
146
215
  const validated = validateConfigObjectWithPlugins(resolved);
147
216
  if (!validated.ok) {
148
217
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", {
@@ -150,44 +219,38 @@ export const configHandlers = {
150
219
  }));
151
220
  return;
152
221
  }
153
- await writeConfigFile(validated.config);
154
- const sessionKey = typeof params.sessionKey === "string"
155
- ? params.sessionKey?.trim() || undefined
156
- : undefined;
157
- const note = typeof params.note === "string"
158
- ? params.note?.trim() || undefined
159
- : undefined;
160
- const restartDelayMsRaw = params.restartDelayMs;
161
- const restartDelayMs = typeof restartDelayMsRaw === "number" && Number.isFinite(restartDelayMsRaw)
162
- ? Math.max(0, Math.floor(restartDelayMsRaw))
163
- : undefined;
164
- const payload = {
165
- kind: "config-apply",
166
- status: "ok",
167
- ts: Date.now(),
222
+ const changedPaths = diffConfigPaths(snapshot.config, validated.config);
223
+ const actor = resolveControlPlaneActor(client);
224
+ context?.logGateway?.info(`config.patch write ${formatControlPlaneActor(actor)} changedPaths=${summarizeChangedPaths(changedPaths)} restartReason=config.patch`);
225
+ await writeConfigFile(validated.config, writeOptions);
226
+ const { sessionKey, note, restartDelayMs, deliveryContext, threadId } = resolveConfigRestartRequest(params);
227
+ const payload = buildConfigRestartSentinelPayload({
228
+ kind: "config-patch",
229
+ mode: "config.patch",
168
230
  sessionKey,
169
- message: note ?? null,
170
- doctorHint: formatDoctorNonInteractiveHint(),
171
- stats: {
172
- mode: "config.patch",
173
- root: CONFIG_PATH,
174
- },
231
+ deliveryContext,
232
+ threadId,
233
+ note,
234
+ });
235
+ const sentinelPath = await tryWriteRestartSentinelPayload(payload);
236
+ const audit = {
237
+ actor: actor.actor,
238
+ deviceId: actor.deviceId,
239
+ clientIp: actor.clientIp,
240
+ changedPaths,
175
241
  };
176
- let sentinelPath = null;
177
- try {
178
- sentinelPath = await writeRestartSentinel(payload);
179
- }
180
- catch {
181
- sentinelPath = null;
182
- }
183
242
  const restart = scheduleGatewaySigusr1Restart({
184
243
  delayMs: restartDelayMs,
185
244
  reason: "config.patch",
245
+ audit,
186
246
  });
247
+ if (restart.coalesced) {
248
+ context?.logGateway?.warn(`config.patch restart coalesced ${formatControlPlaneActor(actor)} delayMs=${restart.delayMs}`);
249
+ }
187
250
  respond(true, {
188
251
  ok: true,
189
252
  path: CONFIG_PATH,
190
- config: validated.config,
253
+ config: redactConfigObject(validated.config, schemaPatch.uiHints),
191
254
  restart,
192
255
  sentinel: {
193
256
  path: sentinelPath,
@@ -195,70 +258,50 @@ export const configHandlers = {
195
258
  },
196
259
  }, undefined);
197
260
  },
198
- "config.apply": async ({ params, respond }) => {
199
- if (!validateConfigApplyParams(params)) {
200
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid config.apply params: ${formatValidationErrors(validateConfigApplyParams.errors)}`));
261
+ "config.apply": async ({ params, respond, client, context }) => {
262
+ if (!assertValidParams(params, validateConfigApplyParams, "config.apply", respond)) {
201
263
  return;
202
264
  }
203
- const snapshot = await readConfigFileSnapshot();
265
+ const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
204
266
  if (!requireConfigBaseHash(params, snapshot, respond)) {
205
267
  return;
206
268
  }
207
- const rawValue = params.raw;
208
- if (typeof rawValue !== "string") {
209
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid config.apply params: raw (string) required"));
210
- return;
211
- }
212
- const parsedRes = parseConfigJson5(rawValue);
213
- if (!parsedRes.ok) {
214
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, parsedRes.error));
215
- return;
216
- }
217
- const validated = validateConfigObjectWithPlugins(parsedRes.parsed);
218
- if (!validated.ok) {
219
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", {
220
- details: { issues: validated.issues },
221
- }));
269
+ const parsed = parseValidateConfigFromRawOrRespond(params, "config.apply", snapshot, respond);
270
+ if (!parsed) {
222
271
  return;
223
272
  }
224
- await writeConfigFile(validated.config);
225
- const sessionKey = typeof params.sessionKey === "string"
226
- ? params.sessionKey?.trim() || undefined
227
- : undefined;
228
- const note = typeof params.note === "string"
229
- ? params.note?.trim() || undefined
230
- : undefined;
231
- const restartDelayMsRaw = params.restartDelayMs;
232
- const restartDelayMs = typeof restartDelayMsRaw === "number" && Number.isFinite(restartDelayMsRaw)
233
- ? Math.max(0, Math.floor(restartDelayMsRaw))
234
- : undefined;
235
- const payload = {
273
+ const changedPaths = diffConfigPaths(snapshot.config, parsed.config);
274
+ const actor = resolveControlPlaneActor(client);
275
+ context?.logGateway?.info(`config.apply write ${formatControlPlaneActor(actor)} changedPaths=${summarizeChangedPaths(changedPaths)} restartReason=config.apply`);
276
+ await writeConfigFile(parsed.config, writeOptions);
277
+ const { sessionKey, note, restartDelayMs, deliveryContext, threadId } = resolveConfigRestartRequest(params);
278
+ const payload = buildConfigRestartSentinelPayload({
236
279
  kind: "config-apply",
237
- status: "ok",
238
- ts: Date.now(),
280
+ mode: "config.apply",
239
281
  sessionKey,
240
- message: note ?? null,
241
- doctorHint: formatDoctorNonInteractiveHint(),
242
- stats: {
243
- mode: "config.apply",
244
- root: CONFIG_PATH,
245
- },
282
+ deliveryContext,
283
+ threadId,
284
+ note,
285
+ });
286
+ const sentinelPath = await tryWriteRestartSentinelPayload(payload);
287
+ const audit = {
288
+ actor: actor.actor,
289
+ deviceId: actor.deviceId,
290
+ clientIp: actor.clientIp,
291
+ changedPaths,
246
292
  };
247
- let sentinelPath = null;
248
- try {
249
- sentinelPath = await writeRestartSentinel(payload);
250
- }
251
- catch {
252
- sentinelPath = null;
253
- }
254
293
  const restart = scheduleGatewaySigusr1Restart({
255
294
  delayMs: restartDelayMs,
256
295
  reason: "config.apply",
296
+ audit,
257
297
  });
298
+ if (restart.coalesced) {
299
+ context?.logGateway?.warn(`config.apply restart coalesced ${formatControlPlaneActor(actor)} delayMs=${restart.delayMs}`);
300
+ }
258
301
  respond(true, {
259
302
  ok: true,
260
303
  path: CONFIG_PATH,
261
- config: validated.config,
304
+ config: redactConfigObject(parsed.config, parsed.schema.uiHints),
262
305
  restart,
263
306
  sentinel: {
264
307
  path: sentinelPath,
@@ -30,3 +30,15 @@ export function safeParseJson(value) {
30
30
  return { payloadJSON: value };
31
31
  }
32
32
  }
33
+ export function respondUnavailableOnNodeInvokeError(respond, res) {
34
+ if (res.ok) {
35
+ return true;
36
+ }
37
+ const message = res.error && typeof res.error === "object" && "message" in res.error
38
+ ? res.error.message
39
+ : null;
40
+ respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, typeof message === "string" ? message : "node invoke failed", {
41
+ details: { nodeError: res.error ?? null },
42
+ }));
43
+ return false;
44
+ }