@tjamescouch/agentchat 0.11.0 → 0.13.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/chat.py +66 -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/chat.py
CHANGED
|
@@ -103,6 +103,62 @@ def poll_new(paths: dict):
|
|
|
103
103
|
return messages
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def wait_for_messages(paths: dict, interval: float = 2.0, timeout: float = 300.0):
|
|
107
|
+
"""Block until new messages arrive. Returns messages or empty list on timeout."""
|
|
108
|
+
import signal
|
|
109
|
+
import time
|
|
110
|
+
|
|
111
|
+
stop_file = paths["inbox"].parent.parent.parent / "stop"
|
|
112
|
+
|
|
113
|
+
# Handle interrupts gracefully
|
|
114
|
+
interrupted = False
|
|
115
|
+
def handle_signal(signum, frame):
|
|
116
|
+
nonlocal interrupted
|
|
117
|
+
interrupted = True
|
|
118
|
+
|
|
119
|
+
old_handler = signal.signal(signal.SIGINT, handle_signal)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
start = time.time()
|
|
123
|
+
while not interrupted and (time.time() - start) < timeout:
|
|
124
|
+
# Check stop file
|
|
125
|
+
if stop_file.exists():
|
|
126
|
+
try:
|
|
127
|
+
stop_file.unlink()
|
|
128
|
+
except FileNotFoundError:
|
|
129
|
+
pass
|
|
130
|
+
return [] # Signal to stop
|
|
131
|
+
|
|
132
|
+
# Check semaphore
|
|
133
|
+
if paths["newdata"].exists():
|
|
134
|
+
messages = read_inbox(paths)
|
|
135
|
+
# Filter out @server messages
|
|
136
|
+
messages = [m for m in messages if m.get("from") != "@server"]
|
|
137
|
+
|
|
138
|
+
if messages:
|
|
139
|
+
# Update timestamp
|
|
140
|
+
max_ts = max(m.get("ts", 0) for m in messages)
|
|
141
|
+
set_last_ts(paths, max_ts)
|
|
142
|
+
# Clear semaphore
|
|
143
|
+
try:
|
|
144
|
+
paths["newdata"].unlink()
|
|
145
|
+
except FileNotFoundError:
|
|
146
|
+
pass
|
|
147
|
+
return messages
|
|
148
|
+
|
|
149
|
+
# Semaphore but no messages after filtering - clear and continue
|
|
150
|
+
try:
|
|
151
|
+
paths["newdata"].unlink()
|
|
152
|
+
except FileNotFoundError:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
time.sleep(interval)
|
|
156
|
+
|
|
157
|
+
return [] # Timeout
|
|
158
|
+
finally:
|
|
159
|
+
signal.signal(signal.SIGINT, old_handler)
|
|
160
|
+
|
|
161
|
+
|
|
106
162
|
def main():
|
|
107
163
|
parser = argparse.ArgumentParser(description="AgentChat daemon helper")
|
|
108
164
|
parser.add_argument("--daemon-dir", type=Path, default=DEFAULT_DAEMON_DIR,
|
|
@@ -133,6 +189,11 @@ def main():
|
|
|
133
189
|
# poll command - efficient check using semaphore
|
|
134
190
|
poll_p = subparsers.add_parser("poll", help="Poll for new messages (uses semaphore, silent if none)")
|
|
135
191
|
|
|
192
|
+
# wait command - block until messages arrive
|
|
193
|
+
wait_p = subparsers.add_parser("wait", help="Block until new messages arrive")
|
|
194
|
+
wait_p.add_argument("--interval", type=float, default=2.0, help="Poll interval in seconds")
|
|
195
|
+
wait_p.add_argument("--timeout", type=float, default=300.0, help="Max wait time in seconds")
|
|
196
|
+
|
|
136
197
|
args = parser.parse_args()
|
|
137
198
|
paths = get_paths(args.daemon_dir)
|
|
138
199
|
|
|
@@ -170,6 +231,11 @@ def main():
|
|
|
170
231
|
print(json.dumps(msg))
|
|
171
232
|
# Empty list = semaphore existed but no new messages after filtering
|
|
172
233
|
|
|
234
|
+
elif args.command == "wait":
|
|
235
|
+
messages = wait_for_messages(paths, interval=args.interval, timeout=args.timeout)
|
|
236
|
+
for msg in messages:
|
|
237
|
+
print(json.dumps(msg))
|
|
238
|
+
|
|
173
239
|
|
|
174
240
|
if __name__ == "__main__":
|
|
175
241
|
main()
|
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
|
}
|