@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
@@ -1,36 +1,182 @@
1
- import { approveNodePairing, listNodePairing, rejectNodePairing, renamePairedNode, requestNodePairing, verifyNodeToken, } from "../../infra/node-pairing.js";
2
- import { listDevicePairing } from "../../infra/device-pairing.js";
3
- import { ErrorCodes, errorShape, validateNodeDescribeParams, validateNodeEventParams, validateNodeInvokeParams, validateNodeInvokeResultParams, validateNodeListParams, validateNodePairApproveParams, validateNodePairListParams, validateNodePairRejectParams, validateNodePairRequestParams, validateNodePairVerifyParams, validateNodeRenameParams, } from "../protocol/index.js";
4
- import { respondInvalidParams, respondUnavailableOnThrow, safeParseJson, uniqueSortedStrings, } from "./nodes.helpers.js";
5
1
  import { loadConfig } from "../../config/config.js";
2
+ import { listDevicePairing } from "../../infra/device-pairing.js";
3
+ import { approveNodePairing, listNodePairing, rejectNodePairing, renamePairedNode, requestNodePairing, verifyNodeToken, } from "../../infra/node-pairing.js";
4
+ import { loadApnsRegistration, resolveApnsAuthConfigFromEnv, sendApnsAlert, sendApnsBackgroundWake, } from "../../infra/push-apns.js";
6
5
  import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-command-policy.js";
6
+ import { sanitizeNodeInvokeParamsForForwarding } from "../node-invoke-sanitize.js";
7
+ import { ErrorCodes, errorShape, validateNodeDescribeParams, validateNodeEventParams, validateNodeInvokeParams, validateNodeListParams, validateNodePairApproveParams, validateNodePairListParams, validateNodePairRejectParams, validateNodePairRequestParams, validateNodePairVerifyParams, validateNodeRenameParams, } from "../protocol/index.js";
8
+ import { handleNodeInvokeResult } from "./nodes.handlers.invoke-result.js";
9
+ import { respondInvalidParams, respondUnavailableOnNodeInvokeError, respondUnavailableOnThrow, safeParseJson, uniqueSortedStrings, } from "./nodes.helpers.js";
10
+ const NODE_WAKE_RECONNECT_WAIT_MS = 3_000;
11
+ const NODE_WAKE_RECONNECT_RETRY_WAIT_MS = 12_000;
12
+ const NODE_WAKE_RECONNECT_POLL_MS = 150;
13
+ const NODE_WAKE_THROTTLE_MS = 15_000;
14
+ const NODE_WAKE_NUDGE_THROTTLE_MS = 10 * 60_000;
15
+ const nodeWakeById = new Map();
16
+ const nodeWakeNudgeById = new Map();
7
17
  function isNodeEntry(entry) {
8
- if (entry.clientMode === "node")
18
+ if (entry.role === "node") {
9
19
  return true;
10
- if (entry.role === "node")
11
- return true;
12
- if (Array.isArray(entry.roles) && entry.roles.includes("node"))
20
+ }
21
+ if (Array.isArray(entry.roles) && entry.roles.includes("node")) {
13
22
  return true;
23
+ }
14
24
  return false;
15
25
  }
