@tjamescouch/agentchat 0.22.0 → 0.22.1
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.
- package/bin/agentchat.js +1 -1
- package/bin/agentchat.ts +1812 -0
- package/lib/allowlist.js +162 -0
- package/lib/client.js +2 -2
- package/lib/client.ts +877 -0
- package/lib/daemon.ts +662 -0
- package/lib/escrow-hooks.ts +391 -0
- package/lib/identity.ts +412 -0
- package/lib/jitter.ts +59 -0
- package/lib/proposals.ts +612 -0
- package/lib/protocol.js +37 -5
- package/lib/receipts.ts +359 -0
- package/lib/reputation.ts +790 -0
- package/lib/server/handlers/admin.js +94 -0
- package/lib/server/handlers/identity.js +16 -0
- package/lib/server/handlers/message.js +19 -6
- package/lib/server/handlers/presence.js +1 -0
- package/lib/server-directory.js +17 -8
- package/lib/server-directory.ts +232 -0
- package/lib/server.js +115 -10
- package/lib/server.ts +698 -0
- package/lib/supervisor/agent-health.sh +107 -0
- package/lib/supervisor/agent-monitor.sh +123 -0
- package/lib/supervisor/agentctl.sh +19 -3
- package/lib/supervisor/god-backup.sh +126 -0
- package/lib/supervisor/god-watchdog.sh +107 -0
- package/lib/supervisor/killswitch.sh +15 -8
- package/lib/types.ts +433 -0
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -49,6 +49,12 @@ import {
|
|
|
49
49
|
import {
|
|
50
50
|
handleSetPresence,
|
|
51
51
|
} from './server/handlers/presence.js';
|
|
52
|
+
import {
|
|
53
|
+
handleAdminApprove,
|
|
54
|
+
handleAdminRevoke,
|
|
55
|
+
handleAdminList,
|
|
56
|
+
} from './server/handlers/admin.js';
|
|
57
|
+
import { Allowlist } from './allowlist.js';
|
|
52
58
|
|
|
53
59
|
export class AgentChatServer {
|
|
54
60
|
constructor(options = {}) {
|
|
@@ -67,6 +73,10 @@ export class AgentChatServer {
|
|
|
67
73
|
// Message buffer size per channel (for replay on join)
|
|
68
74
|
this.messageBufferSize = options.messageBufferSize || 20;
|
|
69
75
|
|
|
76
|
+
// Per-IP connection limiting
|
|
77
|
+
this.maxConnectionsPerIp = options.maxConnectionsPerIp || 10;
|
|
78
|
+
this.connectionsByIp = new Map(); // ip -> Set<ws>
|
|
79
|
+
|
|
70
80
|
// State
|
|
71
81
|
this.agents = new Map(); // ws -> agent info
|
|
72
82
|
this.agentById = new Map(); // id -> ws
|
|
@@ -118,6 +128,14 @@ export class AgentChatServer {
|
|
|
118
128
|
this.pendingVerifications = new Map();
|
|
119
129
|
this.verificationTimeoutMs = options.verificationTimeoutMs || 30000; // 30 seconds default
|
|
120
130
|
|
|
131
|
+
// Allowlist (opt-in)
|
|
132
|
+
this.allowlist = new Allowlist({
|
|
133
|
+
enabled: options.allowlistEnabled || false,
|
|
134
|
+
strict: options.allowlistStrict || false,
|
|
135
|
+
adminKey: options.adminKey || null,
|
|
136
|
+
filePath: options.allowlistFile || undefined,
|
|
137
|
+
});
|
|
138
|
+
|
|
121
139
|
this.wss = null;
|
|
122
140
|
this.httpServer = null;
|
|
123
141
|
this.startedAt = null; // Set on start() for uptime tracking
|
|
@@ -236,18 +254,32 @@ export class AgentChatServer {
|
|
|
236
254
|
const tls = !!(this.tlsCert && this.tlsKey);
|
|
237
255
|
this.startedAt = Date.now();
|
|
238
256
|
|
|
257
|
+
// Security headers applied to all HTTP responses
|
|
258
|
+
const securityHeaders = {
|
|
259
|
+
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
|
260
|
+
'X-Content-Type-Options': 'nosniff',
|
|
261
|
+
'X-Frame-Options': 'DENY',
|
|
262
|
+
'Content-Security-Policy': "default-src 'none'",
|
|
263
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
264
|
+
};
|
|
265
|
+
|
|
239
266
|
// HTTP request handler for health endpoint
|
|
240
267
|
const httpHandler = (req, res) => {
|
|
241
268
|
if (req.method === 'GET' && req.url === '/health') {
|
|
242
269
|
const health = this.getHealth();
|
|
243
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
270
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...securityHeaders });
|
|
244
271
|
res.end(JSON.stringify(health));
|
|
245
272
|
} else {
|
|
246
|
-
res.writeHead(404);
|
|
273
|
+
res.writeHead(404, securityHeaders);
|
|
247
274
|
res.end('Not Found');
|
|
248
275
|
}
|
|
249
276
|
};
|
|
250
277
|
|
|
278
|
+
// WebSocket options: limit max message size to 256KB
|
|
279
|
+
const wsOptions = {
|
|
280
|
+
maxPayload: 256 * 1024,
|
|
281
|
+
};
|
|
282
|
+
|
|
251
283
|
if (tls) {
|
|
252
284
|
// TLS mode: create HTTPS server and attach WebSocket
|
|
253
285
|
const httpsOptions = {
|
|
@@ -255,12 +287,12 @@ export class AgentChatServer {
|
|
|
255
287
|
key: fs.readFileSync(this.tlsKey)
|
|
256
288
|
};
|
|
257
289
|
this.httpServer = https.createServer(httpsOptions, httpHandler);
|
|
258
|
-
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
290
|
+
this.wss = new WebSocketServer({ ...wsOptions, server: this.httpServer });
|
|
259
291
|
this.httpServer.listen(this.port, this.host);
|
|
260
292
|
} else {
|
|
261
293
|
// Plain mode: create HTTP server for health endpoint + WebSocket
|
|
262
294
|
this.httpServer = http.createServer(httpHandler);
|
|
263
|
-
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
295
|
+
this.wss = new WebSocketServer({ ...wsOptions, server: this.httpServer });
|
|
264
296
|
this.httpServer.listen(this.port, this.host);
|
|
265
297
|
}
|
|
266
298
|
|
|
@@ -272,6 +304,16 @@ export class AgentChatServer {
|
|
|
272
304
|
const realIp = forwardedFor ? forwardedFor.split(',')[0].trim() : req.socket.remoteAddress;
|
|
273
305
|
const userAgent = req.headers['user-agent'] || 'unknown';
|
|
274
306
|
|
|
307
|
+
// Per-IP connection limiting
|
|
308
|
+
const ipConns = this.connectionsByIp.get(realIp) || new Set();
|
|
309
|
+
if (ipConns.size >= this.maxConnectionsPerIp) {
|
|
310
|
+
this._log('connection_rejected', { ip: realIp, reason: 'max_connections_per_ip', count: ipConns.size });
|
|
311
|
+
ws.close(1008, 'Too many connections from this IP');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
ipConns.add(ws);
|
|
315
|
+
this.connectionsByIp.set(realIp, ipConns);
|
|
316
|
+
|
|
275
317
|
// Store connection metadata on ws for later logging
|
|
276
318
|
ws._connectedAt = Date.now();
|
|
277
319
|
ws._realIp = realIp;
|
|
@@ -280,14 +322,22 @@ export class AgentChatServer {
|
|
|
280
322
|
this._log('connection', {
|
|
281
323
|
ip: realIp,
|
|
282
324
|
proxy_ip: req.socket.remoteAddress,
|
|
283
|
-
user_agent: userAgent
|
|
325
|
+
user_agent: userAgent,
|
|
326
|
+
ip_connections: ipConns.size
|
|
284
327
|
});
|
|
285
|
-
|
|
328
|
+
|
|
286
329
|
ws.on('message', (data) => {
|
|
287
330
|
this._handleMessage(ws, data.toString());
|
|
288
331
|
});
|
|
289
|
-
|
|
332
|
+
|
|
290
333
|
ws.on('close', () => {
|
|
334
|
+
// Clean up per-IP tracking
|
|
335
|
+
const conns = this.connectionsByIp.get(realIp);
|
|
336
|
+
if (conns) {
|
|
337
|
+
conns.delete(ws);
|
|
338
|
+
if (conns.size === 0) this.connectionsByIp.delete(realIp);
|
|
339
|
+
}
|
|
340
|
+
|
|
291
341
|
// Log if connection closed without ever identifying (drive-by)
|
|
292
342
|
if (!this.agents.has(ws)) {
|
|
293
343
|
const duration = ws._connectedAt ? Math.round((Date.now() - ws._connectedAt) / 1000) : 0;
|
|
@@ -340,7 +390,7 @@ export class AgentChatServer {
|
|
|
340
390
|
const agentMentions = [];
|
|
341
391
|
for (const ws of channel.agents) {
|
|
342
392
|
const agent = this.agents.get(ws);
|
|
343
|
-
if (agent) agentMentions.push(
|
|
393
|
+
if (agent) agentMentions.push(`${agent.name} (@${agent.id})`);
|
|
344
394
|
}
|
|
345
395
|
|
|
346
396
|
const prompt = `${agentMentions.join(', ')} - ${starter}`;
|
|
@@ -379,6 +429,45 @@ export class AgentChatServer {
|
|
|
379
429
|
}
|
|
380
430
|
|
|
381
431
|
_handleMessage(ws, data) {
|
|
432
|
+
// Application-level message size limit (defense-in-depth for proxy bypass)
|
|
433
|
+
const maxPayloadBytes = 256 * 1024; // 256KB - matches wsOptions.maxPayload
|
|
434
|
+
if (data.length > maxPayloadBytes) {
|
|
435
|
+
this._log('message_too_large', {
|
|
436
|
+
ip: ws._realIp,
|
|
437
|
+
size: data.length,
|
|
438
|
+
max: maxPayloadBytes
|
|
439
|
+
});
|
|
440
|
+
this._send(ws, createError(ErrorCode.INVALID_MSG, `Message too large (${data.length} bytes, max ${maxPayloadBytes})`));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Per-connection rate limiting (applies before auth check)
|
|
445
|
+
const now = Date.now();
|
|
446
|
+
if (!ws._msgTimestamps) ws._msgTimestamps = [];
|
|
447
|
+
|
|
448
|
+
// Sliding window: keep only timestamps from last 10 seconds
|
|
449
|
+
ws._msgTimestamps = ws._msgTimestamps.filter(t => now - t < 10000);
|
|
450
|
+
ws._msgTimestamps.push(now);
|
|
451
|
+
|
|
452
|
+
const isIdentified = this.agents.has(ws);
|
|
453
|
+
// Pre-auth: max 10 messages per 10s window (enough for IDENTIFY + JOINs)
|
|
454
|
+
// Post-auth: max 60 messages per 10s window (existing MSG rate limit also applies)
|
|
455
|
+
const maxMessages = isIdentified ? 60 : 10;
|
|
456
|
+
|
|
457
|
+
if (ws._msgTimestamps.length > maxMessages) {
|
|
458
|
+
if (!isIdentified) {
|
|
459
|
+
this._log('pre_auth_rate_limit', {
|
|
460
|
+
ip: ws._realIp,
|
|
461
|
+
count: ws._msgTimestamps.length,
|
|
462
|
+
window: '10s'
|
|
463
|
+
});
|
|
464
|
+
ws.close(1008, 'Rate limit exceeded');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
this._send(ws, createError(ErrorCode.RATE_LIMITED, 'Too many messages'));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
382
471
|
const { valid, msg, error } = validateClientMessage(data);
|
|
383
472
|
|
|
384
473
|
if (!valid) {
|
|
@@ -452,6 +541,16 @@ export class AgentChatServer {
|
|
|
452
541
|
case ClientMessageType.VERIFY_RESPONSE:
|
|
453
542
|
handleVerifyResponse(this, ws, msg);
|
|
454
543
|
break;
|
|
544
|
+
// Admin messages (allowlist management)
|
|
545
|
+
case ClientMessageType.ADMIN_APPROVE:
|
|
546
|
+
handleAdminApprove(this, ws, msg);
|
|
547
|
+
break;
|
|
548
|
+
case ClientMessageType.ADMIN_REVOKE:
|
|
549
|
+
handleAdminRevoke(this, ws, msg);
|
|
550
|
+
break;
|
|
551
|
+
case ClientMessageType.ADMIN_LIST:
|
|
552
|
+
handleAdminList(this, ws, msg);
|
|
553
|
+
break;
|
|
455
554
|
}
|
|
456
555
|
}
|
|
457
556
|
|
|
@@ -479,7 +578,8 @@ export class AgentChatServer {
|
|
|
479
578
|
channel.agents.delete(ws);
|
|
480
579
|
this._broadcast(channelName, createMessage(ServerMessageType.AGENT_LEFT, {
|
|
481
580
|
channel: channelName,
|
|
482
|
-
agent: `@${agent.id}
|
|
581
|
+
agent: `@${agent.id}`,
|
|
582
|
+
name: agent.name
|
|
483
583
|
}));
|
|
484
584
|
}
|
|
485
585
|
}
|
|
@@ -502,7 +602,12 @@ export function startServer(options = {}) {
|
|
|
502
602
|
cert: options.cert || process.env.TLS_CERT || null,
|
|
503
603
|
key: options.key || process.env.TLS_KEY || null,
|
|
504
604
|
rateLimitMs: options.rateLimitMs || parseInt(process.env.RATE_LIMIT_MS || 1000),
|
|
505
|
-
messageBufferSize: options.messageBufferSize || parseInt(process.env.MESSAGE_BUFFER_SIZE || 20)
|
|
605
|
+
messageBufferSize: options.messageBufferSize || parseInt(process.env.MESSAGE_BUFFER_SIZE || 20),
|
|
606
|
+
// Allowlist settings
|
|
607
|
+
allowlistEnabled: options.allowlistEnabled || process.env.ALLOWLIST_ENABLED === 'true',
|
|
608
|
+
allowlistStrict: options.allowlistStrict || process.env.ALLOWLIST_STRICT === 'true',
|
|
609
|
+
adminKey: options.adminKey || process.env.ADMIN_KEY || null,
|
|
610
|
+
allowlistFile: options.allowlistFile || process.env.ALLOWLIST_FILE || undefined,
|
|
506
611
|
};
|
|
507
612
|
|
|
508
613
|
const server = new AgentChatServer(config);
|