agentshield-sdk 10.0.0 → 11.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.
@@ -234,16 +234,28 @@ class DriftMonitor {
234
234
  * @private
235
235
  */
236
236
  _detectDrift(event) {
237
+ // Use minimum std floor to avoid Infinity z-scores on constant baselines
238
+ // If all baseline values were identical (std=0), small deviations are normal variance, not anomalies
239
+ const safeStd = (s, m) => s > 0 ? s : Math.max(Math.abs(m) * 0.1, 1);
240
+
237
241
  const zScores = {
238
- callFreq: Math.abs(zScore(event.callFreq, this.baseline.callFreqMean, this.baseline.callFreqStd)),
239
- responseLength: Math.abs(zScore(event.responseLength, this.baseline.responseLenMean, this.baseline.responseLenStd)),
240
- errorRate: Math.abs(zScore(event.errorRate, this.baseline.errorRateMean, this.baseline.errorRateStd)),
241
- timingMs: Math.abs(zScore(event.timingMs, this.baseline.timingMean, this.baseline.timingStd))
242
+ callFreq: Math.abs(zScore(event.callFreq, this.baseline.callFreqMean, safeStd(this.baseline.callFreqStd, this.baseline.callFreqMean))),
243
+ responseLength: Math.abs(zScore(event.responseLength, this.baseline.responseLenMean, safeStd(this.baseline.responseLenStd, this.baseline.responseLenMean))),
244
+ errorRate: Math.abs(zScore(event.errorRate, this.baseline.errorRateMean, safeStd(this.baseline.errorRateStd, this.baseline.errorRateMean))),
245
+ timingMs: Math.abs(zScore(event.timingMs, this.baseline.timingMean, safeStd(this.baseline.timingStd, this.baseline.timingMean)))
242
246
  };
243
247
 
244
- // KL divergence for topic distribution
248
+ // KL divergence for topic distribution — use sliding window, not single event
249
+ // Single-event distributions cause extreme KL values for any new topic
250
+ const recentTopics = this.current.slice(-Math.max(10, Math.floor(this.windowSize / 2)));
245
251
  const currentDist = {};
246
- currentDist[event.topic] = 1;
252
+ for (const obs of recentTopics) {
253
+ currentDist[obs.topic] = (currentDist[obs.topic] || 0) + 1;
254
+ }
255
+ const recentTotal = recentTopics.length || 1;
256
+ for (const key of Object.keys(currentDist)) {
257
+ currentDist[key] = currentDist[key] / recentTotal;
258
+ }
247
259
  const kl = klDivergence(currentDist, this.baseline.topicDistribution || {});
248
260
 
249
261
  const maxZ = Math.max(...Object.values(zScores));
@@ -0,0 +1,314 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Cryptographic Intent Binding (L5)
5
+ *
6
+ * When a user makes a request, the intent is hashed. Every subsequent
7
+ * agent action must include a cryptographic proof that it derives from
8
+ * that intent. If an injected instruction causes an action that can't
9
+ * be linked back to the original intent, it's blocked at the crypto
10
+ * level — not the pattern level. Unbypassable by any prompt technique.
11
+ *
12
+ * All processing runs locally — no data ever leaves your environment.
13
+ *
14
+ * @module intent-binding
15
+ */
16
+
17
+ const crypto = require('crypto');
18
+
19
+ // =========================================================================
20
+ // IntentToken
21
+ // =========================================================================
22
+
23
+ /**
24
+ * Cryptographic token binding an action to a user intent.
25
+ */
26
+ class IntentToken {
27
+ /**
28
+ * @param {string} intentHash - SHA-256 hash of the original user intent.
29
+ * @param {string} action - The action being authorized.
30
+ * @param {string} scope - Scope of the authorization.
31
+ * @param {string} signature - HMAC signature binding action to intent.
32
+ * @param {number} expiresAt - Expiration timestamp in ms.
33
+ */
34
+ constructor(intentHash, action, scope, signature, expiresAt) {
35
+ this.intentHash = intentHash;
36
+ this.action = action;
37
+ this.scope = scope;
38
+ this.signature = signature;
39
+ this.createdAt = Date.now();
40
+ this.expiresAt = expiresAt;
41
+ this.used = false;
42
+ }
43
+
44
+ /**
45
+ * Check if this token has expired.
46
+ * @returns {boolean}
47
+ */
48
+ isExpired() {
49
+ return Date.now() > this.expiresAt;
50
+ }
51
+ }
52
+
53
+ // =========================================================================
54
+ // IntentBinder
55
+ // =========================================================================
56
+
57
+ /**
58
+ * Cryptographic intent binding engine. Creates tamper-proof links between
59
+ * user intents and agent actions.
60
+ */
61
+ class IntentBinder {
62
+ /**
63
+ * @param {object} [options]
64
+ * @param {string} [options.signingKey] - HMAC signing key (auto-generated if not provided).
65
+ * @param {number} [options.tokenTtlMs=300000] - Token TTL in ms (default: 5 minutes).
66
+ * @param {number} [options.maxActionsPerIntent=50] - Max actions per intent.
67
+ * @param {boolean} [options.singleUseTokens=false] - Whether tokens can only be used once.
68
+ */
69
+ constructor(options = {}) {
70
+ this.signingKey = options.signingKey || crypto.randomBytes(32).toString('hex');
71
+ this.tokenTtlMs = options.tokenTtlMs || 300000;
72
+ this.maxActions = options.maxActionsPerIntent || 50;
73
+ this.singleUse = options.singleUseTokens || false;
74
+
75
+ /** @type {Map<string, { intent: string, hash: string, actions: string[], createdAt: number }>} */
76
+ this.activeIntents = new Map();
77
+
78
+ /** @type {Array<object>} */
79
+ this.auditLog = [];
80
+ this.stats = { intentsBound: 0, tokensIssued: 0, verified: 0, rejected: 0 };
81
+ }
82
+
83
+ /**
84
+ * Bind a user intent. Returns an intent hash that must be included
85
+ * with every subsequent action.
86
+ *
87
+ * @param {string} intentText - The user's original request.
88
+ * @param {object} [metadata] - Additional context (userId, sessionId, etc.).
89
+ * @returns {{ intentHash: string, allowedActions: string[] }}
90
+ */
91
+ bindIntent(intentText, metadata = {}) {
92
+ const intentHash = this._hash(intentText);
93
+ const allowedActions = this._deriveAllowedActions(intentText);
94
+
95
+ this.activeIntents.set(intentHash, {
96
+ intent: intentText,
97
+ hash: intentHash,
98
+ actions: allowedActions,
99
+ metadata,
100
+ createdAt: Date.now(),
101
+ actionCount: 0
102
+ });
103
+
104
+ this.stats.intentsBound++;
105
+ this._log('intent_bound', { intentHash, allowedActions, metadata });
106
+
107
+ // Auto-purge if map grows too large
108
+ if (this.activeIntents.size > 10000) {
109
+ this.purgeExpired();
110
+ }
111
+
112
+ return { intentHash, allowedActions };
113
+ }
114
+
115
+ /**
116
+ * Issue a cryptographic token authorizing a specific action
117
+ * linked to an intent.
118
+ *
119
+ * @param {string} intentHash - The intent hash from bindIntent().
120
+ * @param {string} action - The action to authorize (e.g., 'tool:readFile').
121
+ * @param {string} [scope] - Optional scope restriction.
122
+ * @returns {{ token: IntentToken|null, error: string|null }}
123
+ */
124
+ issueToken(intentHash, action, scope) {
125
+ const intent = this.activeIntents.get(intentHash);
126
+ if (!intent) {
127
+ this.stats.rejected++;
128
+ return { token: null, error: 'Intent not found or expired.' };
129
+ }
130
+
131
+ // Check action limit
132
+ if (intent.actionCount >= this.maxActions) {
133
+ this.stats.rejected++;
134
+ return { token: null, error: `Action limit (${this.maxActions}) exceeded for this intent.` };
135
+ }
136
+
137
+ // Verify the action is derivable from the intent
138
+ if (!this._isActionAllowed(intent, action)) {
139
+ this.stats.rejected++;
140
+ this._log('token_rejected', { intentHash, action, reason: 'Action not derivable from intent.' });
141
+ return { token: null, error: `Action "${action}" is not derivable from the bound intent.` };
142
+ }
143
+
144
+ // Create signed token
145
+ const scopeStr = scope || 'default';
146
+ const payload = `${intentHash}:${action}:${scopeStr}`;
147
+ const signature = this._sign(payload);
148
+ const expiresAt = Date.now() + this.tokenTtlMs;
149
+
150
+ const token = new IntentToken(intentHash, action, scopeStr, signature, expiresAt);
151
+ intent.actionCount++;
152
+ this.stats.tokensIssued++;
153
+ this._log('token_issued', { intentHash, action, scope: scopeStr });
154
+
155
+ return { token, error: null };
156
+ }
157
+
158
+ /**
159
+ * Verify that a token is valid and the action is still bound to the intent.
160
+ *
161
+ * @param {IntentToken} token - The token to verify.
162
+ * @returns {{ valid: boolean, reason: string|null }}
163
+ */
164
+ verify(token) {
165
+ if (!token) {
166
+ this.stats.rejected++;
167
+ return { valid: false, reason: 'No token provided.' };
168
+ }
169
+
170
+ if (token.isExpired()) {
171
+ this.stats.rejected++;
172
+ return { valid: false, reason: 'Token has expired.' };
173
+ }
174
+
175
+ if (this.singleUse && token.used) {
176
+ this.stats.rejected++;
177
+ return { valid: false, reason: 'Token has already been used (single-use mode).' };
178
+ }
179
+
180
+ // Verify HMAC signature
181
+ const payload = `${token.intentHash}:${token.action}:${token.scope}`;
182
+ const expectedSig = this._sign(payload);
183
+ if (token.signature !== expectedSig) {
184
+ this.stats.rejected++;
185
+ this._log('token_tampered', { intentHash: token.intentHash, action: token.action });
186
+ return { valid: false, reason: 'Token signature is invalid. Possible tampering.' };
187
+ }
188
+
189
+ // Verify intent still active
190
+ if (!this.activeIntents.has(token.intentHash)) {
191
+ this.stats.rejected++;
192
+ return { valid: false, reason: 'Bound intent no longer active.' };
193
+ }
194
+
195
+ token.used = true;
196
+ this.stats.verified++;
197
+ return { valid: true, reason: null };
198
+ }
199
+
200
+ /**
201
+ * Revoke an intent and all its tokens.
202
+ * @param {string} intentHash
203
+ * @returns {boolean}
204
+ */
205
+ revokeIntent(intentHash) {
206
+ const deleted = this.activeIntents.delete(intentHash);
207
+ if (deleted) this._log('intent_revoked', { intentHash });
208
+ return deleted;
209
+ }
210
+
211
+ /**
212
+ * Get statistics.
213
+ * @returns {object}
214
+ */
215
+ getStats() {
216
+ return { ...this.stats, activeIntents: this.activeIntents.size };
217
+ }
218
+
219
+ /**
220
+ * Get audit log.
221
+ * @returns {Array<object>}
222
+ */
223
+ getAuditLog() {
224
+ return [...this.auditLog];
225
+ }
226
+
227
+ /**
228
+ * Purge expired intents.
229
+ */
230
+ purgeExpired() {
231
+ const now = Date.now();
232
+ for (const [hash, intent] of this.activeIntents) {
233
+ if (now - intent.createdAt > this.tokenTtlMs * 2) {
234
+ this.activeIntents.delete(hash);
235
+ }
236
+ }
237
+ }
238
+
239
+ // -----------------------------------------------------------------------
240
+ // Private
241
+ // -----------------------------------------------------------------------
242
+
243
+ /**
244
+ * Derive allowed actions from intent text using keyword analysis.
245
+ * @private
246
+ */
247
+ _deriveAllowedActions(intentText) {
248
+ const lower = intentText.toLowerCase();
249
+ const actions = [];
250
+
251
+ if (/\b(?:read|get|fetch|show|display|find|search|query|list|look\s*up)\b/.test(lower)) actions.push('data:read');
252
+ if (/\b(?:write|create|update|edit|modify|save|add|insert|set)\b/.test(lower)) actions.push('data:write');
253
+ if (/\b(?:delete|remove|drop|clear|purge|destroy)\b/.test(lower)) actions.push('data:delete');
254
+ if (/\b(?:send|email|message|notify|post|share|communicate|slack)\b/.test(lower)) actions.push('comm:send');
255
+ if (/\b(?:run|execute|bash|shell|script|compile|build|test)\b/.test(lower)) actions.push('exec:run');
256
+ if (/\b(?:file|open|download|upload|path|directory|folder)\b/.test(lower)) actions.push('fs:access');
257
+ if (/\b(?:http|api|request|fetch|curl|endpoint|url|webhook)\b/.test(lower)) actions.push('net:request');
258
+ if (/\b(?:analyze|calculate|compute|summarize|compare|evaluate)\b/.test(lower)) actions.push('compute:analyze');
259
+
260
+ if (actions.length === 0) actions.push('compute:analyze'); // Default: analysis only
261
+
262
+ return actions;
263
+ }
264
+
265
+ /**
266
+ * Check if an action is allowed by the bound intent.
267
+ * @private
268
+ */
269
+ _isActionAllowed(intent, action) {
270
+ // Exact match
271
+ if (intent.actions.includes(action)) return true;
272
+
273
+ // Category match (e.g., 'data:read' allows 'tool:readFile')
274
+ const actionCategory = action.split(':')[0];
275
+ return intent.actions.some(a => a.split(':')[0] === actionCategory);
276
+ }
277
+
278
+ /**
279
+ * SHA-256 hash.
280
+ * @private
281
+ */
282
+ _hash(text) {
283
+ return crypto.createHash('sha256').update(text).digest('hex');
284
+ }
285
+
286
+ /**
287
+ * HMAC-SHA256 sign.
288
+ * @private
289
+ */
290
+ _sign(payload) {
291
+ return crypto.createHmac('sha256', this.signingKey).update(payload).digest('hex');
292
+ }
293
+
294
+ /**
295
+ * Log an event.
296
+ * @private
297
+ */
298
+ _log(action, details) {
299
+ this.auditLog.push({ timestamp: Date.now(), action, ...details });
300
+ if (this.auditLog.length > 5000) {
301
+ this.auditLog = this.auditLog.slice(-5000);
302
+ }
303
+ }
304
+ }
305
+
306
+ // =========================================================================
307
+ // EXPORTS
308
+ // =========================================================================
309
+
310
+ module.exports = {
311
+ IntentBinder,
312
+ IntentToken,
313
+ PROVENANCE: require('./semantic-isolation').PROVENANCE
314
+ };