@tjamescouch/agentchat 0.11.0 → 0.12.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/bin/agentchat.js CHANGED
@@ -48,6 +48,10 @@ import {
48
48
  DEFAULT_RATINGS_PATH,
49
49
  DEFAULT_RATING
50
50
  } from '../lib/reputation.js';
51
+ import {
52
+ ServerDirectory,
53
+ DEFAULT_DIRECTORY_PATH
54
+ } from '../lib/server-directory.js';
51
55
 
52
56
  program
53
57
  .name('agentchat')
@@ -504,6 +508,39 @@ program
504
508
  }
505
509
  });
506
510
 
511
+ // Verify agent identity command
512
+ program
513
+ .command('verify <server> <agent>')
514
+ .description('Verify another agent\'s identity via challenge-response')
515
+ .option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
516
+ .action(async (server, agent, options) => {
517
+ try {
518
+ const client = new AgentChatClient({ server, identity: options.identity });
519
+ await client.connect();
520
+
521
+ console.log(`Verifying identity of ${agent}...`);
522
+
523
+ const result = await client.verify(agent);
524
+
525
+ if (result.verified) {
526
+ console.log('Identity verified!');
527
+ console.log(` Agent: ${result.agent}`);
528
+ console.log(` Public Key:`);
529
+ console.log(result.pubkey.split('\n').map(line => ` ${line}`).join('\n'));
530
+ } else {
531
+ console.log('Verification failed!');
532
+ console.log(` Target: ${result.target}`);
533
+ console.log(` Reason: ${result.reason}`);
534
+ }
535
+
536
+ client.disconnect();
537
+ process.exit(result.verified ? 0 : 1);
538
+ } catch (err) {
539
+ console.error('Error:', err.message);
540
+ process.exit(1);
541
+ }
542
+ });
543
+
507
544
  // Identity management command
508
545
  program
509
546
  .command('identity')
@@ -511,6 +548,8 @@ program
511
548
  .option('-g, --generate', 'Generate new keypair')
512
549
  .option('-s, --show', 'Show current identity')
513
550
  .option('-e, --export', 'Export public key for sharing (JSON to stdout)')
551
+ .option('-r, --rotate', 'Rotate to new keypair (signs new key with old key)')
552
+ .option('--verify-chain', 'Verify the rotation chain')
514
553
  .option('-f, --file <path>', 'Identity file path', DEFAULT_IDENTITY_PATH)
515
554
  .option('-n, --name <name>', 'Agent name (for --generate)', `agent-${process.pid}`)
516
555
  .option('--force', 'Overwrite existing identity')
@@ -551,6 +590,54 @@ program
551
590
  const identity = await Identity.load(options.file);
552
591
  console.log(JSON.stringify(identity.export(), null, 2));
553
592
 
593
+ } else if (options.rotate) {
594
+ // Rotate to new keypair
595
+ const identity = await Identity.load(options.file);
596
+ const oldAgentId = identity.getAgentId();
597
+ const oldFingerprint = identity.getFingerprint();
598
+
599
+ console.log('Rotating identity...');
600
+ console.log(` Old Agent ID: ${oldAgentId}`);
601
+ console.log(` Old Fingerprint: ${oldFingerprint}`);
602
+
603
+ const record = identity.rotate();
604
+ await identity.save(options.file);
605
+
606
+ console.log('');
607
+ console.log('Rotation complete:');
608
+ console.log(` New Agent ID: ${identity.getAgentId()}`);
609
+ console.log(` New Fingerprint: ${identity.getFingerprint()}`);
610
+ console.log(` Total rotations: ${identity.rotations.length}`);
611
+ console.log('');
612
+ console.log('The new key has been signed by the old key for chain of custody.');
613
+ console.log('Share the rotation record to prove key continuity.');
614
+
615
+ } else if (options.verifyChain) {
616
+ // Verify rotation chain
617
+ const identity = await Identity.load(options.file);
618
+
619
+ if (identity.rotations.length === 0) {
620
+ console.log('No rotations to verify (original identity).');
621
+ console.log(` Agent ID: ${identity.getAgentId()}`);
622
+ process.exit(0);
623
+ }
624
+
625
+ console.log(`Verifying rotation chain (${identity.rotations.length} rotation(s))...`);
626
+ const result = identity.verifyRotationChain();
627
+
628
+ if (result.valid) {
629
+ console.log('Chain verified successfully!');
630
+ console.log(` Original Agent ID: ${identity.getOriginalAgentId()}`);
631
+ console.log(` Current Agent ID: ${identity.getAgentId()}`);
632
+ console.log(` Rotations: ${identity.rotations.length}`);
633
+ } else {
634
+ console.error('Chain verification FAILED:');
635
+ for (const error of result.errors) {
636
+ console.error(` - ${error}`);
637
+ }
638
+ process.exit(1);
639
+ }
640
+
554
641
  } else {
555
642
  // Default: show if exists, otherwise show help
556
643
  const exists = await Identity.exists(options.file);
@@ -1117,6 +1204,91 @@ program
1117
1204
  }
