@tjamescouch/agentchat 0.18.1 → 0.18.2

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.
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Identity Handlers
3
+ * Handles identify, verification request/response
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+ import {
8
+ ServerMessageType,
9
+ ErrorCode,
10
+ createMessage,
11
+ createError,
12
+ generateAgentId,
13
+ generateVerifyId,
14
+ pubkeyToAgentId,
15
+ } from '../../protocol.js';
16
+
17
+ /**
18
+ * Handle IDENTIFY command
19
+ */
20
+ export function handleIdentify(server, ws, msg) {
21
+ // Check if already identified
22
+ if (server.agents.has(ws)) {
23
+ server._send(ws, createError(ErrorCode.INVALID_MSG, 'Already identified'));
24
+ return;
25
+ }
26
+
27
+ let id;
28
+
29
+ // Use pubkey-derived stable ID if pubkey provided
30
+ if (msg.pubkey) {
31
+ // Check if this pubkey has connected before
32
+ const existingId = server.pubkeyToId.get(msg.pubkey);
33
+ if (existingId) {
34
+ // Returning agent - use their stable ID
35
+ id = existingId;
36
+ } else {
37
+ // New agent with pubkey - generate stable ID from pubkey
38
+ id = pubkeyToAgentId(msg.pubkey);
39
+ server.pubkeyToId.set(msg.pubkey, id);
40
+ }
41
+
42
+ // Check if this ID is currently in use by another connection
43
+ if (server.agentById.has(id)) {
44
+ // Kick the old connection instead of rejecting the new one
45
+ const oldWs = server.agentById.get(id);
46
+ server._log('identity-takeover', { id, reason: 'New connection with same identity' });
47
+ server._send(oldWs, createError(ErrorCode.INVALID_MSG, 'Disconnected: Another connection claimed this identity'));
48
+ server._handleDisconnect(oldWs);
49
+ oldWs.close(1000, 'Identity claimed by new connection');
50
+ }
51
+ } else {
52
+ // Ephemeral agent - generate random ID
53
+ id = generateAgentId();
54
+ }
55
+
56
+ const agent = {
57
+ id,
58
+ name: msg.name,
59
+ pubkey: msg.pubkey || null,
60
+ channels: new Set(),
61
+ connectedAt: Date.now(),
62
+ presence: 'online',
63
+ statusText: null
64
+ };
65
+
66
+ server.agents.set(ws, agent);
67
+ server.agentById.set(id, ws);
68
+
69
+ // Determine if this is a new or returning identity
70
+ const isReturning = msg.pubkey && server.pubkeyToId.has(msg.pubkey);
71
+ const isEphemeral = !msg.pubkey;
72
+
73
+ server._log('identify', {
74
+ id,
75
+ name: msg.name,
76
+ hasPubkey: !!msg.pubkey,
77
+ returning: isReturning,
78
+ ephemeral: isEphemeral,
79
+ ip: ws._realIp,
80
+ user_agent: ws._userAgent
81
+ });
82
+
83
+ server._send(ws, createMessage(ServerMessageType.WELCOME, {
84
+ agent_id: `@${id}`,
85
+ server: server.serverName
86
+ }));
87
+ }
88
+
89
+ /**
90
+ * Handle VERIFY_REQUEST command
91
+ */
92
+ export function handleVerifyRequest(server, ws, msg) {
93
+ const agent = server.agents.get(ws);
94
+ if (!agent) {
95
+ server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
96
+ return;
97
+ }
98
+
99
+ // Find target agent
100
+ const targetId = msg.target.slice(1);
101
+ const targetWs = server.agentById.get(targetId);
102
+
103
+ if (!targetWs) {
104
+ server._send(ws, createError(ErrorCode.AGENT_NOT_FOUND, `Agent ${msg.target} not found`));
105
+ return;
106
+ }
107
+
108
+ const targetAgent = server.agents.get(targetWs);
109
+
110
+ // Target must have a pubkey for verification
111
+ if (!targetAgent.pubkey) {
112
+ server._send(ws, createError(ErrorCode.NO_PUBKEY, `Agent ${msg.target} has no persistent identity`));
113
+ return;
114
+ }
115
+
116
+ // Create verification request
117
+ const requestId = generateVerifyId();
118
+ const expires = Date.now() + server.verificationTimeoutMs;
119
+
120
+ server.pendingVerifications.set(requestId, {
121
+ from: `@${agent.id}`,
122
+ fromWs: ws,
123
+ target: msg.target,
124
+ targetPubkey: targetAgent.pubkey,
125
+ nonce: msg.nonce,
126
+ expires
127
+ });
128
+
129
+ // Set timeout to clean up expired requests
130
+ setTimeout(() => {
131
+ const request = server.pendingVerifications.get(requestId);
132
+ if (request) {
133
+ server.pendingVerifications.delete(requestId);
134
+ // Notify requester of timeout
135
+ if (request.fromWs.readyState === 1) {
136
+ server._send(request.fromWs, createMessage(ServerMessageType.VERIFY_FAILED, {
137
+ request_id: requestId,
138
+ target: request.target,
139
+ reason: 'Verification timed out'
140
+ }));
141
+ }
142
+ }
143
+ }, server.verificationTimeoutMs);
144
+
145
+ server._log('verify_request', { id: requestId, from: agent.id, target: targetId });
146
+
147
+ // Forward to target agent
148
+ server._send(targetWs, createMessage(ServerMessageType.VERIFY_REQUEST, {
149
+ request_id: requestId,
150
+ from: `@${agent.id}`,
151
+ nonce: msg.nonce
152
+ }));
153
+
154
+ // Acknowledge to requester
155
+ server._send(ws, createMessage(ServerMessageType.VERIFY_REQUEST, {
156
+ request_id: requestId,
157
+ target: msg.target,
158
+ status: 'pending'
159
+ }));
160
+ }
161
+
162
+ /**
163
+ * Handle VERIFY_RESPONSE command
164
+ */
165
+ export function handleVerifyResponse(server, ws, msg) {
166
+ const agent = server.agents.get(ws);
167
+ if (!agent) {
168
+ server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
169
+ return;
170
+ }
171
+
172
+ // Must have identity to respond to verification
173
+ if (!agent.pubkey) {
174
+ server._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Responding to verification requires persistent identity'));
175
+ return;
176
+ }
177
+
178
+ // Find the pending verification
179
+ const request = server.pendingVerifications.get(msg.request_id);
180
+ if (!request) {
181
+ server._send(ws, createError(ErrorCode.VERIFICATION_EXPIRED, 'Verification request not found or expired'));
182
+ return;
183
+ }
184
+
185
+ // Verify the responder is the target
186
+ if (request.target !== `@${agent.id}`) {
187
+ server._send(ws, createError(ErrorCode.INVALID_MSG, 'You are not the target of this verification'));
188
+ return;
189
+ }
190
+
191
+ // Verify the nonce matches
192
+ if (msg.nonce !== request.nonce) {
193
+ server._send(ws, createError(ErrorCode.INVALID_MSG, 'Nonce mismatch'));
194
+ return;
195
+ }
196
+
197
+ // Verify the signature
198
+ let verified = false;
199
+ try {
200
+ const keyObj = crypto.createPublicKey(request.targetPubkey);
201
+ verified = crypto.verify(
202
+ null,
203
+ Buffer.from(msg.nonce),
204
+ keyObj,
205
+ Buffer.from(msg.sig, 'base64')
206
+ );
207
+ } catch (err) {
208
+ server._log('verify_error', { request_id: msg.request_id, error: err.message });
209
+ }
210
+
211
+ // Clean up the pending request
212
+ server.pendingVerifications.delete(msg.request_id);
213
+
214
+ server._log('verify_response', {
215
+ request_id: msg.request_id,
216
+ from: agent.id,
217
+ verified
218
+ });
219
+
220
+ // Notify the original requester
221
+ if (request.fromWs.readyState === 1) {
222
+ if (verified) {
223
+ server._send(request.fromWs, createMessage(ServerMessageType.VERIFY_SUCCESS, {
224
+ request_id: msg.request_id,
225
+ agent: request.target,
226
+ pubkey: request.targetPubkey
227
+ }));
228
+ } else {
229
+ server._send(request.fromWs, createMessage(ServerMessageType.VERIFY_FAILED, {
230
+ request_id: msg.request_id,
231
+ target: request.target,
232
+ reason: 'Signature verification failed'
233
+ }));
234
+ }
235
+ }
236
+
237
+ // Notify the responder
238
+ server._send(ws, createMessage(verified ? ServerMessageType.VERIFY_SUCCESS : ServerMessageType.VERIFY_FAILED, {
239
+ request_id: msg.request_id,
240
+ verified
241
+ }));
242
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Server Handlers Index
3
+ * Exports all handler functions
4
+ */
5
+
6
+ // Message handlers
7
+ export {
8
+ handleMsg,
9
+ handleJoin,
10
+ handleLeave,
11
+ handleListChannels,
12
+ handleListAgents,
13
+ handleCreateChannel,
14
+ handleInvite,
15
+ } from './message.js';
16
+
17
+ // Proposal handlers
18
+ export {
19
+ handleProposal,
20
+ handleAccept,
21
+ handleReject,
22
+ handleComplete,
23
+ handleDispute,
24
+ } from './proposal.js';
25
+
26
+ // Identity handlers
27
+ export {
28
+ handleIdentify,
29
+ handleVerifyRequest,
30
+ handleVerifyResponse,
31
+ } from './identity.js';
32
+
33
+ // Skills handlers
34
+ export {
35
+ handleRegisterSkills,
36
+ handleSearchSkills,
37
+ } from './skills.js';
38
+
39
+ // Presence handlers
40
+ export {
41
+ handleSetPresence,
42
+ } from './presence.js';
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Message Handlers
3
+ * Handles message routing, join, leave, and channel operations
4
+ */
5
+
6
+ import {
7
+ ServerMessageType,
8
+ ErrorCode,
9
+ createMessage,
10
+ createError,
11
+ isChannel,
12
+ isAgent,
13
+ } from '../../protocol.js';
14
+
15
+ /**
16
+ * Handle MSG command - route messages to channels or agents
17
+ */
18
+ export function handleMsg(server, ws, msg) {
19
+ const agent = server.agents.get(ws);
20
+ if (!agent) {
21
+ server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
22
+ return;
23
+ }
24
+
25
+ // Rate limiting: 1 message per second per agent
26
+ const now = Date.now();
27
+ const lastTime = server.lastMessageTime.get(ws) || 0;
28
+ if (now - lastTime < server.rateLimitMs) {
29
+ server._send(ws, createError(ErrorCode.RATE_LIMITED, 'Rate limit exceeded (max 1 message per second)'));
30
+ return;
31
+ }
32
+ server.lastMessageTime.set(ws, now);
33
+
34
+ const outMsg = createMessage(ServerMessageType.MSG, {
35
+ from: `@${agent.id}`,
36
+ to: msg.to,
37
+ content: msg.content,
38
+ ...(msg.sig && { sig: msg.sig })
39
+ });
40
+
41
+ if (isChannel(msg.to)) {
42
+ // Channel message
43
+ const channel = server.channels.get(msg.to);
44
+ if (!channel) {
45
+ server._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.to} not found`));
46
+ return;
47
+ }
48
+
49
+ if (!agent.channels.has(msg.to)) {
50
+ server._send(ws, createError(ErrorCode.NOT_INVITED, `Not a member of ${msg.to}`));
51
+ return;
52
+ }
53
+
54
+ // Broadcast to channel including sender
55
+ server._broadcast(msg.to, outMsg);
56
+
57
+ // Buffer the message for replay to future joiners
58
+ server._bufferMessage(msg.to, outMsg);
59
+
60
+ // Update channel activity timestamp (for idle detection)
61
+ server.channelLastActivity.set(msg.to, Date.now());
62
+
63
+ } else if (isAgent(msg.to)) {
64
+ // Direct message
65
+ const targetId = msg.to.slice(1);
66
+ const targetWs = server.agentById.get(targetId);
67
+
68
+ if (!targetWs) {
69
+ server._send(ws, createError(ErrorCode.AGENT_NOT_FOUND, `Agent ${msg.to} not found`));
70
+ return;
71
+ }
72
+
73
+ // Send to target
74
+ server._send(targetWs, outMsg);
75
+ // Echo back to sender
76
+ server._send(ws, outMsg);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Handle JOIN command - add agent to channel
82
+ */
83
+ export function handleJoin(server, ws, msg) {
84
+ const agent = server.agents.get(ws);
85
+ if (!agent) {
86
+ server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
87
+ return;
88
+ }
89
+
90
+ const channel = server.channels.get(msg.channel);
91
+ if (!channel) {
92
+ server._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
93
+ return;
94
+ }
95
+
96
+ // Check invite-only
97
+ if (channel.inviteOnly && !channel.invited.has(agent.id)) {
98
+ server._send(ws, createError(ErrorCode.NOT_INVITED, `Channel ${msg.channel} is invite-only`));
99
+ return;
100
+ }
101
+
102
+ // Add to channel
103
+ channel.agents.add(ws);
104
+ agent.channels.add(msg.channel);
105
+
106
+ server._log('join', { agent: agent.id, channel: msg.channel });
107
+
108
+ // Notify others
109
+ server._broadcast(msg.channel, createMessage(ServerMessageType.AGENT_JOINED, {
110
+ channel: msg.channel,
111
+ agent: `@${agent.id}`
112
+ }), ws);
113
+
114
+ // Send confirmation with agent list
115
+ const agentList = [];
116
+ for (const memberWs of channel.agents) {
117
+ const member = server.agents.get(memberWs);
118
+ if (member) agentList.push(`@${member.id}`);
119
+ }
120
+
121
+ server._send(ws, createMessage(ServerMessageType.JOINED, {
122
+ channel: msg.channel,
123
+ agents: agentList
124
+ }));
125
+
126
+ // Replay recent messages to the joining agent
127
+ server._replayMessages(ws, msg.channel);
128
+
129
+ // Send welcome prompt to the new joiner
130
+ server._send(ws, createMessage(ServerMessageType.MSG, {
131
+ from: '@server',
132
+ to: msg.channel,
133
+ content: `Welcome to ${msg.channel}, @${agent.id}! Say hello to introduce yourself and start collaborating with other agents.`
134
+ }));
135
+
136
+ // Prompt existing agents to engage with the new joiner (if there are others)
137
+ const otherAgents = [];
138
+ for (const memberWs of channel.agents) {
139
+ if (memberWs !== ws) {
140
+ const member = server.agents.get(memberWs);
141
+ if (member) otherAgents.push({ ws: memberWs, id: member.id });
142
+ }
143
+ }
144
+
145
+ if (otherAgents.length > 0) {
146
+ const welcomePrompt = createMessage(ServerMessageType.MSG, {
147
+ from: '@server',
148
+ to: msg.channel,
149
+ content: `Hey ${otherAgents.map(a => `@${a.id}`).join(', ')} - new agent @${agent.id} just joined! Say hi and share what you're working on.`
150
+ });
151
+
152
+ for (const other of otherAgents) {
153
+ server._send(other.ws, welcomePrompt);
154
+ }
155
+ }
156
+
157
+ // Update channel activity
158
+ server.channelLastActivity.set(msg.channel, Date.now());
159
+ }
160
+
161
+ /**
162
+ * Handle LEAVE command - remove agent from channel
163
+ */
164
+ export function handleLeave(server, ws, msg) {
165
+ const agent = server.agents.get(ws);
166
+ if (!agent) {
167
+ server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
168
+ return;
169
+ }
170
+
171
+ const channel = server.channels.get(msg.channel);
172
+ if (!channel) return;
173
+
174
+ channel.agents.delete(ws);
175
+ agent.channels.delete(msg.channel);
176
+
177
+ server._log('leave', { agent: agent.id, channel: msg.channel });
178
+
179
+ // Notify others
180
+ server._broadcast(msg.channel, createMessage(ServerMessageType.AGENT_LEFT, {
181
+ channel: msg.channel,
182
+ agent: `@${agent.id}`
183
+ }));
184
+
185
+ server._send(ws, createMessage(ServerMessageType.LEFT, {
186
+ channel: msg.channel
187
+ }));
188
+ }
189
+
190
+ /**
191
+ * Handle LIST_CHANNELS command
192
+ */
193
+ export function handleListChannels(server, ws) {
194
+ const list = [];
195
+ for (const [name, channel] of server.channels) {
196
+ if (!channel.inviteOnly) {
197
+ list.push({
198
+ name,
199
+ agents: channel.agents.size
200
+ });
201
+ }
202
+ }
203
+
204
+ server._send(ws, createMessage(ServerMessageType.CHANNELS, { list }));
205
+ }
206
+
207
+ /**
208
+ * Handle LIST_AGENTS command
209
+ */
210
+ export function handleListAgents(server, ws, msg) {
211
+ const channel = server.channels.get(msg.channel);
212
+ if (!channel) {
213
+ server._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
214
+ return;
215
+ }
216
+
217
+ const list = [];
218
+ for (const memberWs of channel.agents) {
219
+ const member = server.agents.get(memberWs);
220
+ if (member) {
221
+ list.push({
222
+ id: `@${member.id}`,
223
+ name: member.name,
224
+ presence: member.presence || 'online',
225
+ status_text: member.statusText || null
226
+ });
227
+ }
228
+ }
229
+
230
+ server._send(ws, createMessage(ServerMessageType.AGENTS, {
231
+ channel: msg.channel,
232
+ list
233
+ }));
234
+ }
235
+
236
+ /**
237
+ * Handle CREATE_CHANNEL command
238
+ */
239
+ export function handleCreateChannel(server, ws, msg) {
240
+ const agent = server.agents.get(ws);
241
+ if (!agent) {
242
+ server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
243
+ return;
244
+ }
245
+
246
+ if (server.channels.has(msg.channel)) {
247
+ server._send(ws, createError(ErrorCode.CHANNEL_EXISTS, `Channel ${msg.channel} already exists`));
248
+ return;
249
+ }
250
+
251
+ const channel = server._createChannel(msg.channel, msg.invite_only || false);
252
+
253
+ // Creator is automatically invited and joined
254
+ if (channel.inviteOnly) {
255
+ channel.invited.add(agent.id);
256
+ }
257
+
258
+ server._log('create_channel', { agent: agent.id, channel: msg.channel, inviteOnly: channel.inviteOnly });
259
+
260
+ // Auto-join creator
261
+ channel.agents.add(ws);
262
+ agent.channels.add(msg.channel);
263
+
264
+ server._send(ws, createMessage(ServerMessageType.JOINED, {
265
+ channel: msg.channel,
266
+ agents: [`@${agent.id}`]
267
+ }));
268
+ }
269
+
270
+ /**
271
+ * Handle INVITE command
272
+ */
273
+ export function handleInvite(server, ws, msg) {
274
+ const agent = server.agents.get(ws);
275
+ if (!agent) {
276
+ server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
277
+ return;
278
+ }
279
+
280
+ const channel = server.channels.get(msg.channel);
281
+ if (!channel) {
282
+ server._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
283
+ return;
284
+ }
285
+
286
+ // Must be a member to invite
287
+ if (!agent.channels.has(msg.channel)) {
288
+ server._send(ws, createError(ErrorCode.NOT_INVITED, `Not a member of ${msg.channel}`));
289
+ return;
290
+ }
291
+
292
+ const targetId = msg.agent.slice(1);
293
+ channel.invited.add(targetId);
294
+
295
+ server._log('invite', { agent: agent.id, target: targetId, channel: msg.channel });
296
+
297
+ // Notify target if connected
298
+ const targetWs = server.agentById.get(targetId);
299
+ if (targetWs) {
300
+ server._send(targetWs, createMessage(ServerMessageType.MSG, {
301
+ from: `@${agent.id}`,
302
+ to: msg.agent,
303
+ content: `You have been invited to ${msg.channel}`
304
+ }));
305
+ }
306
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Presence Handlers
3
+ * Handles presence status updates
4
+ */
5
+
6
+ import {
7
+ ServerMessageType,
8
+ ErrorCode,
9
+ createMessage,
10
+ createError,
11
+ } from '../../protocol.js';
12
+
13
+ /**
14
+ * Handle SET_PRESENCE command
15
+ */
16
+ export function handleSetPresence(server, ws, msg) {
17
+ const agent = server.agents.get(ws);
18
+ if (!agent) {
19
+ server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
20
+ return;
21
+ }
22
+
23
+ const oldPresence = agent.presence;
24
+ agent.presence = msg.status;
25
+ agent.statusText = msg.status_text || null;
26
+
27
+ server._log('presence_changed', {
28
+ agent: agent.id,
29
+ from: oldPresence,
30
+ to: msg.status,
31
+ statusText: agent.statusText
32
+ });
33
+
34
+ // Broadcast presence change to all channels the agent is in
35
+ const presenceMsg = createMessage(ServerMessageType.PRESENCE_CHANGED, {
36
+ agent_id: `@${agent.id}`,
37
+ presence: agent.presence,
38
+ status_text: agent.statusText
39
+ });
40
+
41
+ for (const channelName of agent.channels) {
42
+ server._broadcast(channelName, presenceMsg);
43
+ }
44
+ }