16
- function normalizeNodeInvokeResultParams(params) {
17
- if (!params || typeof params !== "object")
18
- return params;
19
- const raw = params;
20
- const normalized = { ...raw };
21
- if (normalized.payloadJSON === null) {
22
- delete normalized.payloadJSON;
26
+ async function delayMs(ms) {
27
+ await new Promise((resolve) => setTimeout(resolve, ms));
28
+ }
29
+ async function maybeWakeNodeWithApns(nodeId, opts) {
30
+ const state = nodeWakeById.get(nodeId) ?? { lastWakeAtMs: 0 };
31
+ nodeWakeById.set(nodeId, state);
32
+ if (state.inFlight) {
33
+ return await state.inFlight;
34
+ }
35
+ const now = Date.now();
36
+ const force = opts?.force === true;
37
+ if (!force && state.lastWakeAtMs > 0 && now - state.lastWakeAtMs < NODE_WAKE_THROTTLE_MS) {
38
+ return { available: true, throttled: true, path: "throttled", durationMs: 0 };
39
+ }
40
+ state.inFlight = (async () => {
41
+ const startedAtMs = Date.now();
42
+ const withDuration = (attempt) => ({
43
+ ...attempt,
44
+ durationMs: Math.max(0, Date.now() - startedAtMs),
45
+ });
46
+ try {
47
+ const registration = await loadApnsRegistration(nodeId);
48
+ if (!registration) {
49
+ return withDuration({ available: false, throttled: false, path: "no-registration" });
50
+ }
51
+ const auth = await resolveApnsAuthConfigFromEnv(process.env);
52
+ if (!auth.ok) {
53
+ return withDuration({
54
+ available: false,
55
+ throttled: false,
56
+ path: "no-auth",
57
+ apnsReason: auth.error,
58
+ });
59
+ }
60
+ state.lastWakeAtMs = Date.now();
61
+ const wakeResult = await sendApnsBackgroundWake({
62
+ auth: auth.value,
63
+ registration,
64
+ nodeId,
65
+ wakeReason: "node.invoke",
66
+ });
67
+ if (!wakeResult.ok) {
68
+ return withDuration({
69
+ available: true,
70
+ throttled: false,
71
+ path: "send-error",
72
+ apnsStatus: wakeResult.status,
73
+ apnsReason: wakeResult.reason,
74
+ });
75
+ }
76
+ return withDuration({
77
+ available: true,
78
+ throttled: false,
79
+ path: "sent",
80
+ apnsStatus: wakeResult.status,
81
+ apnsReason: wakeResult.reason,
82
+ });
83
+ }
84
+ catch (err) {
85
+ // Best-effort wake only.
86
+ const message = err instanceof Error ? err.message : String(err);
87
+ if (state.lastWakeAtMs === 0) {
88
+ return withDuration({
89
+ available: false,
90
+ throttled: false,
91
+ path: "send-error",
92
+ apnsReason: message,
93
+ });
94
+ }
95
+ return withDuration({
96
+ available: true,
97
+ throttled: false,
98
+ path: "send-error",
99
+ apnsReason: message,
100
+ });
101
+ }
102
+ })();
103
+ try {
104
+ return await state.inFlight;
105
+ }
106
+ finally {
107
+ state.inFlight = undefined;
108
+ }
109
+ }
110
+ async function maybeSendNodeWakeNudge(nodeId) {
111
+ const startedAtMs = Date.now();
112
+ const withDuration = (attempt) => ({
113
+ ...attempt,
114
+ durationMs: Math.max(0, Date.now() - startedAtMs),
115
+ });
116
+ const lastNudgeAtMs = nodeWakeNudgeById.get(nodeId) ?? 0;
117
+ if (lastNudgeAtMs > 0 && Date.now() - lastNudgeAtMs < NODE_WAKE_NUDGE_THROTTLE_MS) {
118
+ return withDuration({ sent: false, throttled: true, reason: "throttled" });
119
+ }
120
+ const registration = await loadApnsRegistration(nodeId);
121
+ if (!registration) {
122
+ return withDuration({ sent: false, throttled: false, reason: "no-registration" });
123
+ }
124
+ const auth = await resolveApnsAuthConfigFromEnv(process.env);
125
+ if (!auth.ok) {
126
+ return withDuration({
127
+ sent: false,
128
+ throttled: false,
129
+ reason: "no-auth",
130
+ apnsReason: auth.error,
131
+ });
23
132
  }
24
- else if (normalized.payloadJSON !== undefined && typeof normalized.payloadJSON !== "string") {
25
- if (normalized.payload === undefined) {
26
- normalized.payload = normalized.payloadJSON;
133
+ try {
134
+ const result = await sendApnsAlert({
135
+ auth: auth.value,
136
+ registration,
137
+ nodeId,
138
+ title: "Pool Bot needs a quick reopen",
139
+ body: "Tap to reopen Pool Bot and restore the node connection.",
140
+ });
141
+ if (!result.ok) {
142
+ return withDuration({
143
+ sent: false,
144
+ throttled: false,
145
+ reason: "apns-not-ok",
146
+ apnsStatus: result.status,
147
+ apnsReason: result.reason,
148
+ });
27
149
  }
28
- delete normalized.payloadJSON;
150
+ nodeWakeNudgeById.set(nodeId, Date.now());
151
+ return withDuration({
152
+ sent: true,
153
+ throttled: false,
154
+ reason: "sent",
155
+ apnsStatus: result.status,
156
+ apnsReason: result.reason,
157
+ });
29
158
  }
30
- if (normalized.error === null) {
31
- delete normalized.error;
159
+ catch (err) {
160
+ const message = err instanceof Error ? err.message : String(err);
161
+ return withDuration({
162
+ sent: false,
163
+ throttled: false,
164
+ reason: "send-error",
165
+ apnsReason: message,
166
+ });
167
+ }
168
+ }
169
+ async function waitForNodeReconnect(params) {
170
+ const timeoutMs = Math.max(250, params.timeoutMs ?? NODE_WAKE_RECONNECT_WAIT_MS);
171
+ const pollMs = Math.max(50, params.pollMs ?? NODE_WAKE_RECONNECT_POLL_MS);
172
+ const deadline = Date.now() + timeoutMs;
173
+ while (Date.now() < deadline) {
174
+ if (params.context.nodeRegistry.get(params.nodeId)) {
175
+ return true;
176
+ }
177
+ await delayMs(pollMs);
32
178
  }
33
- return normalized;
179
+ return Boolean(params.context.nodeRegistry.get(params.nodeId));
34
180
  }
35
181
  export const nodeHandlers = {
36
182
  "node.pair.request": async ({ params, respond, context }) => {
@@ -227,14 +373,17 @@ export const nodeHandlers = {
227
373
  };
228
374
  });
229
375
  nodes.sort((a, b) => {
230
- if (a.connected !== b.connected)
376
+ if (a.connected !== b.connected) {
231
377
  return a.connected ? -1 : 1;
378
+ }
232
379
  const an = (a.displayName ?? a.nodeId).toLowerCase();
233
380
  const bn = (b.displayName ?? b.nodeId).toLowerCase();
234
- if (an < bn)
381
+ if (an < bn) {
235
382
  return -1;
236
- if (an > bn)
383
+ }
384
+ if (an > bn) {
237
385
  return 1;
386
+ }
238
387
  return a.nodeId.localeCompare(b.nodeId);
239
388
  });
240
389
  respond(true, { ts: Date.now(), nodes }, undefined);
@@ -287,7 +436,7 @@ export const nodeHandlers = {
287
436
  }, undefined);
288
437
  });
289
438
  },
290
- "node.invoke": async ({ params, respond, context }) => {
439
+ "node.invoke": async ({ params, respond, context, client, req }) => {
291
440
  if (!validateNodeInvokeParams(params)) {
292
441
  respondInvalidParams({
293
442
  respond,
@@ -303,13 +452,69 @@ export const nodeHandlers = {
303
452
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "nodeId and command required"));
304
453
  return;
305
454
  }
455
+ if (command === "system.execApprovals.get" || command === "system.execApprovals.set") {
456
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "node.invoke does not allow system.execApprovals.*; use exec.approvals.node.*", { details: { command } }));
457
+ return;
458
+ }
306
459
  await respondUnavailableOnThrow(respond, async () => {
307
- const nodeSession = context.nodeRegistry.get(nodeId);
460
+ let nodeSession = context.nodeRegistry.get(nodeId);
308
461
  if (!nodeSession) {
309
- respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "node not connected", {
310
- details: { code: "NOT_CONNECTED" },
311
- }));
312
- return;
462
+ const wakeReqId = req.id;
463
+ const wakeFlowStartedAtMs = Date.now();
464
+ context.logGateway.info(`node wake start node=${nodeId} req=${wakeReqId} command=${command}`);
465
+ const wake = await maybeWakeNodeWithApns(nodeId);
466
+ context.logGateway.info(`node wake stage=wake1 node=${nodeId} req=${wakeReqId} ` +
467
+ `available=${wake.available} throttled=${wake.throttled} ` +
468
+ `path=${wake.path} durationMs=${wake.durationMs} ` +
469
+ `apnsStatus=${wake.apnsStatus ?? -1} apnsReason=${wake.apnsReason ?? "-"}`);
470
+ if (wake.available) {
471
+ const waitStartedAtMs = Date.now();
472
+ const waitTimeoutMs = NODE_WAKE_RECONNECT_WAIT_MS;
473
+ const reconnected = await waitForNodeReconnect({
474
+ nodeId,
475
+ context,
476
+ timeoutMs: waitTimeoutMs,
477
+ });
478
+ const waitDurationMs = Math.max(0, Date.now() - waitStartedAtMs);
479
+ context.logGateway.info(`node wake stage=wait1 node=${nodeId} req=${wakeReqId} ` +
480
+ `reconnected=${reconnected} timeoutMs=${waitTimeoutMs} durationMs=${waitDurationMs}`);
481
+ }
482
+ nodeSession = context.nodeRegistry.get(nodeId);
483
+ if (!nodeSession && wake.available) {
484
+ const retryWake = await maybeWakeNodeWithApns(nodeId, { force: true });
485
+ context.logGateway.info(`node wake stage=wake2 node=${nodeId} req=${wakeReqId} force=true ` +
486
+ `available=${retryWake.available} throttled=${retryWake.throttled} ` +
487
+ `path=${retryWake.path} durationMs=${retryWake.durationMs} ` +
488
+ `apnsStatus=${retryWake.apnsStatus ?? -1} apnsReason=${retryWake.apnsReason ?? "-"}`);
489
+ if (retryWake.available) {
490
+ const waitStartedAtMs = Date.now();
491
+ const waitTimeoutMs = NODE_WAKE_RECONNECT_RETRY_WAIT_MS;
492
+ const reconnected = await waitForNodeReconnect({
493
+ nodeId,
494
+ context,
495
+ timeoutMs: waitTimeoutMs,
496
+ });
497
+ const waitDurationMs = Math.max(0, Date.now() - waitStartedAtMs);
498
+ context.logGateway.info(`node wake stage=wait2 node=${nodeId} req=${wakeReqId} ` +
499
+ `reconnected=${reconnected} timeoutMs=${waitTimeoutMs} durationMs=${waitDurationMs}`);
500
+ }
501
+ nodeSession = context.nodeRegistry.get(nodeId);
502
+ }
503
+ if (!nodeSession) {
504
+ const totalDurationMs = Math.max(0, Date.now() - wakeFlowStartedAtMs);
505
+ const nudge = await maybeSendNodeWakeNudge(nodeId);
506
+ context.logGateway.info(`node wake nudge node=${nodeId} req=${wakeReqId} sent=${nudge.sent} ` +
507
+ `throttled=${nudge.throttled} reason=${nudge.reason} durationMs=${nudge.durationMs} ` +
508
+ `apnsStatus=${nudge.apnsStatus ?? -1} apnsReason=${nudge.apnsReason ?? "-"}`);
509
+ context.logGateway.warn(`node wake done node=${nodeId} req=${wakeReqId} connected=false ` +
510
+ `reason=not_connected totalMs=${totalDurationMs}`);
511
+ respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "node not connected", {
512
+ details: { code: "NOT_CONNECTED" },
513
+ }));
514
+ return;
515
+ }
516
+ const totalDurationMs = Math.max(0, Date.now() - wakeFlowStartedAtMs);
517
+ context.logGateway.info(`node wake done node=${nodeId} req=${wakeReqId} connected=true totalMs=${totalDurationMs}`);
313
518
  }
