agentshield-sdk 13.3.0 → 14.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/src/enterprise.js CHANGED
@@ -16,18 +16,104 @@ const { loadPolicy } = require('./policy');
16
16
  // Multi-Tenant Shield
17
17
  // =========================================================================
18
18
 
19
+ /**
20
+ * Multi-tenant Shield.
21
+ *
22
+ * SECURITY: Tenant IDs are treated as trust boundaries — scans, stats,
23
+ * and policies are partitioned per `tenantId`. In production, callers
24
+ * MUST configure `options.tenantVerifier` to prove that a supplied
25
+ * tenantId was established by a trusted authentication mechanism
26
+ * (JWT, session, mTLS, etc.). Without a verifier, a caller that can
27
+ * invent tenant IDs can read/write any tenant's data.
28
+ *
29
+ * @example
30
+ * const shield = new MultiTenantShield({
31
+ * tenantVerifier: (tenantId, ctx) => ctx && ctx.jwt && ctx.jwt.tenant === tenantId,
32
+ * strictAuth: true
33
+ * });
34
+ * shield.scan('tenant-42', userInput, { context: { jwt: decodedJwt } });
35
+ */
19
36
  class MultiTenantShield {
20
37
  constructor(options = {}) {
21
38
  this.tenants = new Map();
22
39
  this.defaultPolicy = options.defaultPolicy || { sensitivity: 'high', blockOnThreat: true };
23
40
  this.globalOverrides = options.globalOverrides || {};
24
41
  this.onTenantCreated = options.onTenantCreated || null;
42
+ this.tenantVerifier = typeof options.tenantVerifier === 'function'
43
+ ? options.tenantVerifier
44
+ : null;
45
+ this.strictAuth = options.strictAuth === true;
46
+
47
+ if (!this.tenantVerifier) {
48
+ if (this.strictAuth) {
49
+ throw new Error(
50
+ '[Agent Shield] MultiTenantShield: strictAuth is enabled but no options.tenantVerifier was provided. Supply a (tenantId, context) => boolean verifier.'
51
+ );
52
+ }
53
+ console.warn('[Agent Shield] WARNING: MultiTenantShield has no tenantVerifier. Tenant IDs are trusted by default. Set options.tenantVerifier in production.');
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Verify that a tenantId is authorized for the current caller.
59
+ * @param {string} tenantId
60
+ * @param {object} [context] - Request/auth context passed by the caller.
61
+ * @returns {boolean}
62
+ * @private
63
+ */
64
+ _verifyTenant(tenantId, context) {
65
+ if (typeof tenantId !== 'string' || tenantId.length === 0) {
66
+ throw new Error('[Agent Shield] MultiTenantShield: tenantId must be a non-empty string');
67
+ }
68
+ if (!this.tenantVerifier) {
69
+ // Backward-compatible: permit by default, warning already logged at construction.
70
+ return true;
71
+ }
72
+ let ok = false;
73
+ try {
74
+ ok = this.tenantVerifier(tenantId, context || {}) === true;
75
+ } catch (err) {
76
+ throw new Error(`[Agent Shield] MultiTenantShield: tenantVerifier threw while verifying tenant "${tenantId}": ${err.message}`);
77
+ }
78
+ if (!ok) {
79
+ throw new Error(`[Agent Shield] MultiTenantShield: tenantVerifier rejected tenant "${tenantId}"`);
80
+ }
81
+ return true;
82
+ }
83
+
84
+ /**
85
+ * Return a new MultiTenantShield that reuses this instance's tenant
86
+ * registrations/stats but enforces the supplied tenant verifier. Useful
87
+ * for adding auth to an existing shield without mutating global state.
88
+ *
89
+ * @param {(tenantId: string, context: object) => boolean} verifier
90
+ * @param {object} [extraOptions]
91
+ * @returns {MultiTenantShield}
92
+ */
93
+ withAuth(verifier, extraOptions = {}) {
94
+ if (typeof verifier !== 'function') {
95
+ throw new Error('[Agent Shield] MultiTenantShield.withAuth: verifier must be a function');
96
+ }
97
+ const next = new MultiTenantShield({
98
+ defaultPolicy: this.defaultPolicy,
99
+ globalOverrides: this.globalOverrides,
100
+ onTenantCreated: this.onTenantCreated,
101
+ tenantVerifier: verifier,
102
+ strictAuth: extraOptions.strictAuth === true
103
+ });
104
+ // Share tenant registry so existing tenants remain accessible.
105
+ next.tenants = this.tenants;
106
+ return next;
25
107
  }
26
108
 
27
109
  /**
28
110
  * Register a tenant with its own policy.
111
+ * @param {string} tenantId
112
+ * @param {object} [policy]
113
+ * @param {object} [context] - Auth context forwarded to the tenantVerifier.
29
114
  */
30
- registerTenant(tenantId, policy = {}) {
115
+ registerTenant(tenantId, policy = {}, context) {
116
+ this._verifyTenant(tenantId, context);
31
117
  const mergedPolicy = { ...this.defaultPolicy, ...policy, ...this.globalOverrides };
32
118
  const shield = new AgentShield(mergedPolicy);
33
119
 
@@ -48,19 +134,38 @@ class MultiTenantShield {
48
134
 
49
135
  /**
50
136
  * Get or auto-create a tenant shield.
137
+ * @param {string} tenantId
138
+ * @param {object} [context] - Auth context forwarded to the tenantVerifier.
51
139
  */
52
- getTenant(tenantId) {
140
+ getTenant(tenantId, context) {
141
+ this._verifyTenant(tenantId, context);
53
142
  if (!this.tenants.has(tenantId)) {
54
- this.registerTenant(tenantId);
143
+ // Skip re-verification — we just verified above.
144
+ const mergedPolicy = { ...this.defaultPolicy, ...this.globalOverrides };
145
+ const shield = new AgentShield(mergedPolicy);
146
+ this.tenants.set(tenantId, {
147
+ id: tenantId,
148
+ policy: mergedPolicy,
149
+ shield,
150
+ stats: { scans: 0, threats: 0, blocked: 0 },
151
+ createdAt: new Date().toISOString()
152
+ });
153
+ if (this.onTenantCreated) {
154
+ this.onTenantCreated(tenantId, mergedPolicy);
155
+ }
55
156
  }
56
157
  return this.tenants.get(tenantId);
57
158
  }
58
159
 
59
160
  /**
60
161
  * Scan input for a specific tenant.
162
+ * @param {string} tenantId
163
+ * @param {string} text
164
+ * @param {object} [options]
165
+ * @param {object} [options.context] - Auth context forwarded to the tenantVerifier.
61
166
  */
62
167
  scan(tenantId, text, options = {}) {
63
- const tenant = this.getTenant(tenantId);
168
+ const tenant = this.getTenant(tenantId, options.context);
64
169
  tenant.stats.scans++;
65
170
 
66
171
  const result = tenant.shield.scan(text, options);
@@ -78,30 +183,39 @@ class MultiTenantShield {
78
183
  /**
79
184
  * Scan input for a specific tenant.
80
185
  */
81
- scanInput(tenantId, text) {
82
- return this.scan(tenantId, text);
186
+ scanInput(tenantId, text, options = {}) {
187
+ return this.scan(tenantId, text, options);
83
188
  }
84
189
 
85
190
  /**
86
191
  * Scan output for a specific tenant.
87
192
  */
88
- scanOutput(tenantId, text) {
89
- const tenant = this.getTenant(tenantId);
193
+ scanOutput(tenantId, text, options = {}) {
194
+ const tenant = this.getTenant(tenantId, options.context);
90
195
  return tenant.shield.scanOutput(text);
91
196
  }
92
197
 
93
198
  /**
94
199
  * Update a tenant's policy.
95
200
  */
96
- updatePolicy(tenantId, policy) {
97
- const tenant = this.getTenant(tenantId);
201
+ updatePolicy(tenantId, policy, context) {
202
+ const tenant = this.getTenant(tenantId, context);
98
203
  tenant.policy = { ...tenant.policy, ...policy, ...this.globalOverrides };
99
204
  tenant.shield = new AgentShield(tenant.policy);
100
205
  return tenant.policy;
101
206
  }
102
207
 
103
208
  /**
104
- * Get stats for all tenants.
209
+ * Get stats for a single tenant (auth-checked).
210
+ */
211
+ getStats(tenantId, context) {
212
+ const tenant = this.getTenant(tenantId, context);
213
+ return { ...tenant.stats, policy: tenant.policy };
214
+ }
215
+
216
+ /**
217
+ * Get stats for all tenants. NOTE: this method bypasses per-tenant
218
+ * auth — callers should gate access to it at the admin level.
105
219
  */
106
220
  getAllStats() {
107
221
  const stats = {};
@@ -114,7 +228,8 @@ class MultiTenantShield {
114
228
  /**
115
229
  * Remove a tenant.
116
230
  */
117
- removeTenant(tenantId) {
231
+ removeTenant(tenantId, context) {
232
+ this._verifyTenant(tenantId, context);
118
233
  return this.tenants.delete(tenantId);
119
234
  }
120
235
 
@@ -0,0 +1,373 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Framework Integration Wrappers
5
+ *
6
+ * Plug-and-play integrations for next-generation AI agent frameworks:
7
+ * - CrewAI (task decorators & callbacks)
8
+ * - Google Agent Development Kit (plugin system)
9
+ * - Microsoft Agent Framework (middleware pipeline)
10
+ *
11
+ * These close gaps identified in the Microsoft Agent Governance Toolkit
12
+ * parity audit.
13
+ *
14
+ * @module integrations-frameworks
15
+ */
16
+
17
+ const { AgentShield } = require('./index');
18
+ const { ShieldBlockError } = require('./integrations');
19
+
20
+ // =========================================================================
21
+ // CrewAI Integration
22
+ // =========================================================================
23
+
24
+ /**
25
+ * Creates Agent Shield callbacks for CrewAI task lifecycle.
26
+ *
27
+ * CrewAI uses task decorators and callbacks. This wrapper provides
28
+ * beforeTask / afterTask hooks that scan task descriptions, expected
29
+ * outputs, and task results for prompt injection and other threats.
30
+ *
31
+ * Usage:
32
+ * const { shieldCrewAI } = require('agentshield-sdk/src/integrations-frameworks');
33
+ * const { beforeTask, afterTask } = shieldCrewAI({ blockOnThreat: true });
34
+ *
35
+ * // In your CrewAI task lifecycle:
36
+ * beforeTask(task, agent); // throws ShieldBlockError if threat found
37
+ * const output = await task.run();
38
+ * afterTask(task, output); // throws ShieldBlockError if threat found
39
+ *
40
+ * @param {object} [options]
41
+ * @param {string} [options.sensitivity='high'] - Detection sensitivity level.
42
+ * @param {boolean} [options.blockOnThreat=true] - Whether to throw on threat detection.
43
+ * @param {string} [options.blockThreshold='high'] - Minimum severity that triggers a block.
44
+ * @param {function} [options.onThreat] - Callback when a threat is detected.
45
+ * @returns {{ beforeTask: function, afterTask: function, shield: AgentShield }}
46
+ */
47
+ function shieldCrewAI(options = {}) {
48
+ const shield = new AgentShield({
49
+ sensitivity: options.sensitivity || 'high',
50
+ blockOnThreat: options.blockOnThreat !== false,
51
+ blockThreshold: options.blockThreshold || 'high'
52
+ });
53
+ const onThreat = options.onThreat || null;
54
+
55
+ /**
56
+ * Scans a CrewAI task before execution.
57
+ * Inspects task.description and task.expected_output for injection.
58
+ *
59
+ * @param {object} task - CrewAI task object.
60
+ * @param {object} [agent] - CrewAI agent assigned to the task.
61
+ * @throws {ShieldBlockError} If a threat is detected and blocking is enabled.
62
+ */
63
+ function beforeTask(task, agent) {
64
+ if (!task) return;
65
+
66
+ const fields = [];
67
+ if (task.description) fields.push(String(task.description));
68
+ if (task.expected_output) fields.push(String(task.expected_output));
69
+
70
+ for (const text of fields) {
71
+ const result = shield.scanInput(text);
72
+ if (result.threats && result.threats.length > 0) {
73
+ if (onThreat) {
74
+ try {
75
+ onThreat({
76
+ phase: 'before_task',
77
+ threats: result.threats,
78
+ task: task.description || '',
79
+ agent: agent && agent.role ? agent.role : undefined
80
+ });
81
+ } catch (e) {
82
+ console.error('[Agent Shield] onThreat callback error:', e.message);
83
+ }
84
+ }
85
+ if (result.blocked) {
86
+ throw new ShieldBlockError('CrewAI task blocked by Agent Shield', result.threats);
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Scans a CrewAI task output after execution.
94
+ *
95
+ * @param {object} task - CrewAI task object.
96
+ * @param {*} output - Task execution output.
97
+ * @throws {ShieldBlockError} If a threat is detected and blocking is enabled.
98
+ */
99
+ function afterTask(task, output) {
100
+ if (output == null) return;
101
+
102
+ const text = typeof output === 'string' ? output : JSON.stringify(output);
103
+ const result = shield.scanOutput(text);
104
+
105
+ if (result.threats && result.threats.length > 0) {
106
+ if (onThreat) {
107
+ try {
108
+ onThreat({
109
+ phase: 'after_task',
110
+ threats: result.threats,
111
+ task: task && task.description ? task.description : ''
112
+ });
113
+ } catch (e) {
114
+ console.error('[Agent Shield] onThreat callback error:', e.message);
115
+ }
116
+ }
117
+ if (result.blocked) {
118
+ throw new ShieldBlockError('CrewAI task output blocked by Agent Shield', result.threats);
119
+ }
120
+ }
121
+ }
122
+
123
+ return { beforeTask, afterTask, shield };
124
+ }
125
+
126
+ // =========================================================================
127
+ // Google Agent Development Kit (ADK) Integration
128
+ // =========================================================================
129
+
130
+ /**
131
+ * Creates Agent Shield hooks for the Google Agent Development Kit plugin system.
132
+ *
133
+ * Google ADK uses a plugin architecture with lifecycle hooks. This wrapper
134
+ * provides beforeToolCall, afterToolCall, and beforeGenerate functions that
135
+ * scan tool arguments, tool results, and generation prompts for threats.
136
+ *
137
+ * Usage:
138
+ * const { shieldGoogleADK } = require('agentshield-sdk/src/integrations-frameworks');
139
+ * const hooks = shieldGoogleADK({ blockOnThreat: true });
140
+ *
141
+ * // Register as ADK plugin callbacks:
142
+ * hooks.beforeToolCall('web_search', { query: userInput });
143
+ * const result = await tool.execute(args);
144
+ * hooks.afterToolCall('web_search', result);
145
+ * hooks.beforeGenerate(prompt);
146
+ *
147
+ * @param {object} [options]
148
+ * @param {string} [options.sensitivity='high'] - Detection sensitivity level.
149
+ * @param {boolean} [options.blockOnThreat=true] - Whether to throw on threat detection.
150
+ * @param {string} [options.blockThreshold='high'] - Minimum severity that triggers a block.
151
+ * @param {function} [options.onThreat] - Callback when a threat is detected.
152
+ * @returns {{ beforeToolCall: function, afterToolCall: function, beforeGenerate: function, shield: AgentShield }}
153
+ */
154
+ function shieldGoogleADK(options = {}) {
155
+ const shield = new AgentShield({
156
+ sensitivity: options.sensitivity || 'high',
157
+ blockOnThreat: options.blockOnThreat !== false,
158
+ blockThreshold: options.blockThreshold || 'high'
159
+ });
160
+ const onThreat = options.onThreat || null;
161
+
162
+ /**
163
+ * Scans tool arguments before a tool call.
164
+ *
165
+ * @param {string} toolName - Name of the tool being called.
166
+ * @param {*} args - Tool arguments (object, string, or any serializable value).
167
+ * @throws {ShieldBlockError} If a threat is detected and blocking is enabled.
168
+ */
169
+ function beforeToolCall(toolName, args) {
170
+ if (args == null) return;
171
+
172
+ const text = typeof args === 'string' ? args : JSON.stringify(args);
173
+ const result = shield.scanInput(text);
174
+
175
+ if (result.threats && result.threats.length > 0) {
176
+ if (onThreat) {
177
+ try {
178
+ onThreat({
179
+ phase: 'before_tool_call',
180
+ toolName: toolName || 'unknown',
181
+ threats: result.threats
182
+ });
183
+ } catch (e) {
184
+ console.error('[Agent Shield] onThreat callback error:', e.message);
185
+ }
186
+ }
187
+ if (result.blocked) {
188
+ throw new ShieldBlockError(
189
+ `Google ADK tool "${toolName || 'unknown'}" call blocked by Agent Shield`,
190
+ result.threats
191
+ );
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Scans tool results after a tool call.
198
+ *
199
+ * @param {string} toolName - Name of the tool that was called.
200
+ * @param {*} result - Tool execution result.
201
+ * @throws {ShieldBlockError} If a threat is detected and blocking is enabled.
202
+ */
203
+ function afterToolCall(toolName, toolResult) {
204
+ if (toolResult == null) return;
205
+
206
+ const text = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
207
+ const result = shield.scanOutput(text);
208
+
209
+ if (result.threats && result.threats.length > 0) {
210
+ if (onThreat) {
211
+ try {
212
+ onThreat({
213
+ phase: 'after_tool_call',
214
+ toolName: toolName || 'unknown',
215
+ threats: result.threats
216
+ });
217
+ } catch (e) {
218
+ console.error('[Agent Shield] onThreat callback error:', e.message);
219
+ }
220
+ }
221
+ if (result.blocked) {
222
+ throw new ShieldBlockError(
223
+ `Google ADK tool "${toolName || 'unknown'}" result blocked by Agent Shield`,
224
+ result.threats
225
+ );
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Scans a prompt before generation.
232
+ *
233
+ * @param {string|*} prompt - The prompt to scan.
234
+ * @throws {ShieldBlockError} If a threat is detected and blocking is enabled.
235
+ */
236
+ function beforeGenerate(prompt) {
237
+ if (prompt == null) return;
238
+
239
+ const text = typeof prompt === 'string' ? prompt : JSON.stringify(prompt);
240
+ const result = shield.scanInput(text);
241
+
242
+ if (result.threats && result.threats.length > 0) {
243
+ if (onThreat) {
244
+ try {
245
+ onThreat({
246
+ phase: 'before_generate',
247
+ threats: result.threats
248
+ });
249
+ } catch (e) {
250
+ console.error('[Agent Shield] onThreat callback error:', e.message);
251
+ }
252
+ }
253
+ if (result.blocked) {
254
+ throw new ShieldBlockError('Google ADK generation blocked by Agent Shield', result.threats);
255
+ }
256
+ }
257
+ }
258
+
259
+ return { beforeToolCall, afterToolCall, beforeGenerate, shield };
260
+ }
261
+
262
+ // =========================================================================
263
+ // Microsoft Agent Framework Integration
264
+ // =========================================================================
265
+
266
+ /**
267
+ * Creates Agent Shield middleware for the Microsoft Agent Framework pipeline.
268
+ *
269
+ * The MS Agent Framework uses a middleware pattern where each middleware
270
+ * receives a context and a next() function. This wrapper scans context.input
271
+ * before calling next(), then scans context.output after next() returns.
272
+ *
273
+ * Usage:
274
+ * const { shieldMSAgentFramework } = require('agentshield-sdk/src/integrations-frameworks');
275
+ * const { agentMiddleware } = shieldMSAgentFramework({ blockOnThreat: true });
276
+ *
277
+ * // Register in the MS Agent Framework pipeline:
278
+ * agent.use(agentMiddleware);
279
+ *
280
+ * @param {object} [options]
281
+ * @param {string} [options.sensitivity='high'] - Detection sensitivity level.
282
+ * @param {boolean} [options.blockOnThreat=true] - Whether to throw on threat detection.
283
+ * @param {string} [options.blockThreshold='high'] - Minimum severity that triggers a block.
284
+ * @param {function} [options.onThreat] - Callback when a threat is detected.
285
+ * @returns {{ agentMiddleware: function, shield: AgentShield }}
286
+ */
287
+ function shieldMSAgentFramework(options = {}) {
288
+ const shield = new AgentShield({
289
+ sensitivity: options.sensitivity || 'high',
290
+ blockOnThreat: options.blockOnThreat !== false,
291
+ blockThreshold: options.blockThreshold || 'high'
292
+ });
293
+ const onThreat = options.onThreat || null;
294
+
295
+ /**
296
+ * Middleware function for the MS Agent Framework pipeline.
297
+ * Scans context.input before next() and context.output after next().
298
+ *
299
+ * @param {object} context - Pipeline context with input/output properties.
300
+ * @param {function} next - Next middleware in the pipeline.
301
+ * @throws {ShieldBlockError} If a threat is detected and blocking is enabled.
302
+ */
303
+ async function agentMiddleware(context, next) {
304
+ // Scan input before passing to next middleware
305
+ if (context && context.input != null) {
306
+ const inputText = typeof context.input === 'string'
307
+ ? context.input
308
+ : JSON.stringify(context.input);
309
+
310
+ const inputResult = shield.scanInput(inputText);
311
+
312
+ if (inputResult.threats && inputResult.threats.length > 0) {
313
+ if (onThreat) {
314
+ try {
315
+ onThreat({
316
+ phase: 'input',
317
+ threats: inputResult.threats,
318
+ text: inputText
319
+ });
320
+ } catch (e) {
321
+ console.error('[Agent Shield] onThreat callback error:', e.message);
322
+ }
323
+ }
324
+ if (inputResult.blocked) {
325
+ throw new ShieldBlockError(
326
+ 'MS Agent Framework input blocked by Agent Shield',
327
+ inputResult.threats
328
+ );
329
+ }
330
+ }
331
+ }
332
+
333
+ // Call next middleware in the pipeline
334
+ await next();
335
+
336
+ // Scan output after pipeline execution
337
+ if (context && context.output != null) {
338
+ const outputText = typeof context.output === 'string'
339
+ ? context.output
340
+ : JSON.stringify(context.output);
341
+
342
+ const outputResult = shield.scanOutput(outputText);
343
+
344
+ if (outputResult.threats && outputResult.threats.length > 0) {
345
+ if (onThreat) {
346
+ try {
347
+ onThreat({
348
+ phase: 'output',
349
+ threats: outputResult.threats,
350
+ text: outputText
351
+ });
352
+ } catch (e) {
353
+ console.error('[Agent Shield] onThreat callback error:', e.message);
354
+ }
355
+ }
356
+ if (outputResult.blocked) {
357
+ throw new ShieldBlockError(
358
+ 'MS Agent Framework output blocked by Agent Shield',
359
+ outputResult.threats
360
+ );
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ return { agentMiddleware, shield };
367
+ }
368
+
369
+ module.exports = {
370
+ shieldCrewAI,
371
+ shieldGoogleADK,
372
+ shieldMSAgentFramework
373
+ };