@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.
- package/Dockerfile +1 -1
- package/dist/bin/agentchat.d.ts +7 -0
- package/dist/bin/agentchat.d.ts.map +1 -0
- package/dist/bin/agentchat.js +1511 -0
- package/dist/bin/agentchat.js.map +1 -0
- package/dist/lib/allowlist.d.ts +77 -0
- package/dist/lib/allowlist.d.ts.map +1 -0
- package/dist/lib/allowlist.js +151 -0
- package/dist/lib/allowlist.js.map +1 -0
- package/dist/lib/client.d.ts +147 -0
- package/dist/lib/client.d.ts.map +1 -0
- package/dist/lib/client.js +704 -0
- package/dist/lib/client.js.map +1 -0
- package/dist/lib/daemon.d.ts +122 -0
- package/dist/lib/daemon.d.ts.map +1 -0
- package/dist/lib/daemon.js +523 -0
- package/dist/lib/daemon.js.map +1 -0
- package/dist/lib/deploy/akash.d.ts +271 -0
- package/dist/lib/deploy/akash.d.ts.map +1 -0
- package/dist/lib/deploy/akash.js +671 -0
- package/dist/lib/deploy/akash.js.map +1 -0
- package/dist/lib/deploy/config.d.ts +62 -0
- package/dist/lib/deploy/config.d.ts.map +1 -0
- package/dist/lib/deploy/config.js +116 -0
- package/dist/lib/deploy/config.js.map +1 -0
- package/dist/lib/deploy/docker.d.ts +37 -0
- package/dist/lib/deploy/docker.d.ts.map +1 -0
- package/dist/lib/deploy/docker.js +122 -0
- package/dist/lib/deploy/docker.js.map +1 -0
- package/dist/lib/deploy/index.d.ts +11 -0
- package/dist/lib/deploy/index.d.ts.map +1 -0
- package/dist/lib/deploy/index.js +11 -0
- package/dist/lib/deploy/index.js.map +1 -0
- package/dist/lib/escrow-hooks.d.ts +199 -0
- package/dist/lib/escrow-hooks.d.ts.map +1 -0
- package/dist/lib/escrow-hooks.js +221 -0
- package/dist/lib/escrow-hooks.js.map +1 -0
- package/dist/lib/identity.d.ts +134 -0
- package/dist/lib/identity.d.ts.map +1 -0
- package/dist/lib/identity.js +334 -0
- package/dist/lib/identity.js.map +1 -0
- package/dist/lib/jitter.d.ts +42 -0
- package/dist/lib/jitter.d.ts.map +1 -0
- package/{lib/jitter.ts → dist/lib/jitter.js} +10 -18
- package/dist/lib/jitter.js.map +1 -0
- package/dist/lib/proposals.d.ts +223 -0
- package/dist/lib/proposals.d.ts.map +1 -0
- package/dist/lib/proposals.js +379 -0
- package/dist/lib/proposals.js.map +1 -0
- package/dist/lib/protocol.d.ts +220 -0
- package/dist/lib/protocol.d.ts.map +1 -0
- package/dist/lib/protocol.js +507 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/receipts.d.ts +134 -0
- package/dist/lib/receipts.d.ts.map +1 -0
- package/dist/lib/receipts.js +270 -0
- package/dist/lib/receipts.js.map +1 -0
- package/dist/lib/reputation.d.ts +250 -0
- package/dist/lib/reputation.d.ts.map +1 -0
- package/dist/lib/reputation.js +586 -0
- package/dist/lib/reputation.js.map +1 -0
- package/dist/lib/security.d.ts +27 -0
- package/dist/lib/security.d.ts.map +1 -0
- package/dist/lib/security.js +150 -0
- package/dist/lib/security.js.map +1 -0
- package/dist/lib/server/handlers/admin.d.ts +26 -0
- package/dist/lib/server/handlers/admin.d.ts.map +1 -0
- package/dist/lib/server/handlers/admin.js +76 -0
- package/dist/lib/server/handlers/admin.js.map +1 -0
- package/dist/lib/server/handlers/identity.d.ts +36 -0
- package/dist/lib/server/handlers/identity.d.ts.map +1 -0
- package/dist/lib/server/handlers/identity.js +330 -0
- package/dist/lib/server/handlers/identity.js.map +1 -0
- package/dist/lib/server/handlers/index.d.ts +10 -0
- package/dist/lib/server/handlers/index.d.ts.map +1 -0
- package/dist/lib/server/handlers/index.js +15 -0
- package/dist/lib/server/handlers/index.js.map +1 -0
- package/dist/lib/server/handlers/message.d.ts +47 -0
- package/dist/lib/server/handlers/message.d.ts.map +1 -0
- package/dist/lib/server/handlers/message.js +265 -0
- package/dist/lib/server/handlers/message.js.map +1 -0
- package/dist/lib/server/handlers/presence.d.ts +18 -0
- package/dist/lib/server/handlers/presence.d.ts.map +1 -0
- package/dist/lib/server/handlers/presence.js +35 -0
- package/dist/lib/server/handlers/presence.js.map +1 -0
- package/dist/lib/server/handlers/proposal.d.ts +38 -0
- package/dist/lib/server/handlers/proposal.d.ts.map +1 -0
- package/dist/lib/server/handlers/proposal.js +273 -0
- package/dist/lib/server/handlers/proposal.js.map +1 -0
- package/dist/lib/server/handlers/skills.d.ts +22 -0
- package/dist/lib/server/handlers/skills.d.ts.map +1 -0
- package/dist/lib/server/handlers/skills.js +119 -0
- package/dist/lib/server/handlers/skills.js.map +1 -0
- package/dist/lib/server-directory.d.ts +85 -0
- package/dist/lib/server-directory.d.ts.map +1 -0
- package/dist/lib/server-directory.js +177 -0
- package/dist/lib/server-directory.js.map +1 -0
- package/dist/lib/server.d.ts +162 -0
- package/dist/lib/server.d.ts.map +1 -0
- package/dist/lib/server.js +602 -0
- package/dist/lib/server.js.map +1 -0
- package/dist/lib/types.d.ts +461 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +98 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +22 -13
- package/bin/agentchat.js +0 -1617
- package/bin/agentchat.ts +0 -1812
- package/lib/allowlist.js +0 -162
- package/lib/chat.py +0 -241
- package/lib/client.js +0 -821
- package/lib/client.ts +0 -877
- package/lib/daemon.js +0 -562
- package/lib/daemon.ts +0 -662
- package/lib/deploy/akash.js +0 -811
- package/lib/deploy/config.js +0 -128
- package/lib/deploy/docker.js +0 -132
- package/lib/deploy/index.js +0 -24
- package/lib/elo_swarm.py +0 -569
- package/lib/escrow-hooks.js +0 -237
- package/lib/escrow-hooks.ts +0 -391
- package/lib/identity.js +0 -376
- package/lib/identity.ts +0 -412
- package/lib/jitter.js +0 -54
- package/lib/proposals.js +0 -426
- package/lib/proposals.ts +0 -612
- package/lib/protocol.js +0 -516
- package/lib/receipts.js +0 -294
- package/lib/receipts.ts +0 -359
- package/lib/reputation.js +0 -664
- package/lib/reputation.ts +0 -790
- package/lib/security.js +0 -183
- package/lib/server/handlers/admin.js +0 -94
- package/lib/server/handlers/identity.js +0 -258
- package/lib/server/handlers/index.js +0 -42
- package/lib/server/handlers/message.js +0 -319
- package/lib/server/handlers/presence.js +0 -45
- package/lib/server/handlers/proposal.js +0 -358
- package/lib/server/handlers/skills.js +0 -141
- package/lib/server-directory.js +0 -190
- package/lib/server-directory.ts +0 -232
- package/lib/server.js +0 -633
- package/lib/server.ts +0 -698
- package/lib/supervisor/USAGE.md +0 -110
- package/lib/supervisor/agent-health.sh +0 -107
- package/lib/supervisor/agent-monitor.sh +0 -123
- package/lib/supervisor/agent-supervisor.sh +0 -135
- package/lib/supervisor/agentctl.sh +0 -266
- package/lib/supervisor/god-backup.sh +0 -126
- package/lib/supervisor/god-watchdog.sh +0 -107
- package/lib/supervisor/killswitch.sh +0 -43
- package/lib/supervisor/notify.sh +0 -19
- 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';
|