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