1118
1205
  });
1119
1206
 
1207
+ // Discover command - find public AgentChat servers
1208
+ program
1209
+ .command('discover')
1210
+ .description('Discover available AgentChat servers')
1211
+ .option('--add <url>', 'Add a server to the directory')
1212
+ .option('--remove <url>', 'Remove a server from the directory')
1213
+ .option('--name <name>', 'Server name (for --add)')
1214
+ .option('--description <desc>', 'Server description (for --add)')
1215
+ .option('--region <region>', 'Server region (for --add)')
1216
+ .option('--online', 'Only show online servers')
1217
+ .option('--json', 'Output as JSON')
1218
+ .option('--no-check', 'List servers without health check')
1219
+ .option('--directory <path>', 'Custom directory file path', DEFAULT_DIRECTORY_PATH)
1220
+ .action(async (options) => {
1221
+ try {
1222
+ const directory = new ServerDirectory({ directoryPath: options.directory });
1223
+ await directory.load();
1224
+
1225
+ // Add server
1226
+ if (options.add) {
1227
+ await directory.addServer({
1228
+ url: options.add,
1229
+ name: options.name || options.add,
1230
+ description: options.description || '',
1231
+ region: options.region || 'unknown'
1232
+ });
1233
+ console.log(`Added server: ${options.add}`);
1234
+ process.exit(0);
1235
+ }
1236
+
1237
+ // Remove server
1238
+ if (options.remove) {
1239
+ await directory.removeServer(options.remove);
1240
+ console.log(`Removed server: ${options.remove}`);
1241
+ process.exit(0);
1242
+ }
1243
+
1244
+ // List/discover servers
1245
+ let servers;
1246
+ if (options.check === false) {
1247
+ servers = directory.list().map(s => ({ ...s, status: 'unknown' }));
1248
+ } else {
1249
+ console.error('Checking server status...');
1250
+ servers = await directory.discover({ onlineOnly: options.online });
1251
+ }
1252
+
1253
+ if (options.json) {
1254
+ console.log(JSON.stringify(servers, null, 2));
1255
+ } else {
1256
+ if (servers.length === 0) {
1257
+ console.log('No servers found.');
1258
+ } else {
1259
+ console.log(`\nFound ${servers.length} server(s):\n`);
1260
+ for (const server of servers) {
1261
+ const statusIcon = server.status === 'online' ? '\u2713' :
1262
+ server.status === 'offline' ? '\u2717' : '?';
1263
+ console.log(` ${statusIcon} ${server.name}`);
1264
+ console.log(` URL: ${server.url}`);
1265
+ console.log(` Status: ${server.status}`);
1266
+ if (server.description) {
1267
+ console.log(` Description: ${server.description}`);
1268
+ }
1269
+ if (server.region) {
1270
+ console.log(` Region: ${server.region}`);
1271
+ }
1272
+ if (server.health) {
1273
+ console.log(` Agents: ${server.health.agents?.connected || 0}`);
1274
+ console.log(` Uptime: ${server.health.uptime_seconds || 0}s`);
1275
+ }
1276
+ if (server.error) {
1277
+ console.log(` Error: ${server.error}`);
1278
+ }
1279
+ console.log('');
1280
+ }
1281
+ }
1282
+ console.log(`Directory: ${options.directory}`);
1283
+ }
1284
+
1285
+ process.exit(0);
1286
+ } catch (err) {
1287
+ console.error('Error:', err.message);
1288
+ process.exit(1);
1289
+ }
1290
+ });
1291
+
1120
1292
  // Deploy command
1121
1293
  program
1122
1294
  .command('deploy')
package/lib/client.js CHANGED
@@ -10,7 +10,8 @@ import {
10
10
  ServerMessageType,
11
11
  createMessage,
12
12
  serialize,
13
- parse
13
+ parse,
14
+ generateNonce
14
15
  } from './protocol.js';
15
16
  import { Identity } from './identity.js';
16
17
  import {
@@ -532,6 +533,118 @@ export class AgentChatClient extends EventEmitter {
532
533
  });
533
534
  }
534
535
 
