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.
- package/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- 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
|
+
};
|