314
519
  const cfg = loadConfig();
315
520
  const allowlist = resolveNodeCommandAllowlist(cfg, nodeSession);
@@ -324,17 +529,26 @@ export const nodeHandlers = {
324
529
  }));
325
530
  return;
326
531
  }
532
+ const forwardedParams = sanitizeNodeInvokeParamsForForwarding({
533
+ command,
534
+ rawParams: p.params,
535
+ client,
536
+ execApprovalManager: context.execApprovalManager,
537
+ });
538
+ if (!forwardedParams.ok) {
539
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, forwardedParams.message, {
540
+ details: forwardedParams.details ?? null,
541
+ }));
542
+ return;
543
+ }
327
544
  const res = await context.nodeRegistry.invoke({
328
545
  nodeId,
329
546
  command,
330
- params: p.params,
547
+ params: forwardedParams.params,
331
548
  timeoutMs: p.timeoutMs,
332
549
  idempotencyKey: p.idempotencyKey,
333
550
  });
334
- if (!res.ok) {
335
- respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, res.error?.message ?? "node invoke failed", {
336
- details: { nodeError: res.error ?? null },
337
- }));
551
+ if (!respondUnavailableOnNodeInvokeError(respond, res)) {
338
552
  return;
339
553
  }
340
554
  const payload = res.payloadJSON ? safeParseJson(res.payloadJSON) : res.payload;
