@poolzin/pool-bot 2026.3.22 → 2026.3.24

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 (159) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/bindings-store.js +209 -0
  4. package/dist/acp/control-plane/runtime-cache.js +54 -0
  5. package/dist/acp/control-plane/runtime-options.js +215 -0
  6. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  7. package/dist/acp/policy.js +52 -0
  8. package/dist/acp/runtime/errors.js +47 -0
  9. package/dist/acp/runtime/registry.js +86 -0
  10. package/dist/acp/runtime/types.js +1 -0
  11. package/dist/acp/translator.js +97 -0
  12. package/dist/agents/btw.js +280 -0
  13. package/dist/agents/failover-error.js +145 -47
  14. package/dist/agents/fast-mode.js +24 -0
  15. package/dist/agents/live-model-errors.js +23 -0
  16. package/dist/agents/model-auth-env-vars.js +44 -0
  17. package/dist/agents/model-auth-markers.js +69 -0
  18. package/dist/agents/models-config.providers.discovery.js +180 -0
  19. package/dist/agents/models-config.providers.static.js +480 -0
  20. package/dist/auto-reply/reply/typing-policy.js +15 -0
  21. package/dist/browser/browser-profile-manager.js +319 -0
  22. package/dist/browser/cdp-proxy-bypass.js +129 -0
  23. package/dist/browser/cdp-timeouts.js +41 -0
  24. package/dist/browser/chrome-extension-validator.js +406 -0
  25. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  26. package/dist/browser/chrome-mcp.js +421 -0
  27. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  28. package/dist/browser/errors.js +67 -0
  29. package/dist/browser/form-fields.js +22 -0
  30. package/dist/browser/output-atomic.js +44 -0
  31. package/dist/browser/profile-capabilities.js +47 -0
  32. package/dist/browser/safe-filename.js +25 -0
  33. package/dist/browser/snapshot-roles.js +60 -0
  34. package/dist/build-info.json +3 -3
  35. package/dist/channels/account-snapshot-fields.js +176 -0
  36. package/dist/channels/draft-stream-controls.js +89 -0
  37. package/dist/channels/inbound-debounce-policy.js +28 -0
  38. package/dist/channels/typing-lifecycle.js +39 -0
  39. package/dist/cli/program/command-registry.js +52 -0
  40. package/dist/commands/agent-binding.js +123 -0
  41. package/dist/commands/agents.commands.bind.js +280 -0
  42. package/dist/commands/backup-shared.js +186 -0
  43. package/dist/commands/backup-verify.js +236 -0
  44. package/dist/commands/backup.js +166 -0
  45. package/dist/commands/channel-account-context.js +15 -0
  46. package/dist/commands/channel-account.js +190 -0
  47. package/dist/commands/gateway-install-token.js +117 -0
  48. package/dist/commands/oauth-tls-preflight.js +121 -0
  49. package/dist/commands/ollama-setup.js +402 -0
  50. package/dist/commands/security-owner-only.js +86 -0
  51. package/dist/commands/self-hosted-provider-setup.js +207 -0
  52. package/dist/commands/session-store-targets.js +12 -0
  53. package/dist/commands/sessions-cleanup.js +97 -0
  54. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  55. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  56. package/dist/control-ui/index.html +1 -1
  57. package/dist/cron/cron-filters.js +150 -0
  58. package/dist/cron/heartbeat-policy.js +26 -0
  59. package/dist/gateway/device-pairing-security.js +197 -0
  60. package/dist/gateway/event-deduplication.js +167 -0
  61. package/dist/gateway/hooks-mapping.js +46 -7
  62. package/dist/gateway/run-tracker.js +253 -0
  63. package/dist/gateway/server-methods/nodes.js +14 -0
  64. package/dist/gateway/websocket-preauth-security.js +188 -0
  65. package/dist/hooks/module-loader.js +28 -0
  66. package/dist/infra/agent-command-binding.js +144 -0
  67. package/dist/infra/backup.js +328 -0
  68. package/dist/infra/channel-account-context.js +173 -0
  69. package/dist/infra/errors.js +53 -13
  70. package/dist/infra/exec-approvals-security.js +217 -0
  71. package/dist/infra/security/command-analyzer.js +257 -0
  72. package/dist/infra/session-cleanup.js +143 -0
  73. package/dist/plugins/loader.js +16 -8
  74. package/dist/security/external-content.js +51 -1
  75. package/dist/sessions/session-costs.js +228 -0
  76. package/dist/shared/param-key.js +16 -0
  77. package/dist/shared/poll-params.js +58 -0
  78. package/dist/shared/polls.js +55 -0
  79. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  80. package/docs/FEATURES.md +523 -0
  81. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  82. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  83. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  84. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  85. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  86. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  87. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  88. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  89. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  90. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  91. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  92. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  93. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  94. package/docs/PHASE-7-SUMMARY.md +144 -0
  95. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  96. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  97. package/docs/README.md +116 -0
  98. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  99. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  100. package/docs/channels/googlechat.md +235 -206
  101. package/docs/channels/irc.md +332 -0
  102. package/docs/channels/nostr.md +255 -168
  103. package/docs/components/command-palette.md +166 -0
  104. package/docs/components/login-gate.md +219 -0
  105. package/docs/getting-started/installation.md +191 -0
  106. package/docs/getting-started/introduction.md +120 -0
  107. package/docs/improvements/USAGE-GUIDE.md +359 -0
  108. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  109. package/docs/reference/deadcode-detection.md +72 -0
  110. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  111. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  112. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  113. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  114. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  115. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  116. package/extensions/googlechat/package.json +11 -28
  117. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  118. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  119. package/extensions/googlechat/src/index.ts +14 -0
  120. package/extensions/irc/node_modules/.bin/tsc +21 -0
  121. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  122. package/extensions/irc/node_modules/.bin/vitest +21 -0
  123. package/extensions/irc/package.json +16 -8
  124. package/extensions/irc/src/index.ts +14 -0
  125. package/extensions/irc/src/irc-channel.test.ts +43 -0
  126. package/extensions/irc/src/irc-channel.ts +191 -0
  127. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  128. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  129. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  130. package/extensions/keyed-async-queue/package.json +20 -0
  131. package/extensions/keyed-async-queue/src/index.ts +14 -0
  132. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  133. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  134. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  135. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  136. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  137. package/extensions/memory-core/package.json +11 -8
  138. package/extensions/memory-core/src/index.ts +14 -0
  139. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  140. package/extensions/memory-core/src/memory-manager.ts +186 -0
  141. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  142. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  143. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  144. package/extensions/nostr/package.json +15 -24
  145. package/extensions/nostr/src/index.ts +14 -0
  146. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  147. package/extensions/nostr/src/nostr-channel.ts +228 -0
  148. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  149. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  150. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  151. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  152. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  153. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  154. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  155. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  156. package/package.json +2 -1
  157. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  158. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  159. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Run Tracking System
