agentshield-sdk 7.2.0 → 7.2.1
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/README.md +5 -4
- package/package.json +4 -3
- package/src/circuit-breaker.js +321 -321
- package/src/detector-core.js +3 -3
- package/src/distributed.js +402 -359
- package/src/fuzzer.js +764 -764
- package/src/index.js +23 -7
- package/src/main.js +6 -2
- package/src/mcp-security-runtime.js +30 -5
- package/src/mcp-server.js +12 -8
- package/src/middleware.js +303 -208
- package/src/multi-agent.js +421 -404
- package/src/pii.js +401 -390
- package/src/stream-scanner.js +34 -4
- package/src/testing.js +505 -505
- package/src/utils.js +199 -83
- package/types/index.d.ts +374 -0
package/src/multi-agent.js
CHANGED
|
@@ -1,404 +1,421 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Multi-Agent Protection: Agent-to-Agent Firewall (#39),
|
|
5
|
-
* Delegation Chain Tracking (#40), Consensus Verification (#41),
|
|
6
|
-
* and Shared Threat State (#42)
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const { scanText } = require('./detector-core');
|
|
10
|
-
|
|
11
|
-
// =========================================================================
|
|
12
|
-
// AGENT-TO-AGENT FIREWALL
|
|
13
|
-
// =========================================================================
|
|
14
|
-
|
|
15
|
-
class AgentFirewall {
|
|
16
|
-
/**
|
|
17
|
-
* Scans all inter-agent messages. Enforces trust boundaries.
|
|
18
|
-
*
|
|
19
|
-
* @param {object} [options]
|
|
20
|
-
* @param {object} [options.trustPolicy={}] - Trust levels between agents.
|
|
21
|
-
* @param {string} [options.defaultTrust='scan'] - Default trust: 'trust', 'scan', or 'block'.
|
|
22
|
-
* @param {Function} [options.onViolation] - Callback on firewall violation.
|
|
23
|
-
*/
|
|
24
|
-
constructor(options = {}) {
|
|
25
|
-
this.trustPolicy = options.trustPolicy || {};
|
|
26
|
-
this.defaultTrust = options.defaultTrust || 'scan';
|
|
27
|
-
this.onViolation = options.onViolation || null;
|
|
28
|
-
this.messageLog = [];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Sets trust level between two agents.
|
|
33
|
-
*
|
|
34
|
-
* @param {string} fromAgent - Sending agent ID.
|
|
35
|
-
* @param {string} toAgent - Receiving agent ID.
|
|
36
|
-
* @param {string} level - Trust level: 'trust', 'scan', or 'block'.
|
|
37
|
-
* @returns {AgentFirewall} this
|
|
38
|
-
*/
|
|
39
|
-
setTrust(fromAgent, toAgent, level) {
|
|
40
|
-
const key = `${fromAgent}->${toAgent}`;
|
|
41
|
-
this.trustPolicy[key] = level;
|
|
42
|
-
return this;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Checks an inter-agent message through the firewall.
|
|
47
|
-
*
|
|
48
|
-
* @param {string} fromAgent - Sending agent ID.
|
|
49
|
-
* @param {string} toAgent - Receiving agent ID.
|
|
50
|
-
* @param {string} message - The message content.
|
|
51
|
-
* @returns {object} { allowed: boolean, scanned: boolean, threats: Array, reason?: string }
|
|
52
|
-
*/
|
|
53
|
-
check(fromAgent, toAgent, message) {
|
|
54
|
-
const key = `${fromAgent}->${toAgent}`;
|
|
55
|
-
const trustLevel = this.trustPolicy[key] || this.defaultTrust;
|
|
56
|
-
|
|
57
|
-
const logEntry = {
|
|
58
|
-
from: fromAgent,
|
|
59
|
-
to: toAgent,
|
|
60
|
-
trustLevel,
|
|
61
|
-
timestamp: Date.now(),
|
|
62
|
-
messagePreview: message.substring(0, 100)
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Block immediately
|
|
66
|
-
if (trustLevel === 'block') {
|
|
67
|
-
logEntry.result = 'blocked';
|
|
68
|
-
this.messageLog.push(logEntry);
|
|
69
|
-
if (this.messageLog.length > 500) this.messageLog.shift();
|
|
70
|
-
|
|
71
|
-
const result = {
|
|
72
|
-
allowed: false,
|
|
73
|
-
scanned: false,
|
|
74
|
-
threats: [],
|
|
75
|
-
reason: `Messages from "${fromAgent}" to "${toAgent}" are blocked by firewall policy.`
|
|
76
|
-
};
|
|
77
|
-
if (this.onViolation) { try { this.onViolation(result); } catch (e) { console.error('[Agent Shield] onViolation callback error:', e.message); } }
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Trust — pass through without scanning
|
|
82
|
-
if (trustLevel === 'trust') {
|
|
83
|
-
logEntry.result = 'trusted';
|
|
84
|
-
this.messageLog.push(logEntry);
|
|
85
|
-
if (this.messageLog.length > 500) this.messageLog.shift();
|
|
86
|
-
|
|
87
|
-
return { allowed: true, scanned: false, threats: [] };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Scan — check for threats
|
|
91
|
-
const scanResult = scanText(message, {
|
|
92
|
-
source: `agent_message:${fromAgent}->${toAgent}`,
|
|
93
|
-
sensitivity: 'high'
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const allowed = scanResult.threats.length === 0;
|
|
97
|
-
logEntry.result = allowed ? 'passed' : 'blocked';
|
|
98
|
-
logEntry.threatCount = scanResult.threats.length;
|
|
99
|
-
this.messageLog.push(logEntry);
|
|
100
|
-
if (this.messageLog.length > 500) this.messageLog.shift();
|
|
101
|
-
|
|
102
|
-
if (!allowed && this.onViolation) {
|
|
103
|
-
try {
|
|
104
|
-
this.onViolation({
|
|
105
|
-
allowed: false,
|
|
106
|
-
from: fromAgent,
|
|
107
|
-
to: toAgent,
|
|
108
|
-
threats: scanResult.threats,
|
|
109
|
-
reason: `Inter-agent message from "${fromAgent}" contains threats.`
|
|
110
|
-
});
|
|
111
|
-
} catch (e) { console.error('[Agent Shield] onViolation callback error:', e.message); }
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
allowed,
|
|
116
|
-
scanned: true,
|
|
117
|
-
threats: scanResult.threats,
|
|
118
|
-
reason: allowed ? undefined : `Message from "${fromAgent}" blocked: ${scanResult.threats.length} threat(s) detected.`
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
getLog() {
|
|
123
|
-
return [...this.messageLog];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
reset() {
|
|
127
|
-
this.messageLog = [];
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// =========================================================================
|
|
132
|
-
// DELEGATION CHAIN TRACKER
|
|
133
|
-
// =========================================================================
|
|
134
|
-
|
|
135
|
-
class DelegationChain {
|
|
136
|
-
/**
|
|
137
|
-
* Tracks the full chain of who requested what when agents delegate tasks.
|
|
138
|
-
*
|
|
139
|
-
* @param {object} [options]
|
|
140
|
-
* @param {number} [options.maxDepth=10] - Maximum delegation depth.
|
|
141
|
-
* @param {Function} [options.onMaxDepth] - Callback when max depth reached.
|
|
142
|
-
*/
|
|
143
|
-
constructor(options = {}) {
|
|
144
|
-
this.maxDepth = options.maxDepth || 10;
|
|
145
|
-
this.onMaxDepth = options.onMaxDepth || null;
|
|
146
|
-
this.chains = new Map(); // requestId -> chain
|
|
147
|
-
this.activeChains = new Map(); // agentId -> requestId
|
|
148
|
-
this.maxChains = options.maxChains || 1000;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Starts a new delegation chain.
|
|
153
|
-
*
|
|
154
|
-
* @param {string} requestId - Unique request ID.
|
|
155
|
-
* @param {string} originAgent - The agent that received the original request.
|
|
156
|
-
* @param {string} [originalInput] - The original user input.
|
|
157
|
-
* @returns {object} Chain entry.
|
|
158
|
-
*/
|
|
159
|
-
start(requestId, originAgent, originalInput) {
|
|
160
|
-
const chain = {
|
|
161
|
-
requestId,
|
|
162
|
-
originAgent,
|
|
163
|
-
originalInput: originalInput ? originalInput.substring(0, 500) : null,
|
|
164
|
-
steps: [{
|
|
165
|
-
agent: originAgent,
|
|
166
|
-
action: 'received_request',
|
|
167
|
-
timestamp: Date.now(),
|
|
168
|
-
depth: 0
|
|
169
|
-
}],
|
|
170
|
-
status: 'active',
|
|
171
|
-
createdAt: Date.now()
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
// Prune completed chains if over limit
|
|
175
|
-
if (this.chains.size >= this.maxChains) {
|
|
176
|
-
for (const [id, c] of this.chains) {
|
|
177
|
-
if (c.status === 'completed') { this.chains.delete(id); }
|
|
178
|
-
if (this.chains.size < this.maxChains) break;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
this.chains.set(requestId, chain);
|
|
183
|
-
this.activeChains.set(originAgent, requestId);
|
|
184
|
-
return chain;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Records a delegation from one agent to another.
|
|
189
|
-
*
|
|
190
|
-
* @param {string} requestId - The request chain ID.
|
|
191
|
-
* @param {string} fromAgent - Delegating agent.
|
|
192
|
-
* @param {string} toAgent - Receiving agent.
|
|
193
|
-
* @param {string} action - What was delegated (e.g., 'call_tool:bash').
|
|
194
|
-
* @param {string} [permissions] - What permissions the delegatee has.
|
|
195
|
-
* @returns {object} { allowed: boolean, depth: number, chain: object }
|
|
196
|
-
*/
|
|
197
|
-
delegate(requestId, fromAgent, toAgent, action, permissions) {
|
|
198
|
-
const chain = this.chains.get(requestId);
|
|
199
|
-
if (!chain) {
|
|
200
|
-
return { allowed: false, depth: 0, chain: null, reason: 'Unknown request chain.' };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const depth = chain.steps.length;
|
|
204
|
-
|
|
205
|
-
if (depth >= this.maxDepth) {
|
|
206
|
-
if (this.onMaxDepth) {
|
|
207
|
-
try { this.onMaxDepth({ requestId, depth, fromAgent, toAgent }); } catch (e) { console.error('[Agent Shield] onMaxDepth callback error:', e.message); }
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
allowed: false,
|
|
211
|
-
depth,
|
|
212
|
-
chain,
|
|
213
|
-
reason: `Maximum delegation depth (${this.maxDepth}) reached. Possible delegation loop.`
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Check for circular delegation
|
|
218
|
-
const visited = new Set(chain.steps.map(s => s.agent));
|
|
219
|
-
if (visited.has(toAgent)) {
|
|
220
|
-
return {
|
|
221
|
-
allowed: false,
|
|
222
|
-
depth,
|
|
223
|
-
chain,
|
|
224
|
-
reason: `Circular delegation detected: "${toAgent}" is already in the delegation chain.`
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
chain.steps.push({
|
|
229
|
-
agent: fromAgent,
|
|
230
|
-
delegatedTo: toAgent,
|
|
231
|
-
action,
|
|
232
|
-
permissions: permissions || 'inherited',
|
|
233
|
-
timestamp: Date.now(),
|
|
234
|
-
depth
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
this.activeChains.set(toAgent, requestId);
|
|
238
|
-
|
|
239
|
-
return { allowed: true, depth, chain };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Completes a delegation chain.
|
|
244
|
-
* @param {string} requestId
|
|
245
|
-
*/
|
|
246
|
-
complete(requestId) {
|
|
247
|
-
const chain = this.chains.get(requestId);
|
|
248
|
-
if (chain) {
|
|
249
|
-
chain.status = 'completed';
|
|
250
|
-
chain.completedAt = Date.now();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Gets the full chain for a request.
|
|
256
|
-
* @param {string} requestId
|
|
257
|
-
* @returns {object|null}
|
|
258
|
-
*/
|
|
259
|
-
getChain(requestId) {
|
|
260
|
-
return this.chains.get(requestId) || null;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Gets the active chain for an agent.
|
|
265
|
-
* @param {string} agentId
|
|
266
|
-
* @returns {object|null}
|
|
267
|
-
*/
|
|
268
|
-
getActiveChain(agentId) {
|
|
269
|
-
const requestId = this.activeChains.get(agentId);
|
|
270
|
-
return requestId ? this.chains.get(requestId) : null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Returns all chains.
|
|
275
|
-
* @returns {Array}
|
|
276
|
-
*/
|
|
277
|
-
getAllChains() {
|
|
278
|
-
return Array.from(this.chains.values());
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
reset() {
|
|
282
|
-
this.chains.clear();
|
|
283
|
-
this.activeChains.clear();
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// =========================================================================
|
|
288
|
-
// SHARED THREAT STATE
|
|
289
|
-
// =========================================================================
|
|
290
|
-
|
|
291
|
-
class SharedThreatState {
|
|
292
|
-
/**
|
|
293
|
-
* When one agent detects an attack, broadcast the signature to all others.
|
|
294
|
-
*
|
|
295
|
-
* @param {object} [options]
|
|
296
|
-
* @param {number} [options.ttlMs=3600000] - How long threats stay active (default: 1 hour).
|
|
297
|
-
* @param {Function} [options.onBroadcast] - Callback when threat is broadcast.
|
|
298
|
-
*/
|
|
299
|
-
constructor(options = {}) {
|
|
300
|
-
this.ttlMs = options.ttlMs || 3600000;
|
|
301
|
-
this.onBroadcast = options.onBroadcast || null;
|
|
302
|
-
this.threats = new Map(); // signature -> threat data
|
|
303
|
-
this.subscribers = new Map(); // agentId -> callback
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Registers an agent to receive threat broadcasts.
|
|
308
|
-
*
|
|
309
|
-
* @param {string} agentId
|
|
310
|
-
* @param {Function} callback - Called with threat data when broadcast received.
|
|
311
|
-
*/
|
|
312
|
-
subscribe(agentId, callback) {
|
|
313
|
-
this.subscribers.set(agentId, callback);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Unregisters an agent.
|
|
318
|
-
* @param {string} agentId
|
|
319
|
-
*/
|
|
320
|
-
unsubscribe(agentId) {
|
|
321
|
-
this.subscribers.delete(agentId);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
*
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
return
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
*
|
|
391
|
-
* @
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-Agent Protection: Agent-to-Agent Firewall (#39),
|
|
5
|
+
* Delegation Chain Tracking (#40), Consensus Verification (#41),
|
|
6
|
+
* and Shared Threat State (#42)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { scanText } = require('./detector-core');
|
|
10
|
+
|
|
11
|
+
// =========================================================================
|
|
12
|
+
// AGENT-TO-AGENT FIREWALL
|
|
13
|
+
// =========================================================================
|
|
14
|
+
|
|
15
|
+
class AgentFirewall {
|
|
16
|
+
/**
|
|
17
|
+
* Scans all inter-agent messages. Enforces trust boundaries.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} [options]
|
|
20
|
+
* @param {object} [options.trustPolicy={}] - Trust levels between agents.
|
|
21
|
+
* @param {string} [options.defaultTrust='scan'] - Default trust: 'trust', 'scan', or 'block'.
|
|
22
|
+
* @param {Function} [options.onViolation] - Callback on firewall violation.
|
|
23
|
+
*/
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.trustPolicy = options.trustPolicy || {};
|
|
26
|
+
this.defaultTrust = options.defaultTrust || 'scan';
|
|
27
|
+
this.onViolation = options.onViolation || null;
|
|
28
|
+
this.messageLog = [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sets trust level between two agents.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} fromAgent - Sending agent ID.
|
|
35
|
+
* @param {string} toAgent - Receiving agent ID.
|
|
36
|
+
* @param {string} level - Trust level: 'trust', 'scan', or 'block'.
|
|
37
|
+
* @returns {AgentFirewall} this
|
|
38
|
+
*/
|
|
39
|
+
setTrust(fromAgent, toAgent, level) {
|
|
40
|
+
const key = `${fromAgent}->${toAgent}`;
|
|
41
|
+
this.trustPolicy[key] = level;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Checks an inter-agent message through the firewall.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} fromAgent - Sending agent ID.
|
|
49
|
+
* @param {string} toAgent - Receiving agent ID.
|
|
50
|
+
* @param {string} message - The message content.
|
|
51
|
+
* @returns {object} { allowed: boolean, scanned: boolean, threats: Array, reason?: string }
|
|
52
|
+
*/
|
|
53
|
+
check(fromAgent, toAgent, message) {
|
|
54
|
+
const key = `${fromAgent}->${toAgent}`;
|
|
55
|
+
const trustLevel = this.trustPolicy[key] || this.defaultTrust;
|
|
56
|
+
|
|
57
|
+
const logEntry = {
|
|
58
|
+
from: fromAgent,
|
|
59
|
+
to: toAgent,
|
|
60
|
+
trustLevel,
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
messagePreview: message.substring(0, 100)
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Block immediately
|
|
66
|
+
if (trustLevel === 'block') {
|
|
67
|
+
logEntry.result = 'blocked';
|
|
68
|
+
this.messageLog.push(logEntry);
|
|
69
|
+
if (this.messageLog.length > 500) this.messageLog.shift();
|
|
70
|
+
|
|
71
|
+
const result = {
|
|
72
|
+
allowed: false,
|
|
73
|
+
scanned: false,
|
|
74
|
+
threats: [],
|
|
75
|
+
reason: `Messages from "${fromAgent}" to "${toAgent}" are blocked by firewall policy.`
|
|
76
|
+
};
|
|
77
|
+
if (this.onViolation) { try { this.onViolation(result); } catch (e) { console.error('[Agent Shield] onViolation callback error:', e.message); } }
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Trust — pass through without scanning
|
|
82
|
+
if (trustLevel === 'trust') {
|
|
83
|
+
logEntry.result = 'trusted';
|
|
84
|
+
this.messageLog.push(logEntry);
|
|
85
|
+
if (this.messageLog.length > 500) this.messageLog.shift();
|
|
86
|
+
|
|
87
|
+
return { allowed: true, scanned: false, threats: [] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Scan — check for threats
|
|
91
|
+
const scanResult = scanText(message, {
|
|
92
|
+
source: `agent_message:${fromAgent}->${toAgent}`,
|
|
93
|
+
sensitivity: 'high'
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const allowed = scanResult.threats.length === 0;
|
|
97
|
+
logEntry.result = allowed ? 'passed' : 'blocked';
|
|
98
|
+
logEntry.threatCount = scanResult.threats.length;
|
|
99
|
+
this.messageLog.push(logEntry);
|
|
100
|
+
if (this.messageLog.length > 500) this.messageLog.shift();
|
|
101
|
+
|
|
102
|
+
if (!allowed && this.onViolation) {
|
|
103
|
+
try {
|
|
104
|
+
this.onViolation({
|
|
105
|
+
allowed: false,
|
|
106
|
+
from: fromAgent,
|
|
107
|
+
to: toAgent,
|
|
108
|
+
threats: scanResult.threats,
|
|
109
|
+
reason: `Inter-agent message from "${fromAgent}" contains threats.`
|
|
110
|
+
});
|
|
111
|
+
} catch (e) { console.error('[Agent Shield] onViolation callback error:', e.message); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
allowed,
|
|
116
|
+
scanned: true,
|
|
117
|
+
threats: scanResult.threats,
|
|
118
|
+
reason: allowed ? undefined : `Message from "${fromAgent}" blocked: ${scanResult.threats.length} threat(s) detected.`
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getLog() {
|
|
123
|
+
return [...this.messageLog];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
reset() {
|
|
127
|
+
this.messageLog = [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// =========================================================================
|
|
132
|
+
// DELEGATION CHAIN TRACKER
|
|
133
|
+
// =========================================================================
|
|
134
|
+
|
|
135
|
+
class DelegationChain {
|
|
136
|
+
/**
|
|
137
|
+
* Tracks the full chain of who requested what when agents delegate tasks.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} [options]
|
|
140
|
+
* @param {number} [options.maxDepth=10] - Maximum delegation depth.
|
|
141
|
+
* @param {Function} [options.onMaxDepth] - Callback when max depth reached.
|
|
142
|
+
*/
|
|
143
|
+
constructor(options = {}) {
|
|
144
|
+
this.maxDepth = options.maxDepth || 10;
|
|
145
|
+
this.onMaxDepth = options.onMaxDepth || null;
|
|
146
|
+
this.chains = new Map(); // requestId -> chain
|
|
147
|
+
this.activeChains = new Map(); // agentId -> requestId
|
|
148
|
+
this.maxChains = options.maxChains || 1000;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Starts a new delegation chain.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} requestId - Unique request ID.
|
|
155
|
+
* @param {string} originAgent - The agent that received the original request.
|
|
156
|
+
* @param {string} [originalInput] - The original user input.
|
|
157
|
+
* @returns {object} Chain entry.
|
|
158
|
+
*/
|
|
159
|
+
start(requestId, originAgent, originalInput) {
|
|
160
|
+
const chain = {
|
|
161
|
+
requestId,
|
|
162
|
+
originAgent,
|
|
163
|
+
originalInput: originalInput ? originalInput.substring(0, 500) : null,
|
|
164
|
+
steps: [{
|
|
165
|
+
agent: originAgent,
|
|
166
|
+
action: 'received_request',
|
|
167
|
+
timestamp: Date.now(),
|
|
168
|
+
depth: 0
|
|
169
|
+
}],
|
|
170
|
+
status: 'active',
|
|
171
|
+
createdAt: Date.now()
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Prune completed chains if over limit
|
|
175
|
+
if (this.chains.size >= this.maxChains) {
|
|
176
|
+
for (const [id, c] of this.chains) {
|
|
177
|
+
if (c.status === 'completed') { this.chains.delete(id); }
|
|
178
|
+
if (this.chains.size < this.maxChains) break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.chains.set(requestId, chain);
|
|
183
|
+
this.activeChains.set(originAgent, requestId);
|
|
184
|
+
return chain;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Records a delegation from one agent to another.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} requestId - The request chain ID.
|
|
191
|
+
* @param {string} fromAgent - Delegating agent.
|
|
192
|
+
* @param {string} toAgent - Receiving agent.
|
|
193
|
+
* @param {string} action - What was delegated (e.g., 'call_tool:bash').
|
|
194
|
+
* @param {string} [permissions] - What permissions the delegatee has.
|
|
195
|
+
* @returns {object} { allowed: boolean, depth: number, chain: object }
|
|
196
|
+
*/
|
|
197
|
+
delegate(requestId, fromAgent, toAgent, action, permissions) {
|
|
198
|
+
const chain = this.chains.get(requestId);
|
|
199
|
+
if (!chain) {
|
|
200
|
+
return { allowed: false, depth: 0, chain: null, reason: 'Unknown request chain.' };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const depth = chain.steps.length;
|
|
204
|
+
|
|
205
|
+
if (depth >= this.maxDepth) {
|
|
206
|
+
if (this.onMaxDepth) {
|
|
207
|
+
try { this.onMaxDepth({ requestId, depth, fromAgent, toAgent }); } catch (e) { console.error('[Agent Shield] onMaxDepth callback error:', e.message); }
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
allowed: false,
|
|
211
|
+
depth,
|
|
212
|
+
chain,
|
|
213
|
+
reason: `Maximum delegation depth (${this.maxDepth}) reached. Possible delegation loop.`
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check for circular delegation
|
|
218
|
+
const visited = new Set(chain.steps.map(s => s.agent));
|
|
219
|
+
if (visited.has(toAgent)) {
|
|
220
|
+
return {
|
|
221
|
+
allowed: false,
|
|
222
|
+
depth,
|
|
223
|
+
chain,
|
|
224
|
+
reason: `Circular delegation detected: "${toAgent}" is already in the delegation chain.`
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
chain.steps.push({
|
|
229
|
+
agent: fromAgent,
|
|
230
|
+
delegatedTo: toAgent,
|
|
231
|
+
action,
|
|
232
|
+
permissions: permissions || 'inherited',
|
|
233
|
+
timestamp: Date.now(),
|
|
234
|
+
depth
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
this.activeChains.set(toAgent, requestId);
|
|
238
|
+
|
|
239
|
+
return { allowed: true, depth, chain };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Completes a delegation chain.
|
|
244
|
+
* @param {string} requestId
|
|
245
|
+
*/
|
|
246
|
+
complete(requestId) {
|
|
247
|
+
const chain = this.chains.get(requestId);
|
|
248
|
+
if (chain) {
|
|
249
|
+
chain.status = 'completed';
|
|
250
|
+
chain.completedAt = Date.now();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Gets the full chain for a request.
|
|
256
|
+
* @param {string} requestId
|
|
257
|
+
* @returns {object|null}
|
|
258
|
+
*/
|
|
259
|
+
getChain(requestId) {
|
|
260
|
+
return this.chains.get(requestId) || null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Gets the active chain for an agent.
|
|
265
|
+
* @param {string} agentId
|
|
266
|
+
* @returns {object|null}
|
|
267
|
+
*/
|
|
268
|
+
getActiveChain(agentId) {
|
|
269
|
+
const requestId = this.activeChains.get(agentId);
|
|
270
|
+
return requestId ? this.chains.get(requestId) : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Returns all chains.
|
|
275
|
+
* @returns {Array}
|
|
276
|
+
*/
|
|
277
|
+
getAllChains() {
|
|
278
|
+
return Array.from(this.chains.values());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
reset() {
|
|
282
|
+
this.chains.clear();
|
|
283
|
+
this.activeChains.clear();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// =========================================================================
|
|
288
|
+
// SHARED THREAT STATE
|
|
289
|
+
// =========================================================================
|
|
290
|
+
|
|
291
|
+
class SharedThreatState {
|
|
292
|
+
/**
|
|
293
|
+
* When one agent detects an attack, broadcast the signature to all others.
|
|
294
|
+
*
|
|
295
|
+
* @param {object} [options]
|
|
296
|
+
* @param {number} [options.ttlMs=3600000] - How long threats stay active (default: 1 hour).
|
|
297
|
+
* @param {Function} [options.onBroadcast] - Callback when threat is broadcast.
|
|
298
|
+
*/
|
|
299
|
+
constructor(options = {}) {
|
|
300
|
+
this.ttlMs = options.ttlMs || 3600000;
|
|
301
|
+
this.onBroadcast = options.onBroadcast || null;
|
|
302
|
+
this.threats = new Map(); // signature -> threat data
|
|
303
|
+
this.subscribers = new Map(); // agentId -> callback
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Registers an agent to receive threat broadcasts.
|
|
308
|
+
*
|
|
309
|
+
* @param {string} agentId
|
|
310
|
+
* @param {Function} callback - Called with threat data when broadcast received.
|
|
311
|
+
*/
|
|
312
|
+
subscribe(agentId, callback) {
|
|
313
|
+
this.subscribers.set(agentId, callback);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Unregisters an agent.
|
|
318
|
+
* @param {string} agentId
|
|
319
|
+
*/
|
|
320
|
+
unsubscribe(agentId) {
|
|
321
|
+
this.subscribers.delete(agentId);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Prunes subscribers whose callbacks throw or are no longer reachable.
|
|
326
|
+
* Call periodically or after detecting stale agents.
|
|
327
|
+
*/
|
|
328
|
+
pruneStaleSubscribers() {
|
|
329
|
+
const stale = [];
|
|
330
|
+
for (const [agentId, callback] of this.subscribers) {
|
|
331
|
+
if (typeof callback !== 'function') {
|
|
332
|
+
stale.push(agentId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const agentId of stale) {
|
|
336
|
+
this.subscribers.delete(agentId);
|
|
337
|
+
}
|
|
338
|
+
return stale.length;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Broadcasts a threat to all subscribed agents.
|
|
343
|
+
*
|
|
344
|
+
* @param {string} reportingAgent - Agent that detected the threat.
|
|
345
|
+
* @param {object} threat - Threat data.
|
|
346
|
+
* @param {string} threat.signature - Unique signature (e.g., hash of the attack text).
|
|
347
|
+
* @param {string} threat.category - Threat category.
|
|
348
|
+
* @param {string} threat.severity - Threat severity.
|
|
349
|
+
* @param {string} [threat.description] - Description.
|
|
350
|
+
*/
|
|
351
|
+
broadcast(reportingAgent, threat) {
|
|
352
|
+
const entry = {
|
|
353
|
+
...threat,
|
|
354
|
+
reportedBy: reportingAgent,
|
|
355
|
+
reportedAt: Date.now(),
|
|
356
|
+
expiresAt: Date.now() + this.ttlMs
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
this.threats.set(threat.signature, entry);
|
|
360
|
+
|
|
361
|
+
// Notify all subscribers except the reporter
|
|
362
|
+
for (const [agentId, callback] of this.subscribers) {
|
|
363
|
+
if (agentId !== reportingAgent) {
|
|
364
|
+
try { callback(entry); } catch (e) { console.error(`[Agent Shield] subscriber ${agentId} callback error:`, e.message); }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (this.onBroadcast) {
|
|
369
|
+
try { this.onBroadcast(entry); } catch (e) { console.error('[Agent Shield] onBroadcast callback error:', e.message); }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Checks if a threat signature is already known.
|
|
375
|
+
*
|
|
376
|
+
* @param {string} signature
|
|
377
|
+
* @returns {object|null} Known threat data, or null.
|
|
378
|
+
*/
|
|
379
|
+
isKnown(signature) {
|
|
380
|
+
const entry = this.threats.get(signature);
|
|
381
|
+
if (!entry) return null;
|
|
382
|
+
if (Date.now() > entry.expiresAt) {
|
|
383
|
+
this.threats.delete(signature);
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
return entry;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Returns all active threats.
|
|
391
|
+
* @returns {Array}
|
|
392
|
+
*/
|
|
393
|
+
getActiveThreats() {
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
const active = [];
|
|
396
|
+
for (const [sig, entry] of this.threats) {
|
|
397
|
+
if (now <= entry.expiresAt) {
|
|
398
|
+
active.push(entry);
|
|
399
|
+
} else {
|
|
400
|
+
this.threats.delete(sig);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return active;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Generates a simple signature from text.
|
|
408
|
+
* @param {string} text
|
|
409
|
+
* @returns {string}
|
|
410
|
+
*/
|
|
411
|
+
static generateSignature(text) {
|
|
412
|
+
const crypto = require('crypto');
|
|
413
|
+
return crypto.createHash('sha256').update(text.substring(0, 500)).digest('hex').substring(0, 16);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
reset() {
|
|
417
|
+
this.threats.clear();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
module.exports = { AgentFirewall, DelegationChain, SharedThreatState };
|