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/src/middleware.js CHANGED
@@ -1,208 +1,303 @@
1
- 'use strict';
2
-
3
- /**
4
- * Agent Shield Middleware
5
- *
6
- * Plug-and-play middleware for common agent frameworks.
7
- * Wraps agent input/output pipelines with automatic threat scanning.
8
- */
9
-
10
- const { AgentShield } = require('./index');
11
-
12
- /**
13
- * Creates an Express/Connect-style middleware that scans request bodies
14
- * for AI-specific threats before they reach your agent endpoint.
15
- *
16
- * @param {object} [config] - AgentShield configuration.
17
- * @returns {Function} Express middleware function.
18
- *
19
- * @example
20
- * const express = require('express');
21
- * const { expressMiddleware } = require('agent-shield/src/middleware');
22
- *
23
- * const app = express();
24
- * app.use(express.json());
25
- * app.use(expressMiddleware({ blockOnThreat: true, blockThreshold: 'high' }));
26
- *
27
- * app.post('/agent', (req, res) => {
28
- * // req.agentShield contains scan results
29
- * if (req.agentShield.blocked) {
30
- * return res.status(400).json({ error: 'Input blocked for safety' });
31
- * }
32
- * // ... process the agent request
33
- * });
34
- */
35
- const expressMiddleware = (config = {}) => {
36
- const shield = new AgentShield({ blockOnThreat: true, ...config });
37
-
38
- return (req, res, next) => {
39
- if (!req.body) {
40
- req.agentShield = { status: 'safe', threats: [], blocked: false };
41
- return next();
42
- }
43
-
44
- // Extract text from common request body shapes
45
- const text = extractTextFromBody(req.body);
46
-
47
- if (!text) {
48
- req.agentShield = { status: 'safe', threats: [], blocked: false };
49
- return next();
50
- }
51
-
52
- const result = shield.scanInput(text, { source: 'http_request' });
53
- req.agentShield = result;
54
-
55
- if (result.blocked) {
56
- return res.status(400).json({
57
- error: 'Input blocked by Agent Shield',
58
- status: result.status,
59
- threats: result.threats.map(t => ({
60
- severity: t.severity,
61
- description: t.description
62
- }))
63
- });
64
- }
65
-
66
- next();
67
- };
68
- };
69
-
70
- /**
71
- * Creates a wrapper function that scans input/output around any async function.
72
- * Works with any agent framework just wrap your agent's main function.
73
- *
74
- * @param {Function} agentFn - The agent function to wrap. Should accept (input) and return output.
75
- * @param {object} [config] - AgentShield configuration.
76
- * @returns {Function} Wrapped function with the same signature.
77
- *
78
- * @example
79
- * const { wrapAgent } = require('agent-shield/src/middleware');
80
- *
81
- * async function myAgent(input) {
82
- * const response = await callLLM(input);
83
- * return response;
84
- * }
85
- *
86
- * const protectedAgent = wrapAgent(myAgent, {
87
- * blockOnThreat: true,
88
- * logging: true
89
- * });
90
- *
91
- * // Use it the same way
92
- * const result = await protectedAgent('Hello, how are you?');
93
- */
94
- const wrapAgent = (agentFn, config = {}) => {
95
- const shield = new AgentShield({ blockOnThreat: true, ...config });
96
-
97
- return async (input, ...rest) => {
98
- // Scan input
99
- const inputText = typeof input === 'string' ? input : JSON.stringify(input);
100
- const inputResult = shield.scanInput(inputText, { source: 'agent_input' });
101
-
102
- if (inputResult.blocked) {
103
- return {
104
- blocked: true,
105
- reason: 'Input blocked by Agent Shield',
106
- threats: inputResult.threats,
107
- output: null
108
- };
109
- }
110
-
111
- // Run the agent
112
- const output = await agentFn(input, ...rest);
113
-
114
- // Scan output
115
- const outputText = typeof output === 'string' ? output : JSON.stringify(output);
116
- const outputResult = shield.scanOutput(outputText, { source: 'agent_output' });
117
-
118
- if (outputResult.blocked) {
119
- return {
120
- blocked: true,
121
- reason: 'Output blocked by Agent Shield',
122
- threats: outputResult.threats,
123
- output: null
124
- };
125
- }
126
-
127
- return {
128
- blocked: false,
129
- threats: [...inputResult.threats, ...outputResult.threats],
130
- output
131
- };
132
- };
133
- };
134
-
135
- /**
136
- * Creates a tool-call interceptor that scans tool calls before execution.
137
- *
138
- * @param {object} tools - Map of tool name -> tool function.
139
- * @param {object} [config] - AgentShield configuration.
140
- * @returns {object} Map of tool name -> wrapped tool function.
141
- *
142
- * @example
143
- * const { shieldTools } = require('agent-shield/src/middleware');
144
- *
145
- * const tools = {
146
- * bash: async (args) => exec(args.command),
147
- * readFile: async (args) => fs.readFile(args.path, 'utf-8'),
148
- * };
149
- *
150
- * const protectedTools = shieldTools(tools, {
151
- * blockOnThreat: true,
152
- * logging: true
153
- * });
154
- *
155
- * // Use protectedTools in your agent — dangerous calls get blocked
156
- */
157
- const shieldTools = (tools, config = {}) => {
158
- const shield = new AgentShield({ blockOnThreat: true, ...config });
159
- const wrapped = {};
160
-
161
- for (const [name, fn] of Object.entries(tools)) {
162
- wrapped[name] = async (args, ...rest) => {
163
- const result = shield.scanToolCall(name, args);
164
-
165
- if (result.blocked) {
166
- const error = new Error(
167
- `[Agent Shield] Tool call "${name}" blocked: ${result.threats.map(t => t.description).join('; ')}`
168
- );
169
- error.agentShield = result;
170
- throw error;
171
- }
172
-
173
- return fn(args, ...rest);
174
- };
175
- }
176
-
177
- return wrapped;
178
- };
179
-
180
- /**
181
- * Extracts scannable text from common request body formats.
182
- * @param {object} body
183
- * @returns {string|null}
184
- */
185
- const extractTextFromBody = (body) => {
186
- if (!body || (typeof body !== 'object' && typeof body !== 'string')) return null;
187
- if (typeof body === 'string') return body;
188
-
189
- // OpenAI-style messages array
190
- if (body.messages && Array.isArray(body.messages)) {
191
- return body.messages
192
- .map(m => typeof m.content === 'string' ? m.content : JSON.stringify(m.content))
193
- .join('\n');
194
- }
195
-
196
- // Single message/prompt field
197
- if (body.message) return typeof body.message === 'string' ? body.message : JSON.stringify(body.message);
198
- if (body.prompt) return typeof body.prompt === 'string' ? body.prompt : JSON.stringify(body.prompt);
199
- if (body.input) return typeof body.input === 'string' ? body.input : JSON.stringify(body.input);
200
- if (body.query) return typeof body.query === 'string' ? body.query : JSON.stringify(body.query);
201
- if (body.text) return typeof body.text === 'string' ? body.text : JSON.stringify(body.text);
202
-
203
- // Fallback: stringify the whole body
204
- const str = JSON.stringify(body);
205
- return str.length > 20 ? str : null;
206
- };
207
-
208
- module.exports = { expressMiddleware, wrapAgent, shieldTools, extractTextFromBody };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield Middleware
5
+ *
6
+ * Plug-and-play middleware for common agent frameworks.
7
+ * Wraps agent input/output pipelines with automatic threat scanning.
8
+ */
9
+
10
+ const { AgentShield } = require('./index');
11
+ const { RateLimiter } = require('./circuit-breaker');
12
+
13
+ /**
14
+ * Creates an Express/Connect-style middleware that scans request bodies
15
+ * for AI-specific threats before they reach your agent endpoint.
16
+ *
17
+ * @param {object} [config] - AgentShield configuration.
18
+ * @returns {Function} Express middleware function.
19
+ *
20
+ * @example
21
+ * const express = require('express');
22
+ * const { expressMiddleware } = require('agent-shield/src/middleware');
23
+ *
24
+ * const app = express();
25
+ * app.use(express.json());
26
+ * app.use(expressMiddleware({ blockOnThreat: true, blockThreshold: 'high' }));
27
+ *
28
+ * app.post('/agent', (req, res) => {
29
+ * // req.agentShield contains scan results
30
+ * if (req.agentShield.blocked) {
31
+ * return res.status(400).json({ error: 'Input blocked for safety' });
32
+ * }
33
+ * // ... process the agent request
34
+ * });
35
+ */
36
+ const expressMiddleware = (config = {}) => {
37
+ const shield = new AgentShield({ blockOnThreat: true, ...config });
38
+
39
+ return (req, res, next) => {
40
+ if (!req.body) {
41
+ req.agentShield = { status: 'safe', threats: [], blocked: false };
42
+ return next();
43
+ }
44
+
45
+ // Extract text from common request body shapes
46
+ const text = extractTextFromBody(req.body);
47
+
48
+ if (!text) {
49
+ req.agentShield = { status: 'safe', threats: [], blocked: false };
50
+ return next();
51
+ }
52
+
53
+ const result = shield.scanInput(text, { source: 'http_request' });
54
+ req.agentShield = result;
55
+
56
+ if (result.blocked) {
57
+ return res.status(400).json({
58
+ error: 'Input blocked by Agent Shield',
59
+ status: result.status,
60
+ threats: result.threats.map(t => ({
61
+ severity: t.severity,
62
+ description: t.description
63
+ }))
64
+ });
65
+ }
66
+
67
+ next();
68
+ };
69
+ };
70
+
71
+ /**
72
+ * Creates a wrapper function that scans input/output around any async function.
73
+ * Works with any agent framework — just wrap your agent's main function.
74
+ *
75
+ * @param {Function} agentFn - The agent function to wrap. Should accept (input) and return output.
76
+ * @param {object} [config] - AgentShield configuration.
77
+ * @returns {Function} Wrapped function with the same signature.
78
+ *
79
+ * @example
80
+ * const { wrapAgent } = require('agent-shield/src/middleware');
81
+ *
82
+ * async function myAgent(input) {
83
+ * const response = await callLLM(input);
84
+ * return response;
85
+ * }
86
+ *
87
+ * const protectedAgent = wrapAgent(myAgent, {
88
+ * blockOnThreat: true,
89
+ * logging: true
90
+ * });
91
+ *
92
+ * // Use it the same way
93
+ * const result = await protectedAgent('Hello, how are you?');
94
+ */
95
+ const wrapAgent = (agentFn, config = {}) => {
96
+ const shield = new AgentShield({ blockOnThreat: true, ...config });
97
+
98
+ return async (input, ...rest) => {
99
+ // Scan input
100
+ const inputText = typeof input === 'string' ? input : JSON.stringify(input);
101
+ const inputResult = shield.scanInput(inputText, { source: 'agent_input' });
102
+
103
+ if (inputResult.blocked) {
104
+ return {
105
+ blocked: true,
106
+ reason: 'Input blocked by Agent Shield',
107
+ threats: inputResult.threats,
108
+ output: null
109
+ };
110
+ }
111
+
112
+ // Run the agent
113
+ const output = await agentFn(input, ...rest);
114
+
115
+ // Scan output
116
+ const outputText = typeof output === 'string' ? output : JSON.stringify(output);
117
+ const outputResult = shield.scanOutput(outputText, { source: 'agent_output' });
118
+
119
+ if (outputResult.blocked) {
120
+ return {
121
+ blocked: true,
122
+ reason: 'Output blocked by Agent Shield',
123
+ threats: outputResult.threats,
124
+ output: null
125
+ };
126
+ }
127
+
128
+ return {
129
+ blocked: false,
130
+ threats: [...inputResult.threats, ...outputResult.threats],
131
+ output
132
+ };
133
+ };
134
+ };
135
+
136
+ /**
137
+ * Creates a tool-call interceptor that scans tool calls before execution.
138
+ *
139
+ * @param {object} tools - Map of tool name -> tool function.
140
+ * @param {object} [config] - AgentShield configuration.
141
+ * @returns {object} Map of tool name -> wrapped tool function.
142
+ *
143
+ * @example
144
+ * const { shieldTools } = require('agent-shield/src/middleware');
145
+ *
146
+ * const tools = {
147
+ * bash: async (args) => exec(args.command),
148
+ * readFile: async (args) => fs.readFile(args.path, 'utf-8'),
149
+ * };
150
+ *
151
+ * const protectedTools = shieldTools(tools, {
152
+ * blockOnThreat: true,
153
+ * logging: true
154
+ * });
155
+ *
156
+ * // Use protectedTools in your agent — dangerous calls get blocked
157
+ */
158
+ const shieldTools = (tools, config = {}) => {
159
+ const shield = new AgentShield({ blockOnThreat: true, ...config });
160
+ const wrapped = {};
161
+
162
+ for (const [name, fn] of Object.entries(tools)) {
163
+ wrapped[name] = async (args, ...rest) => {
164
+ const result = shield.scanToolCall(name, args);
165
+
166
+ if (result.blocked) {
167
+ const error = new Error(
168
+ `[Agent Shield] Tool call "${name}" blocked: ${result.threats.map(t => t.description).join('; ')}`
169
+ );
170
+ error.agentShield = result;
171
+ throw error;
172
+ }
173
+
174
+ return fn(args, ...rest);
175
+ };
176
+ }
177
+
178
+ return wrapped;
179
+ };
180
+
181
+ /**
182
+ * Extracts scannable text from common request body formats.
183
+ * @param {object} body
184
+ * @returns {string|null}
185
+ */
186
+ const extractTextFromBody = (body) => {
187
+ if (!body || (typeof body !== 'object' && typeof body !== 'string')) return null;
188
+ if (typeof body === 'string') return body;
189
+
190
+ // OpenAI-style messages array
191
+ if (body.messages && Array.isArray(body.messages)) {
192
+ return body.messages
193
+ .map(m => typeof m.content === 'string' ? m.content : JSON.stringify(m.content))
194
+ .join('\n');
195
+ }
196
+
197
+ // Single message/prompt field
198
+ if (body.message) return typeof body.message === 'string' ? body.message : JSON.stringify(body.message);
199
+ if (body.prompt) return typeof body.prompt === 'string' ? body.prompt : JSON.stringify(body.prompt);
200
+ if (body.input) return typeof body.input === 'string' ? body.input : JSON.stringify(body.input);
201
+ if (body.query) return typeof body.query === 'string' ? body.query : JSON.stringify(body.query);
202
+ if (body.text) return typeof body.text === 'string' ? body.text : JSON.stringify(body.text);
203
+
204
+ // Fallback: stringify the whole body
205
+ const str = JSON.stringify(body);
206
+ return str.length > 20 ? str : null;
207
+ };
208
+
209
+ /**
210
+ * Creates rate-limiting middleware that returns 429 responses when limits are exceeded.
211
+ * Includes backpressure headers (X-RateLimit-Remaining, X-RateLimit-Limit, Retry-After).
212
+ *
213
+ * @param {object} [options]
214
+ * @param {number} [options.maxRequests=100] - Max requests per window.
215
+ * @param {number} [options.windowMs=60000] - Window size in ms (default: 1 minute).
216
+ * @param {number} [options.maxThreatsPerWindow=10] - Max threats before anomaly flag.
217
+ * @param {Function} [options.onLimit] - Callback when limit is hit.
218
+ * @param {boolean} [options.includeBackpressureHeaders=true] - Add rate limit headers to all responses.
219
+ * @returns {Function} Express middleware function.
220
+ *
221
+ * @example
222
+ * const { rateLimitMiddleware } = require('agent-shield/src/middleware');
223
+ * app.use(rateLimitMiddleware({ maxRequests: 50, windowMs: 60000 }));
224
+ */
225
+ const rateLimitMiddleware = (options = {}) => {
226
+ const includeHeaders = options.includeBackpressureHeaders !== false;
227
+ const limiter = new RateLimiter({
228
+ maxRequests: options.maxRequests || 100,
229
+ windowMs: options.windowMs || 60000,
230
+ maxThreatsPerWindow: options.maxThreatsPerWindow || 10,
231
+ onLimit: options.onLimit || null,
232
+ onAnomaly: options.onAnomaly || null
233
+ });
234
+
235
+ return (req, res, next) => {
236
+ const check = limiter.recordRequest();
237
+
238
+ // Always set backpressure headers so callers can see remaining capacity
239
+ if (includeHeaders) {
240
+ res.setHeader('X-RateLimit-Limit', limiter.maxRequests);
241
+ res.setHeader('X-RateLimit-Remaining', Math.max(0, check.remaining));
242
+ }
243
+
244
+ if (!check.allowed) {
245
+ const retryAfterSec = Math.ceil(limiter.windowMs / 1000);
246
+ res.setHeader('Retry-After', retryAfterSec);
247
+ return res.status(429).json({
248
+ error: 'Too Many Requests',
249
+ message: check.reason,
250
+ retryAfter: retryAfterSec
251
+ });
252
+ }
253
+
254
+ // Expose limiter on request for downstream threat recording
255
+ req.agentShieldRateLimiter = limiter;
256
+ next();
257
+ };
258
+ };
259
+
260
+ /**
261
+ * Creates a combined Express middleware that applies rate limiting, threat scanning,
262
+ * and backpressure headers in a single middleware call.
263
+ *
264
+ * @param {object} [config] - AgentShield + rate limiter configuration.
265
+ * @param {number} [config.maxRequests=100] - Rate limit: max requests per window.
266
+ * @param {number} [config.windowMs=60000] - Rate limit: window size in ms.
267
+ * @param {boolean} [config.includeBackpressureHeaders=true] - Add rate limit headers.
268
+ * @returns {Function} Express middleware function.
269
+ *
270
+ * @example
271
+ * app.use(shieldMiddleware({ blockOnThreat: true, maxRequests: 50 }));
272
+ */
273
+ const shieldMiddleware = (config = {}) => {
274
+ const rateLimiter = rateLimitMiddleware({
275
+ maxRequests: config.maxRequests,
276
+ windowMs: config.windowMs,
277
+ maxThreatsPerWindow: config.maxThreatsPerWindow,
278
+ includeBackpressureHeaders: config.includeBackpressureHeaders,
279
+ onLimit: config.onLimit,
280
+ onAnomaly: config.onAnomaly
281
+ });
282
+ const scanner = expressMiddleware(config);
283
+
284
+ return (req, res, next) => {
285
+ // Rate limit first
286
+ rateLimiter(req, res, (err) => {
287
+ if (err) return next(err);
288
+ // Then scan
289
+ scanner(req, res, (scanErr) => {
290
+ if (scanErr) return next(scanErr);
291
+ // Record threats in rate limiter for anomaly detection
292
+ if (req.agentShield && req.agentShield.threats && req.agentShield.threats.length > 0) {
293
+ if (req.agentShieldRateLimiter) {
294
+ req.agentShieldRateLimiter.recordThreat(req.agentShield.threats.length);
295
+ }
296
+ }
297
+ next();
298
+ });
299
+ });
300
+ };
301
+ };
302
+
303
+ module.exports = { expressMiddleware, wrapAgent, shieldTools, extractTextFromBody, rateLimitMiddleware, shieldMiddleware };