@@ -347,39 +561,7 @@ export const nodeHandlers = {
347
561
  }, undefined);
348
562
  });
349
563
  },
350
- "node.invoke.result": async ({ params, respond, context, client }) => {
351
- const normalizedParams = normalizeNodeInvokeResultParams(params);
352
- if (!validateNodeInvokeResultParams(normalizedParams)) {
353
- respondInvalidParams({
354
- respond,
355
- method: "node.invoke.result",
356
- validator: validateNodeInvokeResultParams,
357
- });
358
- return;
359
- }
360
- const p = normalizedParams;
361
- const callerNodeId = client?.connect?.device?.id ?? client?.connect?.client?.id;
362
- if (callerNodeId && callerNodeId !== p.nodeId) {
363
- respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "nodeId mismatch"));
364
- return;
365
- }
366
- const ok = context.nodeRegistry.handleInvokeResult({
367
- id: p.id,
368
- nodeId: p.nodeId,
369
- ok: p.ok,
370
- payload: p.payload,
371
- payloadJSON: p.payloadJSON ?? null,
372
- error: p.error ?? null,
373
- });
374
- if (!ok) {
375
- // Late-arriving results (after invoke timeout) are expected and harmless.
376
- // Return success instead of error to reduce log noise; client can discard.
377
- context.logGateway.debug(`late invoke result ignored: id=${p.id} node=${p.nodeId}`);
378
- respond(true, { ok: true, ignored: true }, undefined);
379
- return;
380
- }
381
- respond(true, { ok: true }, undefined);
382
- },
564
+ "node.invoke.result": handleNodeInvokeResult,
383
565
  "node.event": async ({ params, respond, context, client }) => {
384
566
  if (!validateNodeEventParams(params)) {
385
567
  respondInvalidParams({
@@ -0,0 +1,53 @@
1
+ import { loadApnsRegistration, normalizeApnsEnvironment, resolveApnsAuthConfigFromEnv, sendApnsAlert, } from "../../infra/push-apns.js";
2
+ import { ErrorCodes, errorShape, validatePushTestParams } from "../protocol/index.js";
3
+ import { respondInvalidParams, respondUnavailableOnThrow } from "./nodes.helpers.js";
4
+ function normalizeOptionalString(value) {
5
+ if (typeof value !== "string") {
6
+ return undefined;
7
+ }
8
+ const trimmed = value.trim();
9
+ return trimmed.length > 0 ? trimmed : undefined;
10
+ }
11
+ export const pushHandlers = {
12
+ "push.test": async ({ params, respond }) => {
13
+ if (!validatePushTestParams(params)) {
14
+ respondInvalidParams({
15
+ respond,
16
+ method: "push.test",
17
+ validator: validatePushTestParams,
18
+ });
19
+ return;
20
+ }
21
+ const nodeId = String(params.nodeId ?? "").trim();
22
+ if (!nodeId) {
23
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "nodeId required"));
24
+ return;
25
+ }
26
+ const title = normalizeOptionalString(params.title) ?? "Pool Bot";
27
+ const body = normalizeOptionalString(params.body) ?? `Push test for node ${nodeId}`;
28
+ await respondUnavailableOnThrow(respond, async () => {
29
+ const registration = await loadApnsRegistration(nodeId);
30
+ if (!registration) {
31
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `node ${nodeId} has no APNs registration (connect iOS node first)`));
32
+ return;
33
+ }
34
+ const auth = await resolveApnsAuthConfigFromEnv(process.env);
35
+ if (!auth.ok) {
36
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, auth.error));
37
+ return;
38
+ }
39
+ const overrideEnvironment = normalizeApnsEnvironment(params.environment);
40
+ const result = await sendApnsAlert({
41
+ auth: auth.value,
42
+ registration: {
43
+ ...registration,
44
+ environment: overrideEnvironment ?? registration.environment,
45
+ },
46
+ nodeId,
47
+ title,
48
+ body,
49
+ });
50
+ respond(true, result, undefined);
51
+ });
52
+ },
53
+ };
@@ -1,6 +1,6 @@
1
1
  import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
