@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 +172 -0
- package/lib/client.js +131 -1
- package/lib/elo_swarm.py +569 -0
- package/lib/escrow-hooks.js +237 -0
- package/lib/identity.js +134 -2
- package/lib/protocol.js +89 -3
- package/lib/server-directory.js +181 -0
- package/lib/server.js +304 -11
- package/package.json +1 -1
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
|
}
|