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,528 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Framework Integrations
|
|
5
|
+
*
|
|
6
|
+
* Plug-and-play integrations for popular AI frameworks:
|
|
7
|
+
* - LangChain
|
|
8
|
+
* - Anthropic SDK (Claude)
|
|
9
|
+
* - OpenAI SDK
|
|
10
|
+
* - Vercel AI SDK
|
|
11
|
+
* - Generic fetch wrapper
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { AgentShield } = require('./index');
|
|
15
|
+
const { PIIRedactor } = require('./pii');
|
|
16
|
+
const { CircuitBreaker } = require('./circuit-breaker');
|
|
17
|
+
|
|
18
|
+
// =========================================================================
|
|
19
|
+
// LangChain Integration
|
|
20
|
+
// =========================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* LangChain callback handler that scans inputs/outputs for threats.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* const { ShieldCallbackHandler } = require('agent-shield/src/integrations');
|
|
27
|
+
* const handler = new ShieldCallbackHandler({ blockOnThreat: true });
|
|
28
|
+
* const chain = new LLMChain({ llm, prompt, callbacks: [handler] });
|
|
29
|
+
*/
|
|
30
|
+
class ShieldCallbackHandler {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.shield = new AgentShield({
|
|
33
|
+
sensitivity: options.sensitivity || 'high',
|
|
34
|
+
blockOnThreat: options.blockOnThreat || false
|
|
35
|
+
});
|
|
36
|
+
this.piiRedactor = options.pii ? new PIIRedactor() : null;
|
|
37
|
+
this.onThreat = options.onThreat || null;
|
|
38
|
+
this.name = 'AgentShieldCallback';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async handleLLMStart(_llm, prompts) {
|
|
42
|
+
for (const prompt of prompts) {
|
|
43
|
+
const result = this.shield.scanInput(prompt);
|
|
44
|
+
if (result.threats.length > 0 && this.onThreat) {
|
|
45
|
+
try { this.onThreat({ phase: 'input', threats: result.threats, text: prompt }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
46
|
+
}
|
|
47
|
+
if (result.blocked) {
|
|
48
|
+
throw new ShieldBlockError('Input blocked by Agent Shield', result.threats);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async handleLLMEnd(output) {
|
|
54
|
+
const text = output?.generations?.[0]?.[0]?.text || '';
|
|
55
|
+
if (!text) return;
|
|
56
|
+
|
|
57
|
+
const result = this.shield.scanOutput(text);
|
|
58
|
+
if (result.threats.length > 0 && this.onThreat) {
|
|
59
|
+
try { this.onThreat({ phase: 'output', threats: result.threats, text }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
60
|
+
}
|
|
61
|
+
if (result.blocked) {
|
|
62
|
+
throw new ShieldBlockError('Output blocked by Agent Shield', result.threats);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async handleChainStart(_chain, inputs) {
|
|
67
|
+
const text = typeof inputs === 'string' ? inputs : JSON.stringify(inputs);
|
|
68
|
+
const result = this.shield.scanInput(text);
|
|
69
|
+
if (result.blocked) {
|
|
70
|
+
throw new ShieldBlockError('Chain input blocked by Agent Shield', result.threats);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async handleToolStart(_tool, input) {
|
|
75
|
+
const result = this.shield.scanInput(input);
|
|
76
|
+
if (result.threats.length > 0 && this.onThreat) {
|
|
77
|
+
try { this.onThreat({ phase: 'tool_input', threats: result.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
78
|
+
}
|
|
79
|
+
if (result.blocked) {
|
|
80
|
+
throw new ShieldBlockError('Tool input blocked by Agent Shield', result.threats);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async handleToolEnd(output) {
|
|
85
|
+
const text = typeof output === 'string' ? output : JSON.stringify(output);
|
|
86
|
+
if (!text) return;
|
|
87
|
+
|
|
88
|
+
const result = this.shield.scanInput(text);
|
|
89
|
+
if (result.threats.length > 0 && this.onThreat) {
|
|
90
|
+
try { this.onThreat({ phase: 'tool_output', threats: result.threats, text }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
91
|
+
}
|
|
92
|
+
if (result.blocked) {
|
|
93
|
+
throw new ShieldBlockError('Tool output blocked by Agent Shield — possible injection via tool response', result.threats);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async handleChainEnd(outputs) {
|
|
98
|
+
const text = typeof outputs === 'string' ? outputs : JSON.stringify(outputs);
|
|
99
|
+
if (!text) return;
|
|
100
|
+
|
|
101
|
+
const result = this.shield.scanOutput(text);
|
|
102
|
+
if (result.threats.length > 0 && this.onThreat) {
|
|
103
|
+
try { this.onThreat({ phase: 'chain_output', threats: result.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
104
|
+
}
|
|
105
|
+
if (result.blocked) {
|
|
106
|
+
throw new ShieldBlockError('Chain output blocked by Agent Shield', result.threats);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async handleAgentAction(action) {
|
|
111
|
+
if (action && action.toolInput) {
|
|
112
|
+
const text = typeof action.toolInput === 'string' ? action.toolInput : JSON.stringify(action.toolInput);
|
|
113
|
+
const result = this.shield.scanInput(text);
|
|
114
|
+
if (result.threats.length > 0 && this.onThreat) {
|
|
115
|
+
try { this.onThreat({ phase: 'agent_action', tool: action.tool, threats: result.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
116
|
+
}
|
|
117
|
+
if (result.blocked) {
|
|
118
|
+
throw new ShieldBlockError(`Agent action "${action.tool}" blocked by Agent Shield`, result.threats);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getStats() {
|
|
124
|
+
return this.shield.getStats();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =========================================================================
|
|
129
|
+
// Anthropic SDK Integration
|
|
130
|
+
// =========================================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Wraps an Anthropic client with threat scanning on messages.
|
|
134
|
+
*
|
|
135
|
+
* Usage:
|
|
136
|
+
* const Anthropic = require('@anthropic-ai/sdk');
|
|
137
|
+
* const { shieldAnthropicClient } = require('agent-shield/src/integrations');
|
|
138
|
+
* const client = shieldAnthropicClient(new Anthropic(), { blockOnThreat: true });
|
|
139
|
+
* const msg = await client.messages.create({ model: 'claude-sonnet-4-20250514', messages: [...] });
|
|
140
|
+
*/
|
|
141
|
+
function shieldAnthropicClient(client, options = {}) {
|
|
142
|
+
const shield = new AgentShield({
|
|
143
|
+
sensitivity: options.sensitivity || 'high',
|
|
144
|
+
blockOnThreat: options.blockOnThreat || false
|
|
145
|
+
});
|
|
146
|
+
const piiRedactor = options.pii ? new PIIRedactor() : null;
|
|
147
|
+
const breaker = options.circuitBreaker ? new CircuitBreaker(options.circuitBreaker) : null;
|
|
148
|
+
const onThreat = options.onThreat || null;
|
|
149
|
+
|
|
150
|
+
const originalCreate = client.messages.create.bind(client.messages);
|
|
151
|
+
|
|
152
|
+
client.messages.create = async function shieldedCreate(params) {
|
|
153
|
+
// Circuit breaker check
|
|
154
|
+
if (breaker) {
|
|
155
|
+
const status = breaker.check();
|
|
156
|
+
if (!status.allowed) {
|
|
157
|
+
throw new ShieldBlockError(`Circuit breaker open: ${status.reason}`, []);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Scan all message content
|
|
162
|
+
for (const msg of params.messages || []) {
|
|
163
|
+
const text = extractAnthropicText(msg);
|
|
164
|
+
if (!text) continue;
|
|
165
|
+
|
|
166
|
+
// PII redaction
|
|
167
|
+
if (piiRedactor && msg.role === 'user') {
|
|
168
|
+
const piiResult = piiRedactor.redact(text);
|
|
169
|
+
if (piiResult.count > 0) {
|
|
170
|
+
setAnthropicText(msg, piiResult.redacted);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result = shield.scanInput(text);
|
|
175
|
+
if (result.threats.length > 0) {
|
|
176
|
+
if (onThreat) { try { onThreat({ phase: 'input', role: msg.role, threats: result.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); } }
|
|
177
|
+
if (breaker) breaker.recordThreat(result.threats.length);
|
|
178
|
+
if (result.blocked) {
|
|
179
|
+
throw new ShieldBlockError('Message blocked by Agent Shield', result.threats);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Scan system prompt
|
|
185
|
+
if (params.system) {
|
|
186
|
+
const sysText = typeof params.system === 'string' ? params.system : params.system.map(b => b.text || '').join(' ');
|
|
187
|
+
const sysResult = shield.scanInput(sysText);
|
|
188
|
+
if (sysResult.blocked) {
|
|
189
|
+
throw new ShieldBlockError('System prompt contains threats', sysResult.threats);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Make the actual API call
|
|
194
|
+
const response = await originalCreate(params);
|
|
195
|
+
|
|
196
|
+
// Scan output text
|
|
197
|
+
const outputText = response.content?.map(b => b.text || '').join(' ') || '';
|
|
198
|
+
if (outputText) {
|
|
199
|
+
const outputResult = shield.scanOutput(outputText);
|
|
200
|
+
if (outputResult.threats.length > 0 && onThreat) {
|
|
201
|
+
try { onThreat({ phase: 'output', threats: outputResult.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
202
|
+
}
|
|
203
|
+
if (outputResult.blocked) {
|
|
204
|
+
throw new ShieldBlockError('Response blocked by Agent Shield', outputResult.threats);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Scan tool_use blocks in response — tool calls can contain injection payloads
|
|
209
|
+
if (response.content && Array.isArray(response.content)) {
|
|
210
|
+
for (const block of response.content) {
|
|
211
|
+
if (block.type === 'tool_use' && block.input) {
|
|
212
|
+
const toolText = typeof block.input === 'string' ? block.input : JSON.stringify(block.input);
|
|
213
|
+
const toolResult = shield.scanInput(toolText);
|
|
214
|
+
if (toolResult.threats.length > 0) {
|
|
215
|
+
if (onThreat) { try { onThreat({ phase: 'tool_call', tool: block.name, threats: toolResult.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); } }
|
|
216
|
+
if (breaker) breaker.recordThreat(toolResult.threats.length);
|
|
217
|
+
if (toolResult.blocked) {
|
|
218
|
+
throw new ShieldBlockError(`Tool call "${block.name}" blocked by Agent Shield`, toolResult.threats);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Scan tool_result messages in input — tool responses can inject threats
|
|
226
|
+
for (const msg of params.messages || []) {
|
|
227
|
+
if (msg.role === 'tool' || (Array.isArray(msg.content) && msg.content.some(b => b.type === 'tool_result'))) {
|
|
228
|
+
const toolRespText = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
229
|
+
const toolRespResult = shield.scanInput(toolRespText);
|
|
230
|
+
if (toolRespResult.threats.length > 0) {
|
|
231
|
+
if (onThreat) { try { onThreat({ phase: 'tool_response', threats: toolRespResult.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); } }
|
|
232
|
+
if (breaker) breaker.recordThreat(toolRespResult.threats.length);
|
|
233
|
+
if (toolRespResult.blocked) {
|
|
234
|
+
throw new ShieldBlockError('Tool response blocked by Agent Shield', toolRespResult.threats);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Attach scan metadata
|
|
241
|
+
response._shield = {
|
|
242
|
+
scanned: true,
|
|
243
|
+
toolCallsScanned: (response.content || []).filter(b => b.type === 'tool_use').length,
|
|
244
|
+
stats: shield.getStats()
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return response;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
client._shield = shield;
|
|
251
|
+
return client;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function extractAnthropicText(msg) {
|
|
255
|
+
if (typeof msg.content === 'string') return msg.content;
|
|
256
|
+
if (Array.isArray(msg.content)) {
|
|
257
|
+
return msg.content.map(b => b.text || '').join(' ');
|
|
258
|
+
}
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function setAnthropicText(msg, text) {
|
|
263
|
+
if (typeof msg.content === 'string') {
|
|
264
|
+
msg.content = text;
|
|
265
|
+
} else if (Array.isArray(msg.content)) {
|
|
266
|
+
// Replace text across text blocks proportionally to preserve multi-block structure.
|
|
267
|
+
// Non-text blocks (images, tool_use, etc.) are left untouched.
|
|
268
|
+
const textBlocks = msg.content.filter(b => b.type === 'text' || b.text);
|
|
269
|
+
if (textBlocks.length === 1) {
|
|
270
|
+
textBlocks[0].text = text;
|
|
271
|
+
} else if (textBlocks.length > 1) {
|
|
272
|
+
// Split redacted text across blocks proportionally to original lengths
|
|
273
|
+
const totalLen = textBlocks.reduce((sum, b) => sum + (b.text || '').length, 0);
|
|
274
|
+
if (totalLen === 0) {
|
|
275
|
+
textBlocks[0].text = text;
|
|
276
|
+
} else {
|
|
277
|
+
let offset = 0;
|
|
278
|
+
for (let i = 0; i < textBlocks.length; i++) {
|
|
279
|
+
const ratio = (textBlocks[i].text || '').length / totalLen;
|
|
280
|
+
const chunkLen = i === textBlocks.length - 1
|
|
281
|
+
? text.length - offset
|
|
282
|
+
: Math.round(text.length * ratio);
|
|
283
|
+
textBlocks[i].text = text.slice(offset, offset + chunkLen);
|
|
284
|
+
offset += chunkLen;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// =========================================================================
|
|
292
|
+
// OpenAI SDK Integration
|
|
293
|
+
// =========================================================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Wraps an OpenAI client with threat scanning.
|
|
297
|
+
*
|
|
298
|
+
* Usage:
|
|
299
|
+
* const OpenAI = require('openai');
|
|
300
|
+
* const { shieldOpenAIClient } = require('agent-shield/src/integrations');
|
|
301
|
+
* const client = shieldOpenAIClient(new OpenAI(), { blockOnThreat: true });
|
|
302
|
+
* const completion = await client.chat.completions.create({ model: 'gpt-4', messages: [...] });
|
|
303
|
+
*/
|
|
304
|
+
function shieldOpenAIClient(client, options = {}) {
|
|
305
|
+
const shield = new AgentShield({
|
|
306
|
+
sensitivity: options.sensitivity || 'high',
|
|
307
|
+
blockOnThreat: options.blockOnThreat || false
|
|
308
|
+
});
|
|
309
|
+
const piiRedactor = options.pii ? new PIIRedactor() : null;
|
|
310
|
+
const breaker = options.circuitBreaker ? new CircuitBreaker(options.circuitBreaker) : null;
|
|
311
|
+
const onThreat = options.onThreat || null;
|
|
312
|
+
|
|
313
|
+
const originalCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
314
|
+
|
|
315
|
+
client.chat.completions.create = async function shieldedCreate(params) {
|
|
316
|
+
// Circuit breaker check
|
|
317
|
+
if (breaker) {
|
|
318
|
+
const status = breaker.check();
|
|
319
|
+
if (!status.allowed) {
|
|
320
|
+
throw new ShieldBlockError(`Circuit breaker open: ${status.reason}`, []);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Scan all messages
|
|
325
|
+
for (const msg of params.messages || []) {
|
|
326
|
+
const text = typeof msg.content === 'string' ? msg.content : (msg.content || []).map(p => p.text || '').join(' ');
|
|
327
|
+
if (!text) continue;
|
|
328
|
+
|
|
329
|
+
// PII redaction
|
|
330
|
+
if (piiRedactor && msg.role === 'user') {
|
|
331
|
+
const piiResult = piiRedactor.redact(text);
|
|
332
|
+
if (piiResult.count > 0) {
|
|
333
|
+
msg.content = piiResult.redacted;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const result = shield.scanInput(text);
|
|
338
|
+
if (result.threats.length > 0) {
|
|
339
|
+
if (onThreat) { try { onThreat({ phase: 'input', role: msg.role, threats: result.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); } }
|
|
340
|
+
if (breaker) breaker.recordThreat(result.threats.length);
|
|
341
|
+
if (result.blocked) {
|
|
342
|
+
throw new ShieldBlockError('Message blocked by Agent Shield', result.threats);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Scan system message if present
|
|
348
|
+
const systemMsg = (params.messages || []).find(m => m.role === 'system');
|
|
349
|
+
if (systemMsg && typeof systemMsg.content === 'string') {
|
|
350
|
+
const sysResult = shield.scanInput(systemMsg.content);
|
|
351
|
+
if (sysResult.blocked) {
|
|
352
|
+
throw new ShieldBlockError('System message contains threats', sysResult.threats);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Scan tool/function response messages in input
|
|
357
|
+
for (const msg of params.messages || []) {
|
|
358
|
+
if (msg.role === 'tool' || msg.role === 'function') {
|
|
359
|
+
const toolRespText = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
360
|
+
const toolRespResult = shield.scanInput(toolRespText);
|
|
361
|
+
if (toolRespResult.threats.length > 0) {
|
|
362
|
+
if (onThreat) { try { onThreat({ phase: 'tool_response', tool_call_id: msg.tool_call_id, threats: toolRespResult.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); } }
|
|
363
|
+
if (breaker) breaker.recordThreat(toolRespResult.threats.length);
|
|
364
|
+
if (toolRespResult.blocked) {
|
|
365
|
+
throw new ShieldBlockError('Tool response blocked by Agent Shield', toolRespResult.threats);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const response = await originalCreate(params);
|
|
372
|
+
|
|
373
|
+
// Scan all output choices, not just the first
|
|
374
|
+
for (const choice of response.choices || []) {
|
|
375
|
+
const outputText = choice?.message?.content || '';
|
|
376
|
+
if (outputText) {
|
|
377
|
+
const outputResult = shield.scanOutput(outputText);
|
|
378
|
+
if (outputResult.threats.length > 0 && onThreat) {
|
|
379
|
+
try { onThreat({ phase: 'output', choiceIndex: choice.index, threats: outputResult.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
380
|
+
}
|
|
381
|
+
if (outputResult.blocked) {
|
|
382
|
+
throw new ShieldBlockError('Response blocked by Agent Shield', outputResult.threats);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Scan tool calls in response
|
|
387
|
+
const toolCalls = choice?.message?.tool_calls || choice?.message?.function_call ? [choice.message.function_call] : [];
|
|
388
|
+
for (const tc of (choice?.message?.tool_calls || [])) {
|
|
389
|
+
if (tc.function && tc.function.arguments) {
|
|
390
|
+
const argsResult = shield.scanInput(tc.function.arguments);
|
|
391
|
+
if (argsResult.threats.length > 0) {
|
|
392
|
+
if (onThreat) { try { onThreat({ phase: 'tool_call', tool: tc.function.name, threats: argsResult.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); } }
|
|
393
|
+
if (argsResult.blocked) {
|
|
394
|
+
throw new ShieldBlockError(`Tool call "${tc.function.name}" blocked by Agent Shield`, argsResult.threats);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
response._shield = {
|
|
402
|
+
scanned: true,
|
|
403
|
+
choicesScanned: (response.choices || []).length,
|
|
404
|
+
stats: shield.getStats()
|
|
405
|
+
};
|
|
406
|
+
return response;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
client._shield = shield;
|
|
410
|
+
return client;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// =========================================================================
|
|
414
|
+
// Vercel AI SDK Integration
|
|
415
|
+
// =========================================================================
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Middleware for Vercel AI SDK's streamText/generateText.
|
|
419
|
+
*
|
|
420
|
+
* Usage:
|
|
421
|
+
* const { shieldVercelAI } = require('agent-shield/src/integrations');
|
|
422
|
+
* const middleware = shieldVercelAI({ blockOnThreat: true });
|
|
423
|
+
* const result = await generateText({ model, prompt, middleware });
|
|
424
|
+
*/
|
|
425
|
+
function shieldVercelAI(options = {}) {
|
|
426
|
+
const shield = new AgentShield({
|
|
427
|
+
sensitivity: options.sensitivity || 'high',
|
|
428
|
+
blockOnThreat: options.blockOnThreat || false
|
|
429
|
+
});
|
|
430
|
+
const onThreat = options.onThreat || null;
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
transformParams: async ({ params }) => {
|
|
434
|
+
// Scan all messages
|
|
435
|
+
for (const msg of params.messages || []) {
|
|
436
|
+
const text = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
437
|
+
const result = shield.scanInput(text);
|
|
438
|
+
if (result.threats.length > 0 && onThreat) {
|
|
439
|
+
try { onThreat({ phase: 'input', threats: result.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
440
|
+
}
|
|
441
|
+
if (result.blocked) {
|
|
442
|
+
throw new ShieldBlockError('Blocked by Agent Shield', result.threats);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return params;
|
|
446
|
+
},
|
|
447
|
+
wrapGenerate: async ({ doGenerate }) => {
|
|
448
|
+
const result = await doGenerate();
|
|
449
|
+
const text = result.text || '';
|
|
450
|
+
if (text) {
|
|
451
|
+
const scanResult = shield.scanOutput(text);
|
|
452
|
+
if (scanResult.threats.length > 0 && onThreat) {
|
|
453
|
+
try { onThreat({ phase: 'output', threats: scanResult.threats }); } catch (e) { console.error('[Agent Shield] onThreat callback error:', e.message); }
|
|
454
|
+
}
|
|
455
|
+
if (scanResult.blocked) {
|
|
456
|
+
throw new ShieldBlockError('Response blocked by Agent Shield', scanResult.threats);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// =========================================================================
|
|
465
|
+
// Generic Fetch Wrapper
|
|
466
|
+
// =========================================================================
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Wraps fetch to scan request/response bodies for threats.
|
|
470
|
+
*
|
|
471
|
+
* Usage:
|
|
472
|
+
* const { shieldFetch } = require('agent-shield/src/integrations');
|
|
473
|
+
* const safeFetch = shieldFetch(fetch, { blockOnThreat: true });
|
|
474
|
+
* const response = await safeFetch('https://api.openai.com/...', { body: JSON.stringify(data) });
|
|
475
|
+
*/
|
|
476
|
+
function shieldFetch(fetchFn, options = {}) {
|
|
477
|
+
const shield = new AgentShield({
|
|
478
|
+
sensitivity: options.sensitivity || 'high',
|
|
479
|
+
blockOnThreat: options.blockOnThreat || false
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return async function shieldedFetch(url, init = {}) {
|
|
483
|
+
// Scan request body
|
|
484
|
+
if (init.body) {
|
|
485
|
+
const bodyText = typeof init.body === 'string' ? init.body : JSON.stringify(init.body);
|
|
486
|
+
const result = shield.scanInput(bodyText);
|
|
487
|
+
if (result.blocked) {
|
|
488
|
+
throw new ShieldBlockError('Request body blocked by Agent Shield', result.threats);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const response = await fetchFn(url, init);
|
|
493
|
+
return response;
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// =========================================================================
|
|
498
|
+
// Shared Error Class
|
|
499
|
+
// =========================================================================
|
|
500
|
+
|
|
501
|
+
class ShieldBlockError extends Error {
|
|
502
|
+
constructor(message, threats = []) {
|
|
503
|
+
super(message);
|
|
504
|
+
this.name = 'ShieldBlockError';
|
|
505
|
+
this.threats = threats;
|
|
506
|
+
this.code = 'AGENT_SHIELD_BLOCKED';
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = {
|
|
511
|
+
// LangChain
|
|
512
|
+
ShieldCallbackHandler,
|
|
513
|
+
|
|
514
|
+
// Anthropic
|
|
515
|
+
shieldAnthropicClient,
|
|
516
|
+
|
|
517
|
+
// OpenAI
|
|
518
|
+
shieldOpenAIClient,
|
|
519
|
+
|
|
520
|
+
// Vercel AI
|
|
521
|
+
shieldVercelAI,
|
|
522
|
+
|
|
523
|
+
// Generic
|
|
524
|
+
shieldFetch,
|
|
525
|
+
|
|
526
|
+
// Error
|
|
527
|
+
ShieldBlockError
|
|
528
|
+
};
|