@tjamescouch/agentchat 0.22.1 → 0.23.0

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 (153) hide show
  1. package/Dockerfile +1 -1
  2. package/dist/bin/agentchat.d.ts +7 -0
  3. package/dist/bin/agentchat.d.ts.map +1 -0
  4. package/dist/bin/agentchat.js +1511 -0
  5. package/dist/bin/agentchat.js.map +1 -0
  6. package/dist/lib/allowlist.d.ts +77 -0
  7. package/dist/lib/allowlist.d.ts.map +1 -0
  8. package/dist/lib/allowlist.js +151 -0
  9. package/dist/lib/allowlist.js.map +1 -0
  10. package/dist/lib/client.d.ts +147 -0
  11. package/dist/lib/client.d.ts.map +1 -0
  12. package/dist/lib/client.js +704 -0
  13. package/dist/lib/client.js.map +1 -0
  14. package/dist/lib/daemon.d.ts +122 -0
  15. package/dist/lib/daemon.d.ts.map +1 -0
  16. package/dist/lib/daemon.js +523 -0
  17. package/dist/lib/daemon.js.map +1 -0
  18. package/dist/lib/deploy/akash.d.ts +271 -0
  19. package/dist/lib/deploy/akash.d.ts.map +1 -0
  20. package/dist/lib/deploy/akash.js +671 -0
  21. package/dist/lib/deploy/akash.js.map +1 -0
  22. package/dist/lib/deploy/config.d.ts +62 -0
  23. package/dist/lib/deploy/config.d.ts.map +1 -0
  24. package/dist/lib/deploy/config.js +116 -0
  25. package/dist/lib/deploy/config.js.map +1 -0
  26. package/dist/lib/deploy/docker.d.ts +37 -0
  27. package/dist/lib/deploy/docker.d.ts.map +1 -0
  28. package/dist/lib/deploy/docker.js +122 -0
  29. package/dist/lib/deploy/docker.js.map +1 -0
  30. package/dist/lib/deploy/index.d.ts +11 -0
  31. package/dist/lib/deploy/index.d.ts.map +1 -0
  32. package/dist/lib/deploy/index.js +11 -0
  33. package/dist/lib/deploy/index.js.map +1 -0
  34. package/dist/lib/escrow-hooks.d.ts +199 -0
  35. package/dist/lib/escrow-hooks.d.ts.map +1 -0
  36. package/dist/lib/escrow-hooks.js +221 -0
  37. package/dist/lib/escrow-hooks.js.map +1 -0
  38. package/dist/lib/identity.d.ts +134 -0
  39. package/dist/lib/identity.d.ts.map +1 -0
  40. package/dist/lib/identity.js +334 -0
  41. package/dist/lib/identity.js.map +1 -0
  42. package/dist/lib/jitter.d.ts +42 -0
  43. package/dist/lib/jitter.d.ts.map +1 -0
  44. package/{lib/jitter.ts → dist/lib/jitter.js} +10 -18
  45. package/dist/lib/jitter.js.map +1 -0
  46. package/dist/lib/proposals.d.ts +223 -0
  47. package/dist/lib/proposals.d.ts.map +1 -0
  48. package/dist/lib/proposals.js +379 -0
  49. package/dist/lib/proposals.js.map +1 -0
  50. package/dist/lib/protocol.d.ts +220 -0
  51. package/dist/lib/protocol.d.ts.map +1 -0
  52. package/dist/lib/protocol.js +507 -0
  53. package/dist/lib/protocol.js.map +1 -0
  54. package/dist/lib/receipts.d.ts +134 -0
  55. package/dist/lib/receipts.d.ts.map +1 -0
  56. package/dist/lib/receipts.js +270 -0
  57. package/dist/lib/receipts.js.map +1 -0
  58. package/dist/lib/reputation.d.ts +250 -0
  59. package/dist/lib/reputation.d.ts.map +1 -0
  60. package/dist/lib/reputation.js +586 -0
  61. package/dist/lib/reputation.js.map +1 -0
  62. package/dist/lib/security.d.ts +27 -0
  63. package/dist/lib/security.d.ts.map +1 -0
  64. package/dist/lib/security.js +150 -0
  65. package/dist/lib/security.js.map +1 -0
  66. package/dist/lib/server/handlers/admin.d.ts +26 -0
  67. package/dist/lib/server/handlers/admin.d.ts.map +1 -0
  68. package/dist/lib/server/handlers/admin.js +76 -0
  69. package/dist/lib/server/handlers/admin.js.map +1 -0
  70. package/dist/lib/server/handlers/identity.d.ts +36 -0
  71. package/dist/lib/server/handlers/identity.d.ts.map +1 -0
  72. package/dist/lib/server/handlers/identity.js +330 -0
  73. package/dist/lib/server/handlers/identity.js.map +1 -0
  74. package/dist/lib/server/handlers/index.d.ts +10 -0
  75. package/dist/lib/server/handlers/index.d.ts.map +1 -0
  76. package/dist/lib/server/handlers/index.js +15 -0
  77. package/dist/lib/server/handlers/index.js.map +1 -0
  78. package/dist/lib/server/handlers/message.d.ts +47 -0
  79. package/dist/lib/server/handlers/message.d.ts.map +1 -0
  80. package/dist/lib/server/handlers/message.js +265 -0
  81. package/dist/lib/server/handlers/message.js.map +1 -0
  82. package/dist/lib/server/handlers/presence.d.ts +18 -0
  83. package/dist/lib/server/handlers/presence.d.ts.map +1 -0
  84. package/dist/lib/server/handlers/presence.js +35 -0
  85. package/dist/lib/server/handlers/presence.js.map +1 -0
  86. package/dist/lib/server/handlers/proposal.d.ts +38 -0
  87. package/dist/lib/server/handlers/proposal.d.ts.map +1 -0
  88. package/dist/lib/server/handlers/proposal.js +273 -0
  89. package/dist/lib/server/handlers/proposal.js.map +1 -0
  90. package/dist/lib/server/handlers/skills.d.ts +22 -0
  91. package/dist/lib/server/handlers/skills.d.ts.map +1 -0
  92. package/dist/lib/server/handlers/skills.js +119 -0
  93. package/dist/lib/server/handlers/skills.js.map +1 -0
  94. package/dist/lib/server-directory.d.ts +85 -0
  95. package/dist/lib/server-directory.d.ts.map +1 -0
  96. package/dist/lib/server-directory.js +177 -0
  97. package/dist/lib/server-directory.js.map +1 -0
  98. package/dist/lib/server.d.ts +162 -0
  99. package/dist/lib/server.d.ts.map +1 -0
  100. package/dist/lib/server.js +602 -0
  101. package/dist/lib/server.js.map +1 -0
  102. package/dist/lib/types.d.ts +461 -0
  103. package/dist/lib/types.d.ts.map +1 -0
  104. package/dist/lib/types.js +98 -0
  105. package/dist/lib/types.js.map +1 -0
  106. package/package.json +22 -13
  107. package/bin/agentchat.js +0 -1617
  108. package/bin/agentchat.ts +0 -1812
  109. package/lib/allowlist.js +0 -162
  110. package/lib/chat.py +0 -241
  111. package/lib/client.js +0 -821
  112. package/lib/client.ts +0 -877
  113. package/lib/daemon.js +0 -562
  114. package/lib/daemon.ts +0 -662
  115. package/lib/deploy/akash.js +0 -811
  116. package/lib/deploy/config.js +0 -128
  117. package/lib/deploy/docker.js +0 -132
  118. package/lib/deploy/index.js +0 -24
  119. package/lib/elo_swarm.py +0 -569
  120. package/lib/escrow-hooks.js +0 -237
  121. package/lib/escrow-hooks.ts +0 -391
  122. package/lib/identity.js +0 -376
  123. package/lib/identity.ts +0 -412
  124. package/lib/jitter.js +0 -54
  125. package/lib/proposals.js +0 -426
  126. package/lib/proposals.ts +0 -612
  127. package/lib/protocol.js +0 -516
  128. package/lib/receipts.js +0 -294
  129. package/lib/receipts.ts +0 -359
  130. package/lib/reputation.js +0 -664
  131. package/lib/reputation.ts +0 -790
  132. package/lib/security.js +0 -183
  133. package/lib/server/handlers/admin.js +0 -94
  134. package/lib/server/handlers/identity.js +0 -258
  135. package/lib/server/handlers/index.js +0 -42
  136. package/lib/server/handlers/message.js +0 -319
  137. package/lib/server/handlers/presence.js +0 -45
  138. package/lib/server/handlers/proposal.js +0 -358
  139. package/lib/server/handlers/skills.js +0 -141
  140. package/lib/server-directory.js +0 -190
  141. package/lib/server-directory.ts +0 -232
  142. package/lib/server.js +0 -633
  143. package/lib/server.ts +0 -698
  144. package/lib/supervisor/USAGE.md +0 -110
  145. package/lib/supervisor/agent-health.sh +0 -107
  146. package/lib/supervisor/agent-monitor.sh +0 -123
  147. package/lib/supervisor/agent-supervisor.sh +0 -135
  148. package/lib/supervisor/agentctl.sh +0 -266
  149. package/lib/supervisor/god-backup.sh +0 -126
  150. package/lib/supervisor/god-watchdog.sh +0 -107
  151. package/lib/supervisor/killswitch.sh +0 -43
  152. package/lib/supervisor/notify.sh +0 -19
  153. package/lib/types.ts +0 -433
