@tjamescouch/agentchat 0.12.0 → 0.14.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 +43 -0
- package/lib/chat.py +66 -0
- package/lib/identity.js +78 -0
- package/package.json +1 -1
package/bin/agentchat.js
CHANGED
|
@@ -550,6 +550,8 @@ program
|
|
|
550
550
|
.option('-e, --export', 'Export public key for sharing (JSON to stdout)')
|
|
551
551
|
.option('-r, --rotate', 'Rotate to new keypair (signs new key with old key)')
|
|
552
552
|
.option('--verify-chain', 'Verify the rotation chain')
|
|
553
|
+
.option('--revoke [reason]', 'Generate signed revocation notice (outputs JSON)')
|
|
554
|
+
.option('--verify-revocation <file>', 'Verify a revocation notice file')
|
|
553
555
|
.option('-f, --file <path>', 'Identity file path', DEFAULT_IDENTITY_PATH)
|
|
554
556
|
.option('-n, --name <name>', 'Agent name (for --generate)', `agent-${process.pid}`)
|
|
555
557
|
.option('--force', 'Overwrite existing identity')
|
|
@@ -638,6 +640,43 @@ program
|
|
|
638
640
|
process.exit(1);
|
|
639
641
|
}
|
|
640
642
|
|
|
643
|
+
} else if (options.revoke) {
|
|
644
|
+
// Generate revocation notice
|
|
645
|
+
const identity = await Identity.load(options.file);
|
|
646
|
+
const reason = typeof options.revoke === 'string' ? options.revoke : 'revoked';
|
|
647
|
+
|
|
648
|
+
console.error(`Generating revocation notice for identity...`);
|
|
649
|
+
console.error(` Agent ID: ${identity.getAgentId()}`);
|
|
650
|
+
console.error(` Reason: ${reason}`);
|
|
651
|
+
console.error('');
|
|
652
|
+
console.error('WARNING: Publishing this notice declares your key as untrusted.');
|
|
653
|
+
console.error('');
|
|
654
|
+
|
|
655
|
+
const notice = identity.revoke(reason);
|
|
656
|
+
console.log(JSON.stringify(notice, null, 2));
|
|
657
|
+
|
|
658
|
+
} else if (options.verifyRevocation) {
|
|
659
|
+
// Verify a revocation notice file
|
|
660
|
+
const noticeData = await fs.readFile(options.verifyRevocation, 'utf-8');
|
|
661
|
+
const notice = JSON.parse(noticeData);
|
|
662
|
+
|
|
663
|
+
console.log('Verifying revocation notice...');
|
|
664
|
+
const isValid = Identity.verifyRevocation(notice);
|
|
665
|
+
|
|
666
|
+
if (isValid) {
|
|
667
|
+
console.log('Revocation notice is VALID');
|
|
668
|
+
console.log(` Agent ID: ${notice.agent_id}`);
|
|
669
|
+
console.log(` Fingerprint: ${notice.fingerprint}`);
|
|
670
|
+
console.log(` Reason: ${notice.reason}`);
|
|
671
|
+
console.log(` Timestamp: ${notice.timestamp}`);
|
|
672
|
+
if (notice.original_agent_id) {
|
|
673
|
+
console.log(` Original Agent ID: ${notice.original_agent_id}`);
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
console.error('Revocation notice is INVALID');
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
|
|
641
680
|
} else {
|
|
642
681
|
// Default: show if exists, otherwise show help
|
|
643
682
|
const exists = await Identity.exists(options.file);
|
|
@@ -648,6 +687,10 @@ program
|
|
|
648
687
|
console.log(` Fingerprint: ${identity.getFingerprint()}`);
|
|
649
688
|
console.log(` Agent ID: ${identity.getAgentId()}`);
|
|
650
689
|
console.log(` Created: ${identity.created}`);
|
|
690
|
+
if (identity.rotations.length > 0) {
|
|
691
|
+
console.log(` Rotations: ${identity.rotations.length}`);
|
|
692
|
+
console.log(` Original Agent ID: ${identity.getOriginalAgentId()}`);
|
|
693
|
+
}
|
|
651
694
|
} else {
|
|
652
695
|
console.log('No identity found.');
|
|
653
696
|
console.log(`Use --generate to create one at ${options.file}`);
|
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/identity.js
CHANGED
|
@@ -295,4 +295,82 @@ export class Identity {
|
|
|
295
295
|
getOriginalAgentId() {
|
|
296
296
|
return pubkeyToAgentId(this.getOriginalPubkey());
|
|
297
297
|
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Generate a signed revocation notice for this identity
|
|
301
|
+
* A revocation notice declares that the key should no longer be trusted
|
|
302
|
+
* @param {string} reason - Reason for revocation (e.g., "compromised", "retired", "lost")
|
|
303
|
+
* @returns {object} Revocation notice with pubkey, reason, signature, timestamp
|
|
304
|
+
*/
|
|
305
|
+
revoke(reason = 'revoked') {
|
|
306
|
+
if (!this.privkey) {
|
|
307
|
+
throw new Error('Private key not available - cannot create revocation notice');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const timestamp = new Date().toISOString();
|
|
311
|
+
|
|
312
|
+
// Create revocation content to sign
|
|
313
|
+
const revocationContent = JSON.stringify({
|
|
314
|
+
type: 'REVOCATION',
|
|
315
|
+
pubkey: this.pubkey,
|
|
316
|
+
agent_id: this.getAgentId(),
|
|
317
|
+
reason,
|
|
318
|
+
timestamp
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Sign with the key being revoked (proves ownership)
|
|
322
|
+
const signature = this.sign(revocationContent);
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
type: 'REVOCATION',
|
|
326
|
+
pubkey: this.pubkey,
|
|
327
|
+
agent_id: this.getAgentId(),
|
|
328
|
+
fingerprint: this.getFingerprint(),
|
|
329
|
+
reason,
|
|
330
|
+
timestamp,
|
|
331
|
+
signature,
|
|
332
|
+
// Include rotation history for full chain verification
|
|
333
|
+
rotations: this.rotations.length > 0 ? this.rotations : undefined,
|
|
334
|
+
original_agent_id: this.rotations.length > 0 ? this.getOriginalAgentId() : undefined
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Verify a revocation notice
|
|
340
|
+
* Checks that the signature is valid using the pubkey in the notice
|
|
341
|
+
* @param {object} notice - Revocation notice to verify
|
|
342
|
+
* @returns {boolean} True if signature is valid
|
|
343
|
+
*/
|
|
344
|
+
static verifyRevocation(notice) {
|
|
345
|
+
if (!notice || notice.type !== 'REVOCATION') {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const revocationContent = JSON.stringify({
|
|
351
|
+
type: 'REVOCATION',
|
|
352
|
+
pubkey: notice.pubkey,
|
|
353
|
+
agent_id: notice.agent_id,
|
|
354
|
+
reason: notice.reason,
|
|
355
|
+
timestamp: notice.timestamp
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return Identity.verify(revocationContent, notice.signature, notice.pubkey);
|
|
359
|
+
} catch {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Check if a pubkey has been revoked by checking against a revocation notice
|
|
366
|
+
* @param {string} pubkey - Public key to check
|
|
367
|
+
* @param {object} notice - Revocation notice
|
|
368
|
+
* @returns {boolean} True if the pubkey matches the revoked key
|
|
369
|
+
*/
|
|
370
|
+
static isRevoked(pubkey, notice) {
|
|
371
|
+
if (!Identity.verifyRevocation(notice)) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
return notice.pubkey === pubkey;
|
|
375
|
+
}
|
|
298
376
|
}
|