agentshield-sdk 7.0.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. package/types/index.d.ts +2088 -0
@@ -0,0 +1,998 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Agent Protocol
5
+ *
6
+ * Standardized secure communication protocol between shielded AI agents.
7
+ * Like mTLS but for AI agents: mutual authentication, HMAC-signed messages,
8
+ * replay protection via sequence numbers, challenge-response handshake.
9
+ *
10
+ * All local, no network calls. Uses Node.js crypto for HMAC-SHA256.
11
+ */
12
+
13
+ const crypto = require('crypto');
14
+
15
+ /** @type {string} */
16
+ const PROTOCOL_VERSION = '1.0';
17
+
18
+ // =========================================================================
19
+ // ProtocolMessage — Wire format for messages
20
+ // =========================================================================
21
+
22
+ /**
23
+ * Wire format for protocol messages exchanged between agents.
24
+ */
25
+ class ProtocolMessage {
26
+ /**
27
+ * Valid message types.
28
+ * @type {string[]}
29
+ */
30
+ static TYPES = [
31
+ 'handshake_init',
32
+ 'handshake_response',
33
+ 'handshake_complete',
34
+ 'data',
35
+ 'scan_request',
36
+ 'scan_response',
37
+ 'threat_alert',
38
+ 'channel_close',
39
+ 'heartbeat'
40
+ ];
41
+
42
+ /**
43
+ * @param {string} type - Message type (see ProtocolMessage.TYPES)
44
+ * @param {*} payload - Message payload
45
+ * @param {string} senderId - Sender agent ID
46
+ * @param {number} sequenceNum - Sequence number for replay protection
47
+ */
48
+ constructor(type, payload, senderId, sequenceNum) {
49
+ if (!ProtocolMessage.TYPES.includes(type)) {
50
+ throw new Error(`[Agent Shield] Invalid message type: ${type}`);
51
+ }
52
+ this.type = type;
53
+ this.payload = payload;
54
+ this.senderId = senderId;
55
+ this.sequenceNum = sequenceNum;
56
+ this.timestamp = Date.now();
57
+ this.id = crypto.randomBytes(8).toString('hex');
58
+ }
59
+
60
+ /**
61
+ * Serialize to JSON string with signature placeholder.
62
+ * @returns {string} JSON-encoded message
63
+ */
64
+ serialize() {
65
+ return JSON.stringify({
66
+ id: this.id,
67
+ type: this.type,
68
+ payload: this.payload,
69
+ senderId: this.senderId,
70
+ sequenceNum: this.sequenceNum,
71
+ timestamp: this.timestamp
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Parse and validate a serialized message.
77
+ * @param {string} raw - Raw JSON string
78
+ * @returns {ProtocolMessage} Parsed message
79
+ */
80
+ static deserialize(raw) {
81
+ let data;
82
+ try {
83
+ data = JSON.parse(raw);
84
+ } catch (e) {
85
+ throw new Error('[Agent Shield] Failed to deserialize message: invalid JSON');
86
+ }
87
+
88
+ if (!data.type || !data.senderId || data.sequenceNum === undefined) {
89
+ throw new Error('[Agent Shield] Malformed protocol message: missing required fields');
90
+ }
91
+
92
+ if (!ProtocolMessage.TYPES.includes(data.type)) {
93
+ throw new Error(`[Agent Shield] Unknown message type: ${data.type}`);
94
+ }
95
+
96
+ const msg = new ProtocolMessage(data.type, data.payload, data.senderId, data.sequenceNum);
97
+ msg.timestamp = data.timestamp || Date.now();
98
+ msg.id = data.id || crypto.randomBytes(8).toString('hex');
99
+ return msg;
100
+ }
101
+
102
+ /**
103
+ * Check if the message has expired.
104
+ * @param {number} timeout - Timeout in milliseconds
105
+ * @returns {boolean} True if message is older than timeout
106
+ */
107
+ isExpired(timeout) {
108
+ return (Date.now() - this.timestamp) > timeout;
109
+ }
110
+ }
111
+
112
+ // =========================================================================
113
+ // AgentIdentity — Agent identity and capabilities
114
+ // =========================================================================
115
+
116
+ /**
117
+ * Represents an agent's identity, capabilities, and trust level.
118
+ */
119
+ class AgentIdentity {
120
+ /**
121
+ * Valid trust levels.
122
+ * @type {string[]}
123
+ */
124
+ static TRUST_LEVELS = ['untrusted', 'verified', 'trusted', 'privileged'];
125
+
126
+ /**
127
+ * @param {string} agentId - Unique agent identifier
128
+ * @param {string[]} capabilities - List of capabilities this agent possesses
129
+ * @param {object} [metadata={}] - Additional metadata
130
+ */
131
+ constructor(agentId, capabilities = [], metadata = {}) {
132
+ if (!agentId) {
133
+ throw new Error('[Agent Shield] agentId is required for AgentIdentity');
134
+ }
135
+ this.agentId = agentId;
136
+ this.capabilities = Array.isArray(capabilities) ? capabilities : [];
137
+ this.metadata = metadata || {};
138
+ this.created = Date.now();
139
+ this.trustLevel = 'untrusted';
140
+ this.signature = null;
141
+ }
142
+
143
+ /**
144
+ * Create a signature over the identity fields using HMAC-SHA256.
145
+ * @param {string} secretKey - Secret key for signing
146
+ * @returns {string} Hex-encoded HMAC signature
147
+ */
148
+ sign(secretKey) {
149
+ const data = `${this.agentId}:${this.capabilities.join(',')}:${this.created}`;
150
+ this.signature = crypto.createHmac('sha256', secretKey).update(data).digest('hex');
151
+ return this.signature;
152
+ }
153
+
154
+ /**
155
+ * Verify an identity signature.
156
+ * @param {string} signature - Signature to verify
157
+ * @param {string} secretKey - Secret key used for signing
158
+ * @returns {boolean} True if signature is valid
159
+ */
160
+ verify(signature, secretKey) {
161
+ const data = `${this.agentId}:${this.capabilities.join(',')}:${this.created}`;
162
+ const expected = crypto.createHmac('sha256', secretKey).update(data).digest('hex');
163
+ try {
164
+ return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Check if agent has a specific capability.
172
+ * @param {string} cap - Capability to check
173
+ * @returns {boolean} True if agent has the capability
174
+ */
175
+ hasCapability(cap) {
176
+ return this.capabilities.includes(cap);
177
+ }
178
+
179
+ /**
180
+ * Serialize identity to a plain object.
181
+ * @returns {object} JSON-safe representation
182
+ */
183
+ toJSON() {
184
+ return {
185
+ agentId: this.agentId,
186
+ capabilities: this.capabilities,
187
+ metadata: this.metadata,
188
+ created: this.created,
189
+ trustLevel: this.trustLevel,
190
+ signature: this.signature
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Deserialize identity from a plain object.
196
+ * @param {object} json - Serialized identity
197
+ * @returns {AgentIdentity} Restored identity
198
+ */
199
+ static fromJSON(json) {
200
+ if (!json || !json.agentId) {
201
+ throw new Error('[Agent Shield] Invalid identity JSON: missing agentId');
202
+ }
203
+ const identity = new AgentIdentity(json.agentId, json.capabilities, json.metadata);
204
+ identity.created = json.created || Date.now();
205
+ identity.trustLevel = json.trustLevel || 'untrusted';
206
+ identity.signature = json.signature || null;
207
+ return identity;
208
+ }
209
+ }
210
+
211
+ // =========================================================================
212
+ // SecureChannel — Encrypted bidirectional channel
213
+ // =========================================================================
214
+
215
+ /**
216
+ * Encrypted bidirectional communication channel between two agents.
217
+ * Uses HMAC-SHA256 for message signing and XOR cipher for encryption.
218
+ */
219
+ class SecureChannel {
220
+ /**
221
+ * @param {AgentIdentity} localIdentity - Local agent identity
222
+ * @param {AgentIdentity} remoteIdentity - Remote agent identity
223
+ * @param {string} sharedSecret - Shared secret for encryption and signing
224
+ */
225
+ constructor(localIdentity, remoteIdentity, sharedSecret) {
226
+ if (!localIdentity || !remoteIdentity || !sharedSecret) {
227
+ throw new Error('[Agent Shield] SecureChannel requires localIdentity, remoteIdentity, and sharedSecret');
228
+ }
229
+ this.localIdentity = localIdentity;
230
+ this.remoteIdentity = remoteIdentity;
231
+ this.sharedSecret = sharedSecret;
232
+ this.open = true;
233
+ this.sendSeq = 0;
234
+ this.recvSeq = 0;
235
+ this.messageHistory = [];
236
+ this._maxHistory = 1000;
237
+ this._maxLatencies = 100;
238
+ this.latencies = [];
239
+ this.createdAt = Date.now();
240
+ this._pendingTimestamps = new Map();
241
+ this._lastPendingPurge = Date.now();
242
+ this._pendingPurgeIntervalMs = 60000;
243
+ }
244
+
245
+ /**
246
+ * Encrypt and send a protocol message.
247
+ * @param {*} payload - Message payload
248
+ * @param {string} [type='data'] - Message type
249
+ * @returns {string} Signed, encrypted message (serialized)
250
+ */
251
+ send(payload, type = 'data') {
252
+ if (!this.open) {
253
+ throw new Error('[Agent Shield] Cannot send on a closed channel');
254
+ }
255
+
256
+ const msg = new ProtocolMessage(type, payload, this.localIdentity.agentId, this.sendSeq);
257
+ const serialized = msg.serialize();
258
+ const encrypted = this._encrypt(serialized, this.sharedSecret);
259
+ const signature = this._sign(encrypted, this.sharedSecret);
260
+
261
+ const envelope = JSON.stringify({
262
+ encrypted,
263
+ signature,
264
+ senderId: this.localIdentity.agentId,
265
+ sequenceNum: this.sendSeq
266
+ });
267
+
268
+ this._pendingTimestamps.set(this.sendSeq, Date.now());
269
+ this.sendSeq++;
270
+
271
+ if (this.messageHistory.length >= this._maxHistory) {
272
+ this.messageHistory = this.messageHistory.slice(-Math.floor(this._maxHistory * 0.75));
273
+ }
274
+ this.messageHistory.push({
275
+ direction: 'sent',
276
+ type,
277
+ sequenceNum: msg.sequenceNum,
278
+ timestamp: msg.timestamp
279
+ });
280
+
281
+ return envelope;
282
+ }
283
+
284
+ /**
285
+ * Receive, verify, and decrypt an incoming message.
286
+ * @param {string} rawMessage - Raw message envelope (JSON string)
287
+ * @returns {ProtocolMessage} Verified and decrypted message
288
+ */
289
+ receive(rawMessage) {
290
+ if (!this.open) {
291
+ throw new Error('[Agent Shield] Cannot receive on a closed channel');
292
+ }
293
+
294
+ let envelope;
295
+ try {
296
+ envelope = JSON.parse(rawMessage);
297
+ } catch (e) {
298
+ throw new Error('[Agent Shield] Invalid message envelope: bad JSON');
299
+ }
300
+
301
+ const { encrypted, signature, sequenceNum } = envelope;
302
+
303
+ // Verify HMAC signature
304
+ if (!this._verify(encrypted, signature, this.sharedSecret)) {
305
+ throw new Error('[Agent Shield] Message signature verification failed');
306
+ }
307
+
308
+ // Replay protection: sequence number must match expected
309
+ if (sequenceNum < this.recvSeq) {
310
+ throw new Error(`[Agent Shield] Replay detected: sequence ${sequenceNum} already processed (expected >= ${this.recvSeq})`);
311
+ }
312
+
313
+ // Decrypt
314
+ const decrypted = this._decrypt(encrypted, this.sharedSecret);
315
+ const msg = ProtocolMessage.deserialize(decrypted);
316
+
317
+ this.recvSeq = sequenceNum + 1;
318
+
319
+ // Track latency if we have a pending timestamp for this sequence
320
+ if (this._pendingTimestamps.has(sequenceNum)) {
321
+ const latency = Date.now() - this._pendingTimestamps.get(sequenceNum);
322
+ this.latencies.push(latency);
323
+ if (this.latencies.length > this._maxLatencies) this.latencies.shift();
324
+ this._pendingTimestamps.delete(sequenceNum);
325
+ }
326
+
327
+ // Purge stale pending timestamps periodically (not on every message)
328
+ const now = Date.now();
329
+ if (now - this._lastPendingPurge > this._pendingPurgeIntervalMs) {
330
+ this._lastPendingPurge = now;
331
+ for (const [seq, ts] of this._pendingTimestamps) {
332
+ if (now - ts > this._pendingPurgeIntervalMs) this._pendingTimestamps.delete(seq);
333
+ }
334
+ }
335
+
336
+ if (this.messageHistory.length >= this._maxHistory) {
337
+ this.messageHistory = this.messageHistory.slice(-Math.floor(this._maxHistory * 0.75));
338
+ }
339
+ this.messageHistory.push({
340
+ direction: 'received',
341
+ type: msg.type,
342
+ sequenceNum: msg.sequenceNum,
343
+ timestamp: msg.timestamp
344
+ });
345
+
346
+ return msg;
347
+ }
348
+
349
+ /**
350
+ * Gracefully close the channel with a close notification.
351
+ * @returns {string|null} Close notification message, or null if already closed
352
+ */
353
+ close() {
354
+ if (!this.open) {
355
+ return null;
356
+ }
357
+
358
+ let closeMsg = null;
359
+ try {
360
+ closeMsg = this.send({ reason: 'channel_close' }, 'channel_close');
361
+ } catch (e) {
362
+ // Channel may already be in a bad state, just close it
363
+ }
364
+
365
+ this.open = false;
366
+ this._pendingTimestamps.clear();
367
+ return closeMsg;
368
+ }
369
+
370
+ /**
371
+ * Check if the channel is currently open.
372
+ * @returns {boolean} True if channel is open
373
+ */
374
+ isOpen() {
375
+ return this.open;
376
+ }
377
+
378
+ /**
379
+ * Get the average message round-trip latency.
380
+ * @returns {number} Average latency in milliseconds, or 0 if no data
381
+ */
382
+ getLatency() {
383
+ if (this.latencies.length === 0) return 0;
384
+ const sum = this.latencies.reduce((a, b) => a + b, 0);
385
+ return Math.round(sum / this.latencies.length);
386
+ }
387
+
388
+ /**
389
+ * AES-256-GCM authenticated encryption.
390
+ * @param {string} data - Plaintext data (UTF-8)
391
+ * @param {string} secret - Secret key
392
+ * @returns {string} Base64-encoded JSON envelope { iv, encrypted, authTag }
393
+ * @private
394
+ */
395
+ _encrypt(data, secret) {
396
+ const key = crypto.createHash('sha256').update(secret).digest();
397
+ const iv = crypto.randomBytes(12); // 96-bit IV for GCM
398
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
399
+ const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
400
+ const authTag = cipher.getAuthTag();
401
+ return Buffer.from(JSON.stringify({
402
+ iv: iv.toString('base64'),
403
+ ct: encrypted.toString('base64'),
404
+ at: authTag.toString('base64')
405
+ })).toString('base64');
406
+ }
407
+
408
+ /**
409
+ * AES-256-GCM authenticated decryption.
410
+ * @param {string} data - Base64-encoded JSON envelope from _encrypt
411
+ * @param {string} secret - Secret key
412
+ * @returns {string} Decrypted plaintext (UTF-8)
413
+ * @private
414
+ */
415
+ _decrypt(data, secret) {
416
+ const key = crypto.createHash('sha256').update(secret).digest();
417
+ const envelope = JSON.parse(Buffer.from(data, 'base64').toString('utf8'));
418
+ const iv = Buffer.from(envelope.iv, 'base64');
419
+ const encrypted = Buffer.from(envelope.ct, 'base64');
420
+ const authTag = Buffer.from(envelope.at, 'base64');
421
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
422
+ decipher.setAuthTag(authTag);
423
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
424
+ }
425
+
426
+ /**
427
+ * Create an HMAC-SHA256 signature.
428
+ * @param {string} data - Data to sign
429
+ * @param {string} key - Signing key
430
+ * @returns {string} Hex-encoded HMAC signature
431
+ * @private
432
+ */
433
+ _sign(data, key) {
434
+ return crypto.createHmac('sha256', key).update(data).digest('hex');
435
+ }
436
+
437
+ /**
438
+ * Verify an HMAC-SHA256 signature using timing-safe comparison.
439
+ * @param {string} data - Signed data
440
+ * @param {string} signature - Signature to verify
441
+ * @param {string} key - Signing key
442
+ * @returns {boolean} True if signature is valid
443
+ * @private
444
+ */
445
+ _verify(data, signature, key) {
446
+ const expected = this._sign(data, key);
447
+ try {
448
+ return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
449
+ } catch {
450
+ return false;
451
+ }
452
+ }
453
+ }
454
+
455
+ // =========================================================================
456
+ // HandshakeManager — Mutual authentication handshake
457
+ // =========================================================================
458
+
459
+ /**
460
+ * Manages mutual authentication handshakes between agents using
461
+ * a challenge-response pattern with nonces and timestamp freshness.
462
+ */
463
+ class HandshakeManager {
464
+ /**
465
+ * @param {AgentIdentity} localIdentity - Local agent identity
466
+ * @param {string} secretKey - Secret key for HMAC operations
467
+ */
468
+ constructor(localIdentity, secretKey) {
469
+ if (!localIdentity || !secretKey) {
470
+ throw new Error('[Agent Shield] HandshakeManager requires localIdentity and secretKey');
471
+ }
472
+ this.localIdentity = localIdentity;
473
+ this.secretKey = secretKey;
474
+ this.pendingHandshakes = new Map();
475
+ this.maxAge = 30000; // 30 seconds freshness window
476
+ }
477
+
478
+ /**
479
+ * Initiate a handshake with a remote agent.
480
+ * @param {string} remoteId - Remote agent ID
481
+ * @returns {object} Handshake request with nonce, timestamp, and capabilities
482
+ */
483
+ initiateHandshake(remoteId) {
484
+ const nonce = crypto.randomBytes(16).toString('hex');
485
+ const timestamp = Date.now();
486
+
487
+ const request = {
488
+ type: 'handshake_init',
489
+ fromAgent: this.localIdentity.agentId,
490
+ toAgent: remoteId,
491
+ nonce,
492
+ timestamp,
493
+ capabilities: this.localIdentity.capabilities,
494
+ protocolVersion: PROTOCOL_VERSION
495
+ };
496
+
497
+ // Sign the request
498
+ const signatureInput = `${request.fromAgent}:${request.toAgent}:${request.nonce}:${request.timestamp}`;
499
+ request.signature = crypto.createHmac('sha256', this.secretKey).update(signatureInput).digest('hex');
500
+
501
+ // Store pending handshake state
502
+ this.pendingHandshakes.set(remoteId, {
503
+ nonce,
504
+ timestamp,
505
+ state: 'initiated'
506
+ });
507
+
508
+ return request;
509
+ }
510
+
511
+ /**
512
+ * Respond to an incoming handshake request. Validates timestamp freshness
513
+ * and creates a response with a counter-nonce.
514
+ * @param {object} request - Incoming handshake request
515
+ * @returns {object} Handshake response with counter-nonce
516
+ */
517
+ respondToHandshake(request) {
518
+ if (!request || request.type !== 'handshake_init') {
519
+ throw new Error('[Agent Shield] Invalid handshake request');
520
+ }
521
+
522
+ // Check timestamp freshness
523
+ const age = Date.now() - request.timestamp;
524
+ if (age > this.maxAge) {
525
+ throw new Error(`[Agent Shield] Handshake request expired: ${age}ms old (max ${this.maxAge}ms)`);
526
+ }
527
+
528
+ // Verify request signature
529
+ const signatureInput = `${request.fromAgent}:${request.toAgent}:${request.nonce}:${request.timestamp}`;
530
+ const expectedSig = crypto.createHmac('sha256', this.secretKey).update(signatureInput).digest('hex');
531
+
532
+ try {
533
+ const valid = crypto.timingSafeEqual(
534
+ Buffer.from(request.signature, 'hex'),
535
+ Buffer.from(expectedSig, 'hex')
536
+ );
537
+ if (!valid) {
538
+ throw new Error('[Agent Shield] Handshake request signature invalid');
539
+ }
540
+ } catch (e) {
541
+ if (e.message.includes('signature invalid')) throw e;
542
+ throw new Error('[Agent Shield] Handshake request signature invalid');
543
+ }
544
+
545
+ // Create counter-nonce and response
546
+ const counterNonce = crypto.randomBytes(16).toString('hex');
547
+ const timestamp = Date.now();
548
+
549
+ const response = {
550
+ type: 'handshake_response',
551
+ fromAgent: this.localIdentity.agentId,
552
+ toAgent: request.fromAgent,
553
+ nonce: request.nonce,
554
+ counterNonce,
555
+ timestamp,
556
+ capabilities: this.localIdentity.capabilities,
557
+ protocolVersion: PROTOCOL_VERSION
558
+ };
559
+
560
+ const responseSigInput = `${response.fromAgent}:${response.toAgent}:${response.nonce}:${response.counterNonce}:${response.timestamp}`;
561
+ response.signature = crypto.createHmac('sha256', this.secretKey).update(responseSigInput).digest('hex');
562
+
563
+ // Store pending state
564
+ this.pendingHandshakes.set(request.fromAgent, {
565
+ nonce: request.nonce,
566
+ counterNonce,
567
+ timestamp,
568
+ state: 'responded'
569
+ });
570
+
571
+ return response;
572
+ }
573
+
574
+ /**
575
+ * Complete the handshake by validating the response and deriving a shared secret.
576
+ * @param {object} response - Handshake response from remote agent
577
+ * @returns {object} { sharedSecret, remoteCapabilities }
578
+ */
579
+ completeHandshake(response) {
580
+ if (!response || response.type !== 'handshake_response') {
581
+ throw new Error('[Agent Shield] Invalid handshake response');
582
+ }
583
+
584
+ const pending = this.pendingHandshakes.get(response.fromAgent);
585
+ if (!pending) {
586
+ throw new Error(`[Agent Shield] No pending handshake for agent: ${response.fromAgent}`);
587
+ }
588
+
589
+ // Check timestamp freshness
590
+ const age = Date.now() - response.timestamp;
591
+ if (age > this.maxAge) {
592
+ throw new Error(`[Agent Shield] Handshake response expired: ${age}ms old (max ${this.maxAge}ms)`);
593
+ }
594
+
595
+ // Verify nonce matches what we sent
596
+ if (response.nonce !== pending.nonce) {
597
+ throw new Error('[Agent Shield] Handshake nonce mismatch');
598
+ }
599
+
600
+ // Verify response signature
601
+ const responseSigInput = `${response.fromAgent}:${response.toAgent}:${response.nonce}:${response.counterNonce}:${response.timestamp}`;
602
+ const expectedSig = crypto.createHmac('sha256', this.secretKey).update(responseSigInput).digest('hex');
603
+
604
+ try {
605
+ const valid = crypto.timingSafeEqual(
606
+ Buffer.from(response.signature, 'hex'),
607
+ Buffer.from(expectedSig, 'hex')
608
+ );
609
+ if (!valid) {
610
+ throw new Error('[Agent Shield] Handshake response signature invalid');
611
+ }
612
+ } catch (e) {
613
+ if (e.message.includes('signature invalid')) throw e;
614
+ throw new Error('[Agent Shield] Handshake response signature invalid');
615
+ }
616
+
617
+ // Derive shared secret from combined nonces
618
+ const sharedSecret = crypto.createHmac('sha256', this.secretKey)
619
+ .update(`${response.nonce}:${response.counterNonce}`)
620
+ .digest('hex');
621
+
622
+ // Mark handshake complete
623
+ this.pendingHandshakes.set(response.fromAgent, {
624
+ ...pending,
625
+ counterNonce: response.counterNonce,
626
+ state: 'completed',
627
+ sharedSecret
628
+ });
629
+
630
+ return {
631
+ sharedSecret,
632
+ remoteCapabilities: response.capabilities || []
633
+ };
634
+ }
635
+ }
636
+
637
+ // =========================================================================
638
+ // MessageRouter — Routes messages between multiple agents
639
+ // =========================================================================
640
+
641
+ /**
642
+ * Routes protocol messages between multiple connected agents.
643
+ */
644
+ class MessageRouter {
645
+ constructor() {
646
+ /** @type {Map<string, SecureChannel>} */
647
+ this.routes = new Map();
648
+ }
649
+
650
+ /**
651
+ * Register a route to an agent.
652
+ * @param {string} agentId - Agent ID to route to
653
+ * @param {SecureChannel} channel - Channel to route through
654
+ */
655
+ addRoute(agentId, channel) {
656
+ if (!agentId || !channel) {
657
+ throw new Error('[Agent Shield] addRoute requires agentId and channel');
658
+ }
659
+ this.routes.set(agentId, channel);
660
+ }
661
+
662
+ /**
663
+ * Unregister a route.
664
+ * @param {string} agentId - Agent ID to remove
665
+ * @returns {boolean} True if route existed and was removed
666
+ */
667
+ removeRoute(agentId) {
668
+ return this.routes.delete(agentId);
669
+ }
670
+
671
+ /**
672
+ * Route a message to the correct channel based on recipient.
673
+ * @param {ProtocolMessage} message - Message to route (payload.recipient identifies target)
674
+ * @returns {string|null} Encrypted envelope if routed, null if no route found
675
+ */
676
+ route(message) {
677
+ const recipient = message.payload && message.payload.recipient;
678
+ if (!recipient) {
679
+ throw new Error('[Agent Shield] Message must have payload.recipient for routing');
680
+ }
681
+
682
+ const channel = this.routes.get(recipient);
683
+ if (!channel || !channel.isOpen()) {
684
+ return null;
685
+ }
686
+
687
+ return channel.send(message.payload, message.type);
688
+ }
689
+
690
+ /**
691
+ * Broadcast a threat alert to all connected agents.
692
+ * @param {object} threat - Threat information to broadcast
693
+ * @returns {string[]} Array of sent envelopes
694
+ */
695
+ broadcastThreat(threat) {
696
+ const results = [];
697
+ for (const [agentId, channel] of this.routes.entries()) {
698
+ if (channel.isOpen()) {
699
+ try {
700
+ const envelope = channel.send(threat, 'threat_alert');
701
+ results.push(envelope);
702
+ } catch (e) {
703
+ console.error(`[Agent Shield] Failed to broadcast threat to ${agentId}: ${e.message}`);
704
+ }
705
+ }
706
+ }
707
+ return results;
708
+ }
709
+
710
+ /**
711
+ * Get the network topology of connected agents.
712
+ * @returns {object} Topology map: { agents, connections, openChannels }
713
+ */
714
+ getTopology() {
715
+ const agents = [];
716
+ const connections = [];
717
+ let openChannels = 0;
718
+
719
+ for (const [agentId, channel] of this.routes.entries()) {
720
+ const isOpen = channel.isOpen();
721
+ agents.push({
722
+ agentId,
723
+ isOpen,
724
+ latency: channel.getLatency()
725
+ });
726
+ connections.push({
727
+ from: channel.localIdentity.agentId,
728
+ to: agentId,
729
+ open: isOpen
730
+ });
731
+ if (isOpen) openChannels++;
732
+ }
733
+
734
+ return { agents, connections, openChannels };
735
+ }
736
+
737
+ /**
738
+ * Get the number of active routes.
739
+ * @returns {number} Number of registered routes
740
+ */
741
+ getRouteCount() {
742
+ return this.routes.size;
743
+ }
744
+ }
745
+
746
+ // =========================================================================
747
+ // AgentProtocol — Main protocol coordinator
748
+ // =========================================================================
749
+
750
+ /**
751
+ * Main protocol coordinator for secure agent-to-agent communication.
752
+ * Manages identities, channels, handshakes, and message routing.
753
+ */
754
+ class AgentProtocol {
755
+ /**
756
+ * @param {object} [config={}]
757
+ * @param {string} config.agentId - Unique agent identifier
758
+ * @param {string} config.secretKey - Secret key for signing and encryption
759
+ * @param {string} [config.protocolVersion='1.0'] - Protocol version
760
+ * @param {number} [config.maxChannels=100] - Maximum concurrent channels
761
+ * @param {number} [config.messageTimeout=30000] - Message timeout in ms
762
+ * @param {boolean} [config.requireMutualAuth=true] - Require mutual authentication
763
+ */
764
+ constructor(config = {}) {
765
+ if (!config.agentId) {
766
+ throw new Error('[Agent Shield] AgentProtocol requires config.agentId');
767
+ }
768
+ if (!config.secretKey) {
769
+ throw new Error('[Agent Shield] AgentProtocol requires config.secretKey');
770
+ }
771
+
772
+ this.agentId = config.agentId;
773
+ this.secretKey = config.secretKey;
774
+ this.protocolVersion = config.protocolVersion || PROTOCOL_VERSION;
775
+ this.maxChannels = config.maxChannels || 100;
776
+ this.messageTimeout = config.messageTimeout || 30000;
777
+ this.requireMutualAuth = config.requireMutualAuth !== false;
778
+
779
+ /** @type {Map<string, SecureChannel>} */
780
+ this.channels = new Map();
781
+ this.router = new MessageRouter();
782
+ this.identity = null;
783
+
784
+ this.stats = {
785
+ messagesSent: 0,
786
+ messagesReceived: 0,
787
+ channelsOpened: 0,
788
+ channelsClosed: 0,
789
+ authFailures: 0,
790
+ handshakesInitiated: 0,
791
+ handshakesCompleted: 0
792
+ };
793
+ }
794
+
795
+ /**
796
+ * Create a signed agent identity.
797
+ * @param {string} agentId - Agent identifier
798
+ * @param {string[]} [capabilities=[]] - Agent capabilities
799
+ * @param {object} [metadata={}] - Additional metadata
800
+ * @returns {AgentIdentity} Signed identity
801
+ */
802
+ createIdentity(agentId, capabilities = [], metadata = {}) {
803
+ const identity = new AgentIdentity(agentId, capabilities, metadata);
804
+ identity.sign(this.secretKey);
805
+ identity.trustLevel = 'verified';
806
+
807
+ if (agentId === this.agentId) {
808
+ this.identity = identity;
809
+ }
810
+
811
+ return identity;
812
+ }
813
+
814
+ /**
815
+ * Initiate a handshake and open a secure channel to a remote agent.
816
+ * @param {AgentIdentity} remoteIdentity - Remote agent's identity
817
+ * @returns {object} { channel, handshakeRequest }
818
+ */
819
+ openChannel(remoteIdentity) {
820
+ if (this.channels.size >= this.maxChannels) {
821
+ throw new Error(`[Agent Shield] Maximum channels (${this.maxChannels}) reached`);
822
+ }
823
+
824
+ if (!this.identity) {
825
+ this.identity = this.createIdentity(this.agentId, [], {});
826
+ }
827
+
828
+ const handshake = new HandshakeManager(this.identity, this.secretKey);
829
+ const request = handshake.initiateHandshake(remoteIdentity.agentId);
830
+ this.stats.handshakesInitiated++;
831
+
832
+ // Store handshake manager for later completion
833
+ this._pendingHandshakes = this._pendingHandshakes || new Map();
834
+ this._pendingHandshakes.set(remoteIdentity.agentId, {
835
+ handshake,
836
+ remoteIdentity
837
+ });
838
+
839
+ return {
840
+ channel: null, // Channel created after handshake completes
841
+ handshakeRequest: request
842
+ };
843
+ }
844
+
845
+ /**
846
+ * Accept an incoming handshake and establish a secure channel.
847
+ * @param {object} handshakeRequest - Incoming handshake request
848
+ * @returns {object} { channel, handshakeResponse }
849
+ */
850
+ acceptChannel(handshakeRequest) {
851
+ if (this.channels.size >= this.maxChannels) {
852
+ throw new Error(`[Agent Shield] Maximum channels (${this.maxChannels}) reached`);
853
+ }
854
+
855
+ if (!this.identity) {
856
+ this.identity = this.createIdentity(this.agentId, [], {});
857
+ }
858
+
859
+ const handshake = new HandshakeManager(this.identity, this.secretKey);
860
+
861
+ let response;
862
+ try {
863
+ response = handshake.respondToHandshake(handshakeRequest);
864
+ } catch (e) {
865
+ this.stats.authFailures++;
866
+ throw e;
867
+ }
868
+
869
+ // Derive shared secret for the responder side
870
+ const sharedSecret = crypto.createHmac('sha256', this.secretKey)
871
+ .update(`${handshakeRequest.nonce}:${response.counterNonce}`)
872
+ .digest('hex');
873
+
874
+ // Create remote identity from request info
875
+ const remoteIdentity = new AgentIdentity(
876
+ handshakeRequest.fromAgent,
877
+ handshakeRequest.capabilities || [],
878
+ {}
879
+ );
880
+ remoteIdentity.trustLevel = 'verified';
881
+
882
+ const channel = new SecureChannel(this.identity, remoteIdentity, sharedSecret);
883
+ this.channels.set(handshakeRequest.fromAgent, channel);
884
+ this.router.addRoute(handshakeRequest.fromAgent, channel);
885
+ this.stats.channelsOpened++;
886
+ this.stats.handshakesCompleted++;
887
+
888
+ return {
889
+ channel,
890
+ handshakeResponse: response
891
+ };
892
+ }
893
+
894
+ /**
895
+ * Complete an initiated handshake and open the channel.
896
+ * Called by the initiator after receiving the handshake response.
897
+ * @param {object} handshakeResponse - Response from the remote agent
898
+ * @returns {SecureChannel} Established secure channel
899
+ */
900
+ completeChannel(handshakeResponse) {
901
+ this._pendingHandshakes = this._pendingHandshakes || new Map();
902
+ const pending = this._pendingHandshakes.get(handshakeResponse.fromAgent);
903
+ if (!pending) {
904
+ throw new Error(`[Agent Shield] No pending handshake for agent: ${handshakeResponse.fromAgent}`);
905
+ }
906
+
907
+ let result;
908
+ try {
909
+ result = pending.handshake.completeHandshake(handshakeResponse);
910
+ } catch (e) {
911
+ this.stats.authFailures++;
912
+ throw e;
913
+ }
914
+
915
+ const channel = new SecureChannel(this.identity, pending.remoteIdentity, result.sharedSecret);
916
+ this.channels.set(handshakeResponse.fromAgent, channel);
917
+ this.router.addRoute(handshakeResponse.fromAgent, channel);
918
+ this._pendingHandshakes.delete(handshakeResponse.fromAgent);
919
+ this.stats.channelsOpened++;
920
+ this.stats.handshakesCompleted++;
921
+
922
+ return channel;
923
+ }
924
+
925
+ /**
926
+ * Broadcast a message to multiple channels.
927
+ * @param {*} message - Message payload to broadcast
928
+ * @param {SecureChannel[]} [channels] - Channels to broadcast to (defaults to all)
929
+ * @returns {string[]} Array of sent envelopes
930
+ */
931
+ broadcast(message, channels) {
932
+ const targets = channels || Array.from(this.channels.values());
933
+ const results = [];
934
+
935
+ for (const channel of targets) {
936
+ if (channel.isOpen()) {
937
+ try {
938
+ const envelope = channel.send(message, 'data');
939
+ results.push(envelope);
940
+ this.stats.messagesSent++;
941
+ } catch (e) {
942
+ console.error(`[Agent Shield] Broadcast failed: ${e.message}`);
943
+ }
944
+ }
945
+ }
946
+
947
+ return results;
948
+ }
949
+
950
+ /**
951
+ * List all active (open) channels.
952
+ * @returns {object[]} Array of channel info objects
953
+ */
954
+ getActiveChannels() {
955
+ const active = [];
956
+ for (const [agentId, channel] of this.channels.entries()) {
957
+ if (channel.isOpen()) {
958
+ active.push({
959
+ agentId,
960
+ localAgent: channel.localIdentity.agentId,
961
+ remoteAgent: channel.remoteIdentity.agentId,
962
+ open: true,
963
+ latency: channel.getLatency(),
964
+ messageCount: channel.messageHistory.length,
965
+ createdAt: channel.createdAt
966
+ });
967
+ }
968
+ }
969
+ return active;
970
+ }
971
+
972
+ /**
973
+ * Get protocol statistics.
974
+ * @returns {object} Stats including messages sent/received, channels opened, auth failures
975
+ */
976
+ getStats() {
977
+ return {
978
+ ...this.stats,
979
+ activeChannels: Array.from(this.channels.values()).filter(c => c.isOpen()).length,
980
+ totalChannels: this.channels.size,
981
+ routeCount: this.router.getRouteCount()
982
+ };
983
+ }
984
+ }
985
+
986
+ // =========================================================================
987
+ // Exports
988
+ // =========================================================================
989
+
990
+ module.exports = {
991
+ AgentProtocol,
992
+ SecureChannel,
993
+ HandshakeManager,
994
+ AgentIdentity,
995
+ ProtocolMessage,
996
+ MessageRouter,
997
+ PROTOCOL_VERSION
998
+ };