package/lib/server.ts DELETED
@@ -1,698 +0,0 @@
1
- /**
2
- * AgentChat Server
3
- * WebSocket relay for agent-to-agent communication
4
- */
5
-
6
- import { WebSocketServer, WebSocket } from 'ws';
7
- import http, { IncomingMessage, ServerResponse } from 'http';
8
- import https from 'https';
9
- import fs from 'fs';
10
- import {
11
- ClientMessageType,
12
- ServerMessageType,
13
- ErrorCode,
14
- Skill,
15
- ClientMessage,
16
- ServerMessage,
17
- PresenceStatus,
18
- } from './types.js';
19
- import {
20
- createMessage,
21
- createError,
22
- validateClientMessage,
23
- serialize,
24
- } from './protocol.js';
25
- import { ProposalStore } from './proposals.js';
26
- import { ReputationStore } from './reputation.js';
27
- import { EscrowHooks } from './escrow-hooks.js';
28
-
29
- // Import extracted handlers
30
- import {
31
- handleMsg,
32
- handleJoin,
33
- handleLeave,
34
- handleListChannels,
35
- handleListAgents,
36
- handleCreateChannel,
37
- handleInvite,
38
- } from './server/handlers/message.js';
39
- import {
40
- handleProposal,
41
- handleAccept,
42
- handleReject,
43
- handleComplete,
44
- handleDispute,
45
- } from './server/handlers/proposal.js';
46
- import {
47
- handleIdentify,
48
- handleVerifyRequest,
49
- handleVerifyResponse,
50
- } from './server/handlers/identity.js';
51
- import {
52
- handleRegisterSkills,
53
- handleSearchSkills,
54
- } from './server/handlers/skills.js';
55
- import {
56
- handleSetPresence,
57
- } from './server/handlers/presence.js';
58
-
59
- // Extended WebSocket with custom properties
60
- interface ExtendedWebSocket extends WebSocket {
61
- _connectedAt?: number;
62
- _realIp?: string;
63
- _userAgent?: string;
64
- _msgTimestamps?: number[];
65
- }
66
-
67
- // Agent info stored per connection
68
- export interface AgentState {
69
- id: string;
70
- name?: string;
71
- channels: Set<string>;
72
- pubkey?: string | null;
73
- presence?: PresenceStatus;
74
- status_text?: string | null;
75
- }
76
-
77
- // Channel state
78
- export interface ChannelState {
79
- name: string;
80
- inviteOnly: boolean;
81
- invited: Set<string>;
82
- agents: Set<ExtendedWebSocket>;
83
- messageBuffer: ServerMessage[];
84
- }
85
-
86
- // Skill registration entry
87
- export interface SkillRegistration {
88
- skills: Skill[];
89
- registered_at: number;
90
- sig: string;
91
- }
92
-
93
- // Pending verification request
94
- export interface PendingVerification {
95
- from: string;
96
- target: string;
97
- nonce: string;
98
- expires: number;
99
- }
100
-
101
- // Server options
102
- export interface AgentChatServerOptions {
103
- port?: number;
104
- host?: string;
105
- name?: string;
106
- logMessages?: boolean;
107
- cert?: string | null;
108
- key?: string | null;
109
- rateLimitMs?: number;
110
- messageBufferSize?: number;
111
- idleTimeoutMs?: number;
112
- verificationTimeoutMs?: number;
113
- logger?: Console;
114
- escrowHandlers?: Record<string, (payload: unknown) => Promise<void>>;
115
- }
116
-
117
- // Health status response
118
- export interface HealthStatus {
119
- status: string;
120
- server: string;
121
- version: string;
122
- uptime_seconds: number;
123
- started_at: string | null;
124
- agents: {
125
- connected: number;
126
- with_identity: number;
127
- };
128
- channels: {
129
- total: number;
130
- public: number;
131
- };
132
- proposals: ReturnType<ProposalStore['stats']>;
133
- timestamp: string;
134
- }
135
-
136
- export class AgentChatServer {
137
- port: number;
138
- host: string;
139
- serverName: string;
140
- logMessages: boolean;
141
-
142
- // TLS options
143
- tlsCert: string | null;
144
- tlsKey: string | null;
145
-
146
- // Rate limiting
147
- rateLimitMs: number;
148
-
149
- // Message buffer size per channel
150
- messageBufferSize: number;
151
-
152
- // State
153
- agents: Map<ExtendedWebSocket, AgentState>;
154
- agentById: Map<string, ExtendedWebSocket>;
155
- channels: Map<string, ChannelState>;
156
- lastMessageTime: Map<ExtendedWebSocket, number>;
157
- pubkeyToId: Map<string, string>;
158
-
159
- // Idle prompt settings
160
- idleTimeoutMs: number;
161
- idleCheckInterval: NodeJS.Timeout | null;
162
- channelLastActivity: Map<string, number>;
163
-
164
- // Conversation starters
165
- conversationStarters: string[];
166
-
167
- // Proposal store
168
- proposals: ProposalStore;
169
-
170
- // Skills registry
171
- skillsRegistry: Map<string, SkillRegistration>;
172
-
173
- // Reputation store
174
- reputationStore: ReputationStore;
175
-
176
- // Escrow hooks
177
- escrowHooks: EscrowHooks;
178
-
179
- // Pending verifications
180
- pendingVerifications: Map<string, PendingVerification>;
181
- verificationTimeoutMs: number;
182
-
183
- wss: WebSocketServer | null;
184
- httpServer: http.Server | https.Server | null;
185
- startedAt: number | null;
186
-
187
- constructor(options: AgentChatServerOptions = {}) {
188
- this.port = options.port || 6667;
189
- this.host = options.host || '0.0.0.0';
190
- this.serverName = options.name || 'agentchat';
191
- this.logMessages = options.logMessages || false;
192
-
193
- // TLS options
194
- this.tlsCert = options.cert || null;
195
- this.tlsKey = options.key || null;
196
-
197
- // Rate limiting: 1 message per second per agent
198
- this.rateLimitMs = options.rateLimitMs || 1000;
199
-
200
- // Message buffer size per channel (for replay on join)
201
- this.messageBufferSize = options.messageBufferSize || 20;
202
-
203
- // State
204
- this.agents = new Map();
205
- this.agentById = new Map();
206
- this.channels = new Map();
207
- this.lastMessageTime = new Map();
208
- this.pubkeyToId = new Map();
209
-
210
- // Idle prompt settings
211
- this.idleTimeoutMs = options.idleTimeoutMs || 5 * 60 * 1000; // 5 minutes default
212
- this.idleCheckInterval = null;
213
- this.channelLastActivity = new Map();
214
-
215
- // Conversation starters for idle prompts
216
- this.conversationStarters = [
217
- "It's quiet here. What's everyone working on?",
218
- "Any agents want to test the proposal system? Try: PROPOSE @agent \"task\" --amount 0",
219
- "Topic: What capabilities would make agent coordination more useful?",
220
- "Looking for collaborators? Post your skills and what you're building.",
221
- "Challenge: Describe your most interesting current project in one sentence.",
222
- "Question: What's the hardest part about agent-to-agent coordination?",
223
- "Idle hands... anyone want to pair on a spec or code review?",
224
- ];
225
-
226
- // Create default channels
227
- this._createChannel('#general', false);
228
- this._createChannel('#agents', false);
229
- this._createChannel('#discovery', false);
230
-
231
- // Proposal store for structured negotiations
232
- this.proposals = new ProposalStore();
233
-
234
- // Skills registry
235
- this.skillsRegistry = new Map();
236
-
237
- // Reputation store for ELO ratings
238
- this.reputationStore = new ReputationStore();
239
-
240
- // Escrow hooks for external integrations
241
- this.escrowHooks = new EscrowHooks({ logger: options.logger || console });
242
-
243
- // Register external escrow handlers if provided
244
- if (options.escrowHandlers) {
245
- for (const [event, handler] of Object.entries(options.escrowHandlers)) {
246
- this.escrowHooks.on(event, handler);
247
- }
248
- }
249
-
250
- // Pending verification requests
251
- this.pendingVerifications = new Map();
252
- this.verificationTimeoutMs = options.verificationTimeoutMs || 30000;
253
-
254
- this.wss = null;
255
- this.httpServer = null;
256
- this.startedAt = null;
257
- }
258
-
259
- /**
260
- * Register a handler for escrow events
261
- */
262
- onEscrow(event: string, handler: (payload: unknown) => Promise<void>): () => void {
263
- return this.escrowHooks.on(event, handler);
264
- }
265
-
266
- /**
267
- * Get server health status
268
- */
269
- getHealth(): HealthStatus {
270
- const now = Date.now();
271
- const uptime = this.startedAt ? Math.floor((now - this.startedAt) / 1000) : 0;
272
-
273
- return {
274
- status: 'healthy',
275
- server: this.serverName,
276
- version: process.env.npm_package_version || '0.0.0',
277
- uptime_seconds: uptime,
278
- started_at: this.startedAt ? new Date(this.startedAt).toISOString() : null,
279
- agents: {
280
- connected: this.agents.size,
281
- with_identity: Array.from(this.agents.values()).filter(a => a.pubkey).length
282
- },
283
- channels: {
284
- total: this.channels.size,
285
- public: Array.from(this.channels.values()).filter(c => !c.inviteOnly).length
286
- },
287
- proposals: this.proposals.stats(),
288
- timestamp: new Date(now).toISOString()
289
- };
290
- }
291
-
292
- _createChannel(name: string, inviteOnly: boolean = false): ChannelState {
293
- if (!this.channels.has(name)) {
294
- this.channels.set(name, {
295
- name,
296
- inviteOnly,
297
- invited: new Set(),
298
- agents: new Set(),
299
- messageBuffer: []
300
- });
301
- }
302
- return this.channels.get(name)!;
303
- }
304
-
305
- /**
306
- * Add a message to a channel's buffer (circular buffer)
307
- */
308
- _bufferMessage(channel: string, msg: ServerMessage): void {
309
- const ch = this.channels.get(channel);
310
- if (!ch) return;
311
-
312
- ch.messageBuffer.push(msg);
313
-
314
- // Trim to buffer size
315
- if (ch.messageBuffer.length > this.messageBufferSize) {
316
- ch.messageBuffer.shift();
317
- }
318
- }
319
-
320
- /**
321
- * Replay buffered messages to a newly joined agent
322
- */
323
- _replayMessages(ws: ExtendedWebSocket, channel: string): void {
324
- const ch = this.channels.get(channel);
325
- if (!ch || ch.messageBuffer.length === 0) return;
326
-
327
- for (const msg of ch.messageBuffer) {
328
- // Send with replay flag so client knows it's history
329
- this._send(ws, { ...msg, replay: true } as ServerMessage & { replay: boolean });
330
- }
331
- }
332
-
333
- _log(event: string, data: Record<string, unknown> = {}): void {
334
- const entry = {
335
- ts: new Date().toISOString(),
336
- event,
337
- ...data
338
- };
339
- console.error(JSON.stringify(entry));
340
- }
341
-
342
- _send(ws: ExtendedWebSocket, msg: ServerMessage | (ServerMessage & { replay: boolean })): void {
343
- if (ws.readyState === 1) { // OPEN
344
- ws.send(serialize(msg));
345
- }
346
- }
347
-
348
- _broadcast(channel: string, msg: ServerMessage, excludeWs: ExtendedWebSocket | null = null): void {
349
- const ch = this.channels.get(channel);
350
- if (!ch) return;
351
-
352
- for (const ws of ch.agents) {
353
- if (ws !== excludeWs) {
354
- this._send(ws, msg);
355
- }
356
- }
357
- }
358
-
359
- _getAgentId(ws: ExtendedWebSocket): string | null {
360
- const agent = this.agents.get(ws);
361
- return agent ? `@${agent.id}` : null;
362
- }
363
-
364
- start(): this {
365
- const tls = !!(this.tlsCert && this.tlsKey);
366
- this.startedAt = Date.now();
367
-
368
- // HTTP request handler for health endpoint
369
- const httpHandler = (req: IncomingMessage, res: ServerResponse): void => {
370
- if (req.method === 'GET' && req.url === '/health') {
371
- const health = this.getHealth();
372
- res.writeHead(200, { 'Content-Type': 'application/json' });
373
- res.end(JSON.stringify(health));
374
- } else {
375
- res.writeHead(404);
376
- res.end('Not Found');
377
- }
378
- };
379
-
380
- if (tls) {
381
- // TLS mode: create HTTPS server and attach WebSocket
382
- const httpsOptions = {
383
- cert: fs.readFileSync(this.tlsCert!),
384
- key: fs.readFileSync(this.tlsKey!)
385
- };
386
- this.httpServer = https.createServer(httpsOptions, httpHandler);
387
- this.wss = new WebSocketServer({ server: this.httpServer });
388
- this.httpServer.listen(this.port, this.host);
389
- } else {
390
- // Plain mode: create HTTP server for health endpoint + WebSocket
391
- this.httpServer = http.createServer(httpHandler);
392
- this.wss = new WebSocketServer({ server: this.httpServer });
393
- this.httpServer.listen(this.port, this.host);
394
- }
395
-
396
- this._log('server_start', { port: this.port, host: this.host, tls });
397
-
398
- this.wss.on('connection', (ws: ExtendedWebSocket, req: IncomingMessage) => {
399
- // Get real IP (X-Forwarded-For for proxied connections like Fly.io)
400
- const forwardedFor = req.headers['x-forwarded-for'];
401
- const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
402
- const realIp = forwardedForStr ? forwardedForStr.split(',')[0].trim() : req.socket.remoteAddress;
403
- const userAgent = req.headers['user-agent'] || 'unknown';
404
-
405
- // Store connection metadata on ws for later logging
406
- ws._connectedAt = Date.now();
407
- ws._realIp = realIp;
408
- ws._userAgent = userAgent;
409
-
410
- this._log('connection', {
411
- ip: realIp,
412
- proxy_ip: req.socket.remoteAddress,
413
- user_agent: userAgent
414
- });
415
-
416
- ws.on('message', (data: Buffer) => {
417
- this._handleMessage(ws, data.toString());
418
- });
419
-
420
- ws.on('close', () => {
421
- // Log if connection closed without ever identifying (drive-by)
422
- if (!this.agents.has(ws)) {
423
- const duration = ws._connectedAt ? Math.round((Date.now() - ws._connectedAt) / 1000) : 0;
424
- this._log('connection_closed_unidentified', {
425
- ip: ws._realIp,
426
- duration_sec: duration,
427
- user_agent: ws._userAgent
428
- });
429
- }
430
- this._handleDisconnect(ws);
431
- });
432
-
433
- ws.on('error', (err: Error) => {
434
- this._log('ws_error', { error: err.message });
435
- });
436
- });
437
-
438
- this.wss.on('error', (err: Error) => {
439
- this._log('server_error', { error: err.message });
440
- });
441
-
442
- // Start idle channel checker
443
- this.idleCheckInterval = setInterval(() => {
444
- this._checkIdleChannels();
445
- }, 60 * 1000); // Check every minute
446
-
447
- return this;
448
- }
449
-
450
- /**
451
- * Check for idle channels and post conversation starters
452
- */
453
- _checkIdleChannels(): void {
454
- const now = Date.now();
455
-
456
- for (const [channelName, channel] of this.channels) {
457
- // Skip if no agents in channel
458
- if (channel.agents.size < 2) continue;
459
-
460
- const lastActivity = this.channelLastActivity.get(channelName) || 0;
461
- const idleTime = now - lastActivity;
462
-
463
- if (idleTime >= this.idleTimeoutMs) {
464
- // Pick a random conversation starter
465
- const starter = this.conversationStarters[
466
- Math.floor(Math.random() * this.conversationStarters.length)
467
- ];
468
-
469
- // Get list of agents to mention
470
- const agentMentions: string[] = [];
471
- for (const ws of channel.agents) {
472
- const agent = this.agents.get(ws);
473
- if (agent) agentMentions.push(`@${agent.id}`);
474
- }
475
-
476
- const prompt = `${agentMentions.join(', ')} - ${starter}`;
477
-
478
- // Broadcast the prompt
479
- const msg = createMessage(ServerMessageType.MSG, {
480
- from: '@server',
481
- to: channelName,
482
- content: prompt
483
- });
484
- this._broadcast(channelName, msg);
485
- this._bufferMessage(channelName, msg);
486
-
487
- // Update activity time so we don't spam
488
- this.channelLastActivity.set(channelName, now);
489
-
490
- this._log('idle_prompt', { channel: channelName, agents: agentMentions.length });
491
- }
492
- }
493
- }
494
-
495
- stop(): void {
496
- if (this.idleCheckInterval) {
497
- clearInterval(this.idleCheckInterval);
498
- }
499
- if (this.wss) {
500
- this.wss.close();
501
- }
502
- if (this.httpServer) {
503
- this.httpServer.close();
504
- }
505
- if (this.proposals) {
506
- this.proposals.close();
507
- }
508
- this._log('server_stop');
509
- }
510
-
511
- _handleMessage(ws: ExtendedWebSocket, data: string): void {
512
- // Application-level message size limit (defense-in-depth for proxy bypass)
513
- const maxPayloadBytes = 256 * 1024; // 256KB - matches wsOptions.maxPayload
514
- if (data.length > maxPayloadBytes) {
515
- this._log('message_too_large', {
516
- ip: ws._realIp,
517
- size: data.length,
518
- max: maxPayloadBytes
519
- });
520
- this._send(ws, createError(ErrorCode.INVALID_MSG, `Message too large (${data.length} bytes, max ${maxPayloadBytes})`));
521
- return;
522
- }
523
-
524
- // Per-connection rate limiting (applies before auth check)
525
- const now = Date.now();
526
- if (!ws._msgTimestamps) ws._msgTimestamps = [];
527
-
528
- // Sliding window: keep only timestamps from last 10 seconds
529
- ws._msgTimestamps = ws._msgTimestamps.filter((t: number) => now - t < 10000);
530
- ws._msgTimestamps.push(now);
531
-
532
- const isIdentified = this.agents.has(ws);
533
- // Pre-auth: max 10 messages per 10s window (enough for IDENTIFY + JOINs)
534
- // Post-auth: max 60 messages per 10s window (existing MSG rate limit also applies)
535
- const maxMessages = isIdentified ? 60 : 10;
536
-
537
- if (ws._msgTimestamps.length > maxMessages) {
538
- if (!isIdentified) {
539
- this._log('pre_auth_rate_limit', {
540
- ip: ws._realIp,
541
- count: ws._msgTimestamps.length,
542
- window: '10s'
543
- });
544
- ws.close(1008, 'Rate limit exceeded');
545
- return;
546
- }
547
- this._send(ws, createError(ErrorCode.RATE_LIMITED, 'Too many messages'));
548
- return;
549
- }
550
-
551
- const result = validateClientMessage(data);
552
-
553
- if (!result.valid) {
554
- this._send(ws, createError(ErrorCode.INVALID_MSG, result.error));
555
- return;
556
- }
557
-
558
- const msg = result.msg;
559
-
560
- if (this.logMessages) {
561
- this._log('message', { type: msg.type, from: this._getAgentId(ws) });
562
- }
563
-
564
- switch (msg.type) {
565
- case ClientMessageType.IDENTIFY:
566
- handleIdentify(this, ws, msg);
567
- break;
568
- case ClientMessageType.JOIN:
569
- handleJoin(this, ws, msg);
570
- break;
571
- case ClientMessageType.LEAVE:
572
- handleLeave(this, ws, msg);
573
- break;
574
- case ClientMessageType.MSG:
575
- handleMsg(this, ws, msg);
576
- break;
577
- case ClientMessageType.LIST_CHANNELS:
578
- handleListChannels(this, ws);
579
- break;
580
- case ClientMessageType.LIST_AGENTS:
581
- handleListAgents(this, ws, msg);
582
- break;
583
- case ClientMessageType.CREATE_CHANNEL:
584
- handleCreateChannel(this, ws, msg);
585
- break;
586
- case ClientMessageType.INVITE:
587
- handleInvite(this, ws, msg);
588
- break;
589
- case ClientMessageType.PING:
590
- this._send(ws, createMessage(ServerMessageType.PONG));
591
- break;
592
- // Proposal/negotiation messages
593
- case ClientMessageType.PROPOSAL:
594
- handleProposal(this, ws, msg);
595
- break;
596
- case ClientMessageType.ACCEPT:
597
- handleAccept(this, ws, msg);
598
- break;
599
- case ClientMessageType.REJECT:
600
- handleReject(this, ws, msg);
601
- break;
602
- case ClientMessageType.COMPLETE:
603
- handleComplete(this, ws, msg);
604
- break;
605
- case ClientMessageType.DISPUTE:
606
- handleDispute(this, ws, msg);
607
- break;
608
- // Skill discovery messages
609
- case ClientMessageType.REGISTER_SKILLS:
610
- handleRegisterSkills(this, ws, msg);
611
- break;
612
- case ClientMessageType.SEARCH_SKILLS:
613
- handleSearchSkills(this, ws, msg);
614
- break;
615
- // Presence messages
616
- case ClientMessageType.SET_PRESENCE:
617
- handleSetPresence(this, ws, msg);
618
- break;
619
- // Identity verification messages
620
- case ClientMessageType.VERIFY_REQUEST:
621
- handleVerifyRequest(this, ws, msg);
622
- break;
623
- case ClientMessageType.VERIFY_RESPONSE:
624
- handleVerifyResponse(this, ws, msg);
625
- break;
626
- }
627
- }
628
-
629
- _handleDisconnect(ws: ExtendedWebSocket): void {
630
- const agent = this.agents.get(ws);
631
- if (!agent) return;
632
-
633
- // Calculate connection duration
634
- const duration = ws._connectedAt ? Math.round((Date.now() - ws._connectedAt) / 1000) : 0;
635
- const channelCount = agent.channels.size;
636
-
637
- this._log('disconnect', {
638
- agent: agent.id,
639
- duration_sec: duration,
640
- channels_joined: channelCount,
641
- had_pubkey: !!agent.pubkey,
642
- ip: ws._realIp
643
- });
644
-
645
- // Leave all channels
646
- for (const channelName of agent.channels) {
647
- const channel = this.channels.get(channelName);
648
- if (channel) {
649
- channel.agents.delete(ws);
650
- this._broadcast(channelName, createMessage(ServerMessageType.AGENT_LEFT, {
651
- channel: channelName,
652
- agent: `@${agent.id}`
653
- }));
654
- }
655
- }
656
-
657
- // Remove from state
658
- this.agentById.delete(agent.id);
659
- this.agents.delete(ws);
660
- this.lastMessageTime.delete(ws);
661
- }
662
- }
663
-
664
- // Allow running directly
665
- export function startServer(options: AgentChatServerOptions = {}): AgentChatServer {
666
- // Support environment variable overrides (for Docker)
667
- const config: AgentChatServerOptions = {
668
- port: parseInt(String(options.port || process.env.PORT || 6667)),
669
- host: options.host || process.env.HOST || '0.0.0.0',
670
- name: options.name || process.env.SERVER_NAME || 'agentchat',
671
- logMessages: options.logMessages || process.env.LOG_MESSAGES === 'true',
672
- cert: options.cert || process.env.TLS_CERT || null,
673
- key: options.key || process.env.TLS_KEY || null,
674
- rateLimitMs: options.rateLimitMs || parseInt(process.env.RATE_LIMIT_MS || '1000'),
675
- messageBufferSize: options.messageBufferSize || parseInt(process.env.MESSAGE_BUFFER_SIZE || '20')
676
- };
677
-
678
- const server = new AgentChatServer(config);
679
- server.start();
680
-
681
- const protocol = (config.cert && config.key) ? 'wss' : 'ws';
682
- console.log(`AgentChat server running on ${protocol}://${server.host}:${server.port}`);
683
- console.log('Default channels: #general, #agents');
684
- if (config.cert && config.key) {
685
- console.log('TLS enabled');
686
- }
687
- console.log('Press Ctrl+C to stop');
688
-
689
- process.on('SIGINT', () => {
690
- server.stop();
691
- process.exit(0);
692
- });
693
-
694
- return server;
695
- }
696
-
697
- // Re-export EscrowEvent for consumers
698
- export { EscrowEvent } from './escrow-hooks.js';