3
+ *
4
+ * Tracks active agent runs with AbortController support for cancellation.
5
+ * Provides run lifecycle management and resource cleanup.
6
+ */
7
+ export class RunTracker {
8
+ runs = new Map();
9
+ completedRuns = new Map();
10
+ MAX_COMPLETED_RUNS;
11
+ constructor(options) {
12
+ this.MAX_COMPLETED_RUNS = options?.maxCompletedRuns ?? 1000;
13
+ }
14
+ /**
15
+ * Start tracking a new run
16
+ */
17
+ startRun(runId, params) {
18
+ if (this.runs.has(runId)) {
19
+ throw new Error(`Run ${runId} already exists`);
20
+ }
21
+ const abortController = new AbortController();
22
+ const buffer = new TransformStream();
23
+ const writer = buffer.writable.getWriter();
24
+ const reader = buffer.readable.getReader();
25
+ const state = {
26
+ abortController,
27
+ buffer,
28
+ writer,
29
+ reader,
30
+ startTime: Date.now(),
31
+ model: params.model,
32
+ sessionKey: params.sessionKey,
33
+ runId,
34
+ status: "running",
35
+ metadata: params.metadata,
36
+ };
37
+ this.runs.set(runId, state);
38
+ return state;
39
+ }
40
+ /**
41
+ * Get run state by ID
42
+ */
43
+ getRun(runId) {
44
+ return this.runs.get(runId);
45
+ }
46
+ /**
47
+ * Check if run exists and is active
48
+ */
49
+ hasRun(runId) {
50
+ return this.runs.has(runId);
51
+ }
52
+ /**
53
+ * Get all active runs
54
+ */
55
+ getActiveRuns() {
56
+ return Array.from(this.runs.values());
57
+ }
58
+ /**
59
+ * Get runs by session key
60
+ */
61
+ getRunsBySession(sessionKey) {
62
+ return Array.from(this.runs.values()).filter((run) => run.sessionKey === sessionKey);
63
+ }
64
+ /**
65
+ * Abort a specific run
66
+ */
67
+ async abortRun(runId, reason) {
68
+ const run = this.runs.get(runId);
69
+ if (!run) {
70
+ return false;
71
+ }
72
+ try {
73
+ run.status = "aborted";
74
+ run.abortController.abort(reason);
75
+ // Cancel writer and reader (ignore errors)
76
+ try {
77
+ await run.writer.abort();
78
+ }
79
+ catch {
80
+ // Ignore writer abort errors
81
+ }
82
+ try {
83
+ await run.reader.cancel();
84
+ }
85
+ catch {
86
+ // Ignore reader cancel errors
87
+ }
88
+ // Cleanup
89
+ this.runs.delete(runId);
90
+ this.markCompleted(run, reason ? new Error(reason) : undefined);
91
+ return true;
92
+ }
93
+ catch (error) {
94
+ console.error(`Error aborting run ${runId}:`, error);
95
+ // Still cleanup even if there was an error
96
+ this.runs.delete(runId);
97
+ return false;
98
+ }
99
+ }
100
+ /**
101
+ * Mark run as completed
102
+ */
103
+ async completeRun(runId, metadata) {
104
+ const run = this.runs.get(runId);
105
+ if (!run) {
106
+ return false;
107
+ }
108
+ try {
109
+ run.status = "completed";
110
+ // Close streams
111
+ await run.writer.close();
112
+ // Cleanup
113
+ this.runs.delete(runId);
114
+ this.markCompleted(run, undefined, metadata);
115
+ return true;
116
+ }
117
+ catch (error) {
118
+ console.error(`Error completing run ${runId}:`, error);
119
+ run.status = "failed";
120
+ run.error = error instanceof Error ? error : new Error(String(error));
121
+ this.runs.delete(runId);
122
+ this.markCompleted(run, run.error);
123
+ return false;
124
+ }
125
+ }
126
+ /**
127
+ * Mark run as failed
128
+ */
129
+ async failRun(runId, error) {
130
+ const run = this.runs.get(runId);
131
+ if (!run) {
132
+ return false;
133
+ }
134
+ try {
135
+ run.status = "failed";
136
+ run.error = error;
137
+ // Cleanup streams (ignore errors)
138
+ try {
139
+ await run.writer.abort(error);
140
+ }
141
+ catch {
142
+ // Ignore writer abort errors
143
+ }
144
+ try {
145
+ await run.reader.cancel(error);
146
+ }
147
+ catch {
148
+ // Ignore reader cancel errors
149
+ }
150
+ // Cleanup
151
+ this.runs.delete(runId);
152
+ this.markCompleted(run, error);
153
+ return true;
154
+ }
155
+ catch (cleanupError) {
156
+ console.error(`Error failing run ${runId}:`, cleanupError);
157
+ // Still cleanup even if there was an error
158
+ this.runs.delete(runId);
159
+ this.markCompleted(run, error);
160
+ return false;
161
+ }
162
+ }
163
+ /**
164
+ * Write data to run buffer
165
+ */
166
+ async writeToRun(runId, data) {
167
+ const run = this.runs.get(runId);
168
+ if (!run) {
169
+ throw new Error(`Run ${runId} not found`);
170
+ }
171
+ if (run.status !== "running") {
172
+ throw new Error(`Run ${runId} is not running (status: ${run.status})`);
173
+ }
174
+ if (run.abortController.signal.aborted) {
175
+ throw new Error(`Run ${runId} was aborted`);
176
+ }
177
+ await run.writer.write(data);
178
+ }
179
+ /**
180
+ * Get run duration in milliseconds
181
+ */
182
+ getRunDuration(runId) {
183
+ const run = this.runs.get(runId);
184
+ if (run) {
185
+ return Date.now() - run.startTime;
186
+ }
187
+ const completed = this.completedRuns.get(runId);
188
+ if (completed && completed.endTime) {
189
+ return completed.endTime - completed.startTime;
190
+ }
191
+ return null;
192
+ }
193
+ /**
194
+ * Get statistics about runs
195
+ */
196
+ getStats() {
197
+ const completed = Array.from(this.completedRuns.values());
198
+ const durations = completed
199
+ .filter((r) => r.endTime && r.startTime)
200
+ .map((r) => r.endTime - r.startTime);
201
+ const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
202
+ return {
203
+ activeRuns: this.runs.size,
204
+ completedRuns: completed.filter((r) => r.status === "completed").length,
205
+ abortedRuns: completed.filter((r) => r.status === "aborted").length,
206
+ failedRuns: completed.filter((r) => r.status === "failed").length,
207
+ averageDuration: avgDuration,
208
+ };
209
+ }
210
+ /**
211
+ * Clear all completed runs from memory
212
+ */
213
+ clearCompletedRuns() {
214
+ this.completedRuns.clear();
215
+ }
216
+ /**
217
+ * Destroy tracker and cleanup all runs
218
+ */
219
+ async destroy() {
220
+ // Abort all active runs
221
+ const abortPromises = Array.from(this.runs.keys()).map((runId) => this.abortRun(runId, "Tracker destroyed"));
222
+ await Promise.all(abortPromises);
223
+ this.runs.clear();
224
+ this.completedRuns.clear();
225
+ }
226
+ markCompleted(run, error, metadata) {
227
+ const completedMetadata = {
228
+ runId: run.runId,
229
+ sessionKey: run.sessionKey,
230
+ model: run.model,
231
+ startTime: run.startTime,
232
+ endTime: Date.now(),
233
+ status: run.status,
234
+ error: error?.message,
235
+ ...run.metadata,
236
+ ...metadata,
237
+ };
238
+ this.completedRuns.set(run.runId, completedMetadata);
239
+ // Evict oldest if at capacity
240
+ if (this.completedRuns.size > this.MAX_COMPLETED_RUNS) {
241
+ const oldest = Array.from(this.completedRuns.entries()).sort((a, b) => a[1].startTime - b[1].startTime)[0];
242
+ if (oldest) {
243
+ this.completedRuns.delete(oldest[0]);
244
+ }
245
+ }
246
+ }
247
+ }
248
+ /**
249
+ * Generate unique run ID
250
+ */
251
+ export function generateRunId() {
252
+ return `run_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
253
+ }
@@ -2,6 +2,7 @@ import { loadConfig } from "../../config/config.js";
2
2
  import { listDevicePairing } from "../../infra/device-pairing.js";
3
3
  import { approveNodePairing, listNodePairing, rejectNodePairing, renamePairedNode, requestNodePairing, verifyNodeToken, } from "../../infra/node-pairing.js";
4
4
  import { loadApnsRegistration, resolveApnsAuthConfigFromEnv, sendApnsAlert, sendApnsBackgroundWake, } from "../../infra/push-apns.js";
5
+ import { CommandAnalyzer } from "../../infra/security/command-analyzer.js";
5
6
  import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-command-policy.js";
6
7
  import { sanitizeNodeInvokeParamsForForwarding } from "../node-invoke-sanitize.js";
7
8
  import { ErrorCodes, errorShape, validateNodeDescribeParams, validateNodeEventParams, validateNodeInvokeParams, validateNodeListParams, validateNodePairApproveParams, validateNodePairListParams, validateNodePairRejectParams, validateNodePairRequestParams, validateNodePairVerifyParams, validateNodeRenameParams, } from "../protocol/index.js";
@@ -529,6 +530,19 @@ export const nodeHandlers = {
529
530
  }));
530
531
  return;
531
532
  }
533
+ // Command security analysis
534
+ const commandAnalyzer = new CommandAnalyzer();
535
+ const analysis = commandAnalyzer.analyze(command, []);
536
+ if (analysis.blockedReason) {
537
+ context.logGateway.warn(`node.invoke blocked command=${command} nodeId=${nodeId} reason=${analysis.blockedReason}`);
538
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, analysis.blockedReason, {
539
+ details: { command, riskLevel: analysis.riskLevel },
540
+ }));
541
+ return;
542
+ }
543
+ if (analysis.requiresApproval && analysis.riskLevel !== "low") {
544
+ context.logGateway.info(`node.invoke requires approval command=${command} nodeId=${nodeId} riskLevel=${analysis.riskLevel}`);
545
+ }
532
546
  const forwardedParams = sanitizeNodeInvokeParamsForForwarding({
533
547
  command,
534
548
  rawParams: p.params,
@@ -0,0 +1,188 @@
1
+ /**
2
+ * WebSocket Pre-Authentication Security Hardening
3
+ *
4
+ * Security fixes for WebSocket pre-auth:
5
+ * - Shorten unauthenticated handshake retention
6
+ * - Reject oversized pre-auth frames
7
+ * - Fail-closed for malformed frames
8
+ */
9
+ /**
10
+ * Default security configuration
11
+ *
12
+ * Security: Hardened values for production
13
+ * - 5 second handshake timeout (reduced from 10s)
14
+ * - 1KB max pre-auth frame size (prevents DoS)
15
+ * - 3 max pre-auth frames (limits attack surface)
16
+ */
17
+ export const DEFAULT_PREAUTH_CONFIG = {
18
+ handshakeTimeoutMs: 5_000, // 5 seconds (reduced from 10s)
19
+ maxPreAuthFrameSize: 1024, // 1KB
20
+ maxPreAuthFrames: 3,
21
+ };
22
+ /**
23
+ * Create a new pre-auth connection state
24
+ */
25
+ export function createPreAuthConnectionState(ws, config = DEFAULT_PREAUTH_CONFIG) {
26
+ return {
27
+ ws,
28
+ createdAt: Date.now(),
29
+ frameCount: 0,
30
+ totalBytesReceived: 0,
31
+ authenticated: false,
32
+ config,
33
+ };
34
+ }
35
+ /**
36
+ * Start handshake timeout timer
37
+ *
38
+ * Security: Shorter timeout reduces attack surface for unauthenticated connections
39
+ */
40
+ export function startHandshakeTimeout(state, onTimeout) {
41
+ // Clear any existing timeout
42
+ if (state.handshakeTimeout) {
43
+ clearTimeout(state.handshakeTimeout);
44
+ }
45
+ // Set new timeout with hardened value
46
+ state.handshakeTimeout = setTimeout(() => {
47
+ if (!state.authenticated) {
48
+ onTimeout();
49
+ }
50
+ }, state.config.handshakeTimeoutMs);
51
+ }
52
+ /**
53
+ * Clear handshake timeout
54
+ */
55
+ export function clearHandshakeTimeout(state) {
56
+ if (state.handshakeTimeout) {
57
+ clearTimeout(state.handshakeTimeout);
58
+ state.handshakeTimeout = undefined;
59
+ }
60
+ }
61
+ /**
62
+ * Validate pre-auth frame size
63
+ *
64
+ * Security: Reject oversized frames before authentication to prevent DoS
65
+ *
66
+ * Returns true if frame is acceptable, false if it should be rejected
67
+ */
68
+ export function validatePreAuthFrameSize(state, frameSize) {
69
+ // Check single frame size
70
+ if (frameSize > state.config.maxPreAuthFrameSize) {
71
+ return {
72
+ valid: false,
73
+ reason: `Pre-auth frame too large: ${frameSize} bytes (max: ${state.config.maxPreAuthFrameSize})`,
74
+ };
75
+ }
76
+ // Check cumulative size
77
+ const newTotal = state.totalBytesReceived + frameSize;
78
+ const maxCumulativeSize = state.config.maxPreAuthFrameSize * state.config.maxPreAuthFrames;
79
+ if (newTotal > maxCumulativeSize) {
80
+ return {
81
+ valid: false,
82
+ reason: `Pre-auth cumulative size exceeded: ${newTotal} bytes (max: ${maxCumulativeSize})`,
83
+ };
84
+ }
85
+ return { valid: true };
86
+ }
87
+ /**
88
+ * Track pre-auth frame reception
89
+ *
90
+ * Security: Enforce frame count and size limits
91
+ *
92
+ * Returns true if frame is acceptable, false if connection should be closed
93
+ */
94
+ export function trackPreAuthFrame(state, frameSize) {
95
+ // Check frame count limit
96
+ if (state.frameCount >= state.config.maxPreAuthFrames) {
97
+ return {
98
+ accepted: false,
99
+ reason: `Max pre-auth frames exceeded: ${state.frameCount} (max: ${state.config.maxPreAuthFrames})`,
100
+ };
101
+ }
102
+ // Validate frame size
103
+ const sizeValidation = validatePreAuthFrameSize(state, frameSize);
104
+ if (!sizeValidation.valid) {
105
+ return { accepted: false, reason: sizeValidation.reason };
106
+ }
107
+ // Update counters
108
+ state.frameCount++;
109
+ state.totalBytesReceived += frameSize;
110
+ return { accepted: true };
111
+ }
112
+ /**
113
+ * Mark connection as authenticated
114
+ *
115
+ * Security: Clear pre-auth restrictions after successful authentication
116
+ */
117
+ export function markAuthenticated(state) {
118
+ state.authenticated = true;
119
+ clearHandshakeTimeout(state);
120
+ }
121
+ /**
122
+ * Check if connection is still in pre-auth state
123
+ */
124
+ export function isPreAuth(state) {
125
+ return !state.authenticated;
126
+ }
127
+ /**
128
+ * Get connection age in milliseconds
129
+ */
130
+ export function getConnectionAge(state) {
131
+ return Date.now() - state.createdAt;
132
+ }
133
+ /**
134
+ * Analyze WebSocket connection security
135
+ */
136
+ export function analyzeWebSocketSecurity(state) {
137
+ const age = getConnectionAge(state);
138
+ const timeoutRemaining = state.handshakeTimeout
139
+ ? Math.max(0, state.config.handshakeTimeoutMs - age)
140
+ : undefined;
141
+ // Determine security level
142
+ let securityLevel;
143
+ if (!state.authenticated && age > state.config.handshakeTimeoutMs) {
144
+ securityLevel = "critical"; // Overdue for auth
145
+ }
146
+ else if (!state.authenticated && age > state.config.handshakeTimeoutMs / 2) {
147
+ securityLevel = "low"; // Approaching timeout
148
+ }
149
+ else if (!state.authenticated) {
150
+ securityLevel = "medium"; // Normal pre-auth
151
+ }
152
+ else {
153
+ securityLevel = "high"; // Authenticated
154
+ }
155
+ return {
156
+ isPreAuth: isPreAuth(state),
157
+ connectionAge: age,
158
+ frameCount: state.frameCount,
159
+ totalBytesReceived: state.totalBytesReceived,
160
+ handshakeTimeoutRemaining: timeoutRemaining,
161
+ securityLevel,
162
+ };
163
+ }
164
+ /**
165
+ * Get security event message for logging
166
+ */
167
+ export function getSecurityEventMessage(event, state) {
168
+ const analysis = analyzeWebSocketSecurity(state);
169
+ switch (event) {
170
+ case "handshake_timeout":
171
+ return `WebSocket handshake timeout after ${analysis.connectionAge}ms (pre-auth)`;
172
+ case "frame_size_exceeded":
173
+ return `WebSocket pre-auth frame size exceeded: ${analysis.totalBytesReceived} bytes`;
174
+ case "frame_count_exceeded":
175
+ return `WebSocket pre-auth frame count exceeded: ${analysis.frameCount} frames`;
176
+ case "authenticated":
177
+ return `WebSocket authenticated successfully after ${analysis.connectionAge}ms`;
178
+ default:
179
+ return `WebSocket security event: ${event}`;
180
+ }
181
+ }
182
+ /**
183
+ * Cleanup pre-auth connection state
184
+ */
185
+ export function cleanupPreAuthConnectionState(state) {
186
+ clearHandshakeTimeout(state);
187
+ state.ws.removeAllListeners();
188
+ }
@@ -0,0 +1,28 @@
1
+ import { pathToFileURL } from "node:url";
2
+ export function resolveFileModuleUrl(params) {
3
+ const url = pathToFileURL(params.modulePath).href;
4
+ if (!params.cacheBust) {
5
+ return url;
6
+ }
7
+ const ts = params.nowMs ?? Date.now();
8
+ return `${url}?t=${ts}`;
9
+ }
10
+ export async function importFileModule(params) {
11
+ const specifier = resolveFileModuleUrl(params);
12
+ return (await import(specifier));
13
+ }
14
+ export function resolveFunctionModuleExport(params) {
15
+ const explicitExport = params.exportName?.trim();
16
+ if (explicitExport) {
17
+ const candidate = params.mod[explicitExport];
18
+ return typeof candidate === "function" ? candidate : undefined;
19
+ }
20
+ const fallbacks = params.fallbackExportNames ?? ["default"];
21
+ for (const exportName of fallbacks) {
22
+ const candidate = params.mod[exportName];
23
+ if (typeof candidate === "function") {
24
+ return candidate;
25
+ }
26
+ }
27
+ return undefined;
28
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Agent Command Binding System
3
+ *
4
+ * Allows binding specific commands to agents.
5
+ * Implemented from scratch for Pool Bot architecture.
6
+ */
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ const BINDINGS_FILE = path.join(process.cwd(), ".poolbot", "agent-command-bindings.json");
10
+ /**
11
+ * Load command bindings from file
12
+ */
13
+ export async function loadCommandBindings() {
14
+ try {
15
+ const data = await fs.readFile(BINDINGS_FILE, "utf-8");
16
+ return JSON.parse(data);
17
+ }
18
+ catch (error) {
19
+ if (error.code === "ENOENT") {
20
+ return { bindings: {} };
21
+ }
22
+ throw error;
23
+ }
24
+ }
25
+ /**
26
+ * Save command bindings to file
27
+ */
28
+ export async function saveCommandBindings(store) {
29
+ await fs.mkdir(path.dirname(BINDINGS_FILE), { recursive: true });
30
+ await fs.writeFile(BINDINGS_FILE, JSON.stringify(store, null, 2));
31
+ }
32
+ /**
33
+ * Bind commands to an agent
34
+ */
35
+ export async function bindCommandsToAgent(params) {
36
+ const { agentId, commands, allowlist = true } = params;
37
+ const now = Date.now();
38
+ const store = await loadCommandBindings();
39
+ const existing = store.bindings[agentId];
40
+ const binding = {
41
+ agentId,
42
+ commands: [...new Set(commands)], // Remove duplicates
43
+ allowlist,
44
+ createdAt: existing?.createdAt || now,
45
+ updatedAt: now,
46
+ };
47
+ store.bindings[agentId] = binding;
48
+ await saveCommandBindings(store);
49
+ return binding;
50
+ }
51
+ /**
52
+ * Unbind commands from an agent
53
+ */
54
+ export async function unbindCommandsFromAgent(agentId) {
55
+ const store = await loadCommandBindings();
56
+ if (!store.bindings[agentId]) {
57
+ return false;
58
+ }
59
+ delete store.bindings[agentId];
60
+ await saveCommandBindings(store);
61
+ return true;
62
+ }
63
+ /**
64
+ * Get command bindings for an agent
65
+ */
66
+ export async function getAgentBindings(agentId) {
67
+ const store = await loadCommandBindings();
68
+ return store.bindings[agentId];
69
+ }
70
+ /**
71
+ * Get all command bindings
72
+ */
73
+ export async function getAllBindings() {
74
+ const store = await loadCommandBindings();
75
+ return Object.values(store.bindings);
76
+ }
77
+ /**
78
+ * Check if a command is allowed for an agent
79
+ */
80
+ export async function isCommandAllowedForAgent(params) {
81
+ const { agentId, command } = params;
82
+ const binding = await getAgentBindings(agentId);
83
+ // No binding = all commands allowed
84
+ if (!binding) {
85
+ return { allowed: true, reason: "No binding - all commands allowed" };
86
+ }
87
+ const commandExists = binding.commands.includes(command);
88
+ if (binding.allowlist) {
89
+ // Allowlist mode: only these commands are allowed
90
+ if (commandExists) {
91
+ return { allowed: true, reason: "Command in allowlist" };
92
+ }
93
+ else {
94
+ return { allowed: false, reason: "Command not in allowlist" };
95
+ }
96
+ }
97
+ else {
98
+ // Denylist mode: all commands allowed except these
99
+ if (commandExists) {
100
+ return { allowed: false, reason: "Command in denylist" };
101
+ }
102
+ else {
103
+ return { allowed: true, reason: "Command not in denylist" };
104
+ }
105
+ }
106
+ }
107
+ /**
108
+ * Add commands to an agent's binding
109
+ */
110
+ export async function addCommandsToAgent(params) {
111
+ const { agentId, commands } = params;
112
+ const binding = await getAgentBindings(agentId);
113
+ if (!binding) {
114
+ return bindCommandsToAgent({ agentId, commands });
115
+ }
116
+ const updatedCommands = [...new Set([...binding.commands, ...commands])];
117
+ return bindCommandsToAgent({ agentId, commands: updatedCommands, allowlist: binding.allowlist });
118
+ }
119
+ /**
120
+ * Remove commands from an agent's binding
121
+ */
122
+ export async function removeCommandsFromAgent(params) {
123
+ const { agentId, commands } = params;
124
+ const binding = await getAgentBindings(agentId);
125
+ if (!binding) {
126
+ throw new Error(`No binding found for agent ${agentId}`);
127
+ }
128
+ const updatedCommands = binding.commands.filter((cmd) => !commands.includes(cmd));
129
+ return bindCommandsToAgent({ agentId, commands: updatedCommands, allowlist: binding.allowlist });
130
+ }
131
+ /**
132
+ * Toggle binding mode (allowlist/denylist)
133
+ */
134
+ export async function toggleBindingMode(agentId) {
135
+ const binding = await getAgentBindings(agentId);
136
+ if (!binding) {
137
+ throw new Error(`No binding found for agent ${agentId}`);
138
+ }
139
+ return bindCommandsToAgent({
140
+ agentId,
141
+ commands: binding.commands,
142
+ allowlist: !binding.allowlist,
143
+ });
144
+ }