536
+ // ===== IDENTITY VERIFICATION METHODS =====
537
+
538
+ /**
539
+ * Request identity verification from another agent
540
+ * Sends a challenge nonce that the target must sign to prove they control their identity
541
+ * @param {string} target - Target agent to verify (@id)
542
+ * @returns {Promise<object>} Verification result with pubkey if successful
543
+ */
544
+ async verify(target) {
545
+ const targetAgent = target.startsWith('@') ? target : `@${target}`;
546
+ const nonce = generateNonce();
547
+
548
+ const msg = {
549
+ type: ClientMessageType.VERIFY_REQUEST,
550
+ target: targetAgent,
551
+ nonce
552
+ };
553
+
554
+ this._send(msg);
555
+
556
+ return new Promise((resolve, reject) => {
557
+ const timeout = setTimeout(() => {
558
+ this.removeListener('verify_success', onSuccess);
559
+ this.removeListener('verify_failed', onFailed);
560
+ this.removeListener('error', onError);
561
+ reject(new Error('Verification timeout'));
562
+ }, 35000); // Slightly longer than server timeout
563
+
564
+ const onSuccess = (response) => {
565
+ if (response.agent === targetAgent || response.target === targetAgent) {
566
+ clearTimeout(timeout);
567
+ this.removeListener('verify_failed', onFailed);
568
+ this.removeListener('error', onError);
569
+ resolve({
570
+ verified: true,
571
+ agent: response.agent,
572
+ pubkey: response.pubkey,
573
+ request_id: response.request_id
574
+ });
575
+ }
576
+ };
577
+
578
+ const onFailed = (response) => {
579
+ if (response.target === targetAgent) {
580
+ clearTimeout(timeout);
581
+ this.removeListener('verify_success', onSuccess);
582
+ this.removeListener('error', onError);
583
+ resolve({
584
+ verified: false,
585
+ target: response.target,
586
+ reason: response.reason,
587
+ request_id: response.request_id
588
+ });
589
+ }
590
+ };
591
+
592
+ const onError = (err) => {
593
+ clearTimeout(timeout);
594
+ this.removeListener('verify_success', onSuccess);
595
+ this.removeListener('verify_failed', onFailed);
596
+ reject(new Error(err.message));
597
+ };
598
+
599
+ this.on('verify_success', onSuccess);
600
+ this.on('verify_failed', onFailed);
601
+ this.once('error', onError);
602
+ });
603
+ }
604
+
605
+ /**
606
+ * Respond to a verification request by signing the nonce
607
+ * This is typically called automatically when a VERIFY_REQUEST is received
608
+ * @param {string} requestId - The verification request ID
609
+ * @param {string} nonce - The nonce to sign
610
+ */
611
+ async respondToVerification(requestId, nonce) {
612
+ if (!this._identity || !this._identity.privkey) {
613
+ throw new Error('Responding to verification requires persistent identity.');
614
+ }
615
+
616
+ const sig = this._identity.sign(nonce);
617
+
618
+ const msg = {
619
+ type: ClientMessageType.VERIFY_RESPONSE,
620
+ request_id: requestId,
621
+ nonce,
622
+ sig
623
+ };
624
+
625
+ this._send(msg);
626
+ }
627
+
628
+ /**
629
+ * Enable automatic verification response
630
+ * When enabled, the client will automatically respond to VERIFY_REQUEST messages
631
+ * @param {boolean} enabled - Whether to enable auto-response
632
+ */
633
+ enableAutoVerification(enabled = true) {
634
+ if (enabled) {
635
+ this._autoVerifyHandler = (msg) => {
636
+ if (msg.request_id && msg.nonce && msg.from) {
637
+ this.respondToVerification(msg.request_id, msg.nonce)
638
+ .catch(err => this.emit('error', { message: `Auto-verification failed: ${err.message}` }));
639
+ }
640
+ };
641
+ this.on('verify_request', this._autoVerifyHandler);
642
+ } else if (this._autoVerifyHandler) {
643
+ this.removeListener('verify_request', this._autoVerifyHandler);
644
+ this._autoVerifyHandler = null;
645
+ }
646
+ }
647
+
535
648
  _send(msg) {
536
649
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
537
650
  this.ws.send(serialize(msg));
@@ -630,6 +743,23 @@ export class AgentChatClient extends EventEmitter {
630
743
  this.emit('search_results', msg);
631
744
  this.emit('message', msg);
632
745
  break;
746
+
747
+ // Identity verification messages
748
+ case ServerMessageType.VERIFY_REQUEST:
749
+ this.emit('verify_request', msg);
750
+ break;
751
+
752
+ case ServerMessageType.VERIFY_RESPONSE:
753
+ this.emit('verify_response', msg);
754
+ break;
755
+
756
+ case ServerMessageType.VERIFY_SUCCESS:
757
+ this.emit('verify_success', msg);
758
+ break;
759
+
760
+ case ServerMessageType.VERIFY_FAILED:
761
+ this.emit('verify_failed', msg);
762
+ break;
633
763
  }
634
764
  }
635
765
  }