2
2
  import { resetDirectoryCache } from "../infra/outbound/target-resolver.js";
3
- import { authorizeGatewaySigusr1Restart, setGatewaySigusr1RestartPolicy, } from "../infra/restart.js";
3
+ import { emitGatewayRestart, setGatewaySigusr1RestartPolicy } from "../infra/restart.js";
4
4
  import { setCommandLaneConcurrency } from "../process/command-queue.js";
5
5
  import { resolveAgentMaxConcurrent, resolveSubagentMaxConcurrent } from "../config/agent-limits.js";
6
6
  import { isTruthyEnvValue } from "../infra/env.js";
@@ -105,8 +105,7 @@ export function createGatewayReloadHandlers(params) {
105
105
  params.logReload.warn("no SIGUSR1 listener found; restart skipped");
106
106
  return;
107
107
  }
108
- authorizeGatewaySigusr1Restart();
109
- process.emit("SIGUSR1");
108
+ emitGatewayRestart();
110
109
  };
111
110
  return { applyHotReload, requestGatewayRestart };
112
111
  }
@@ -13,6 +13,10 @@ export async function resolveGatewayRuntimeConfig(params) {
13
13
  const openResponsesConfig = params.cfg.gateway?.http?.endpoints?.responses;
14
14
  const openResponsesEnabled = params.openResponsesEnabled ?? openResponsesConfig?.enabled ?? false;
15
15
  const controlUiBasePath = normalizeControlUiBasePath(params.cfg.gateway?.controlUi?.basePath);
16
+ const controlUiRootRaw = params.cfg.gateway?.controlUi?.root;
17
+ const controlUiRoot = typeof controlUiRootRaw === "string" && controlUiRootRaw.trim().length > 0
18
+ ? controlUiRootRaw.trim()
19
+ : undefined;
16
20
  const authBase = params.cfg.gateway?.auth ?? {};
17
21
  const authOverrides = params.auth ?? {};
18
22
  const authConfig = {
@@ -56,6 +60,7 @@ export async function resolveGatewayRuntimeConfig(params) {
56
60
  ? { ...openResponsesConfig, enabled: openResponsesEnabled }
57
61
  : undefined,
58
62
  controlUiBasePath,
63
+ controlUiRoot,
59
64
  resolvedAuth,
60
65
  authMode,
61
66
  tailscaleConfig,
@@ -60,6 +60,7 @@ export async function createGatewayRuntimeState(params) {
60
60
  handleHooksRequest,
61
61
  handlePluginRequest,
62
62
  resolvedAuth: params.resolvedAuth,
63
+ rateLimiter: params.rateLimiter,
63
64
  tlsOptions: params.gatewayTls?.enabled ? params.gatewayTls.tlsOptions : undefined,
64
65
  });
65
66
  try {
@@ -93,6 +94,7 @@ export async function createGatewayRuntimeState(params) {
93
94
  canvasHost,
94
95
  clients,
95
96
  resolvedAuth: params.resolvedAuth,
97
+ rateLimiter: params.rateLimiter,
96
98
  });
97
99
  }
98
100
  const agentRunSeq = new Map();
@@ -8,6 +8,7 @@ export function attachGatewayWsHandlers(params) {
8
8
  canvasHostEnabled: params.canvasHostEnabled,
9
9
  canvasHostServerPort: params.canvasHostServerPort,
10
10
  resolvedAuth: params.resolvedAuth,
11
+ rateLimiter: params.rateLimiter,
11
12
  gatewayMethods: params.gatewayMethods,
12
13
  events: params.events,
13
14
  logGateway: params.logGateway,