agentshield-sdk 10.0.0 → 12.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 +88 -79
- package/README.md +252 -11
- package/package.json +3 -3
- package/src/agent-intent.js +359 -672
- package/src/attack-surface.js +408 -0
- package/src/continuous-security.js +237 -0
- package/src/cross-turn.js +215 -563
- package/src/detector-core.js +928 -1
- package/src/drift-monitor.js +18 -6
- package/src/ensemble.js +300 -409
- package/src/incident-response.js +265 -0
- package/src/intent-binding.js +314 -0
- package/src/intent-graph.js +381 -0
- package/src/main.js +143 -33
- package/src/mcp-guard.js +565 -3
- package/src/message-integrity.js +226 -0
- package/src/micro-model.js +199 -11
- package/src/ml-detector.js +110 -266
- package/src/normalizer.js +296 -604
- package/src/persistent-learning.js +104 -620
- package/src/prompt-hardening.js +195 -0
- package/src/redteam-cli.js +5 -4
- package/src/self-training.js +586 -631
- package/src/semantic-isolation.js +304 -0
- package/src/smart-config.js +557 -705
- package/src/sota-benchmark.js +749 -0
- package/src/supply-chain-scanner.js +199 -1
- package/types/index.d.ts +251 -580
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Automated Incident Response (v12)
|
|
5
|
+
*
|
|
6
|
+
* When an attack is detected, don't just alert — automatically:
|
|
7
|
+
* isolate the compromised agent, preserve forensic evidence,
|
|
8
|
+
* notify the SOC, generate an incident report, and suggest remediation.
|
|
9
|
+
*
|
|
10
|
+
* Closes the loop from detection to response.
|
|
11
|
+
*
|
|
12
|
+
* All processing runs locally — no data ever leaves your environment.
|
|
13
|
+
*
|
|
14
|
+
* @module incident-response
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
// =========================================================================
|
|
20
|
+
// RESPONSE STRATEGIES
|
|
21
|
+
// =========================================================================
|
|
22
|
+
|
|
23
|
+
const RESPONSE_STRATEGIES = {
|
|
24
|
+
/** Block the action and continue monitoring. */
|
|
25
|
+
block: { name: 'block', description: 'Block the malicious action. Agent continues operating.' },
|
|
26
|
+
/** Isolate the agent — kill its active sessions. */
|
|
27
|
+
isolate: { name: 'isolate', description: 'Isolate the compromised agent. Terminate all sessions.' },
|
|
28
|
+
/** Alert only — log the incident but take no action. */
|
|
29
|
+
alert: { name: 'alert', description: 'Alert security team. No automated action.' },
|
|
30
|
+
/** Quarantine — block and preserve state for forensics. */
|
|
31
|
+
quarantine: { name: 'quarantine', description: 'Quarantine agent and preserve full state for investigation.' },
|
|
32
|
+
/** Rollback — revert to last known-good state. */
|
|
33
|
+
rollback: { name: 'rollback', description: 'Revert agent to last known-good configuration.' }
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const SEVERITY_TO_STRATEGY = {
|
|
37
|
+
critical: 'quarantine',
|
|
38
|
+
high: 'block',
|
|
39
|
+
medium: 'alert',
|
|
40
|
+
low: 'alert'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// =========================================================================
|
|
44
|
+
// IncidentResponse
|
|
45
|
+
// =========================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Automated incident response engine for AI agent security events.
|
|
49
|
+
*/
|
|
50
|
+
class IncidentResponse {
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} [options]
|
|
53
|
+
* @param {object} [options.strategyOverrides] - Override default severity→strategy mapping.
|
|
54
|
+
* @param {Function} [options.onIncident] - Callback when incident is created.
|
|
55
|
+
* @param {Function} [options.onAction] - Callback when response action is taken.
|
|
56
|
+
* @param {boolean} [options.autoRespond=true] - Automatically execute response strategy.
|
|
57
|
+
*/
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.strategies = { ...SEVERITY_TO_STRATEGY, ...(options.strategyOverrides || {}) };
|
|
60
|
+
this.onIncident = options.onIncident || null;
|
|
61
|
+
this.onAction = options.onAction || null;
|
|
62
|
+
this.autoRespond = options.autoRespond !== false;
|
|
63
|
+
|
|
64
|
+
/** @type {Array<object>} */
|
|
65
|
+
this.incidents = [];
|
|
66
|
+
/** @type {Array<object>} */
|
|
67
|
+
this.actions = [];
|
|
68
|
+
this.stats = { incidentsCreated: 0, actionsExecuted: 0, agentsIsolated: 0, actionsBlocked: 0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Handle a security event — create incident, determine strategy, execute response.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} event
|
|
75
|
+
* @param {string} event.type - Threat type (e.g., 'prompt_injection', 'ssrf', 'tool_poisoning').
|
|
76
|
+
* @param {string} event.severity - 'critical', 'high', 'medium', 'low'.
|
|
77
|
+
* @param {string} [event.agentId] - Affected agent ID.
|
|
78
|
+
* @param {string} [event.serverId] - Affected MCP server ID.
|
|
79
|
+
* @param {string} [event.description] - Human-readable description.
|
|
80
|
+
* @param {*} [event.evidence] - Raw evidence (tool call, input text, etc.).
|
|
81
|
+
* @returns {object} Incident record with response actions.
|
|
82
|
+
*/
|
|
83
|
+
handleEvent(event) {
|
|
84
|
+
const incidentId = crypto.randomBytes(8).toString('hex');
|
|
85
|
+
const strategy = this.strategies[event.severity] || 'alert';
|
|
86
|
+
const strategyInfo = RESPONSE_STRATEGIES[strategy] || RESPONSE_STRATEGIES.alert;
|
|
87
|
+
|
|
88
|
+
const incident = {
|
|
89
|
+
incidentId,
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
type: event.type,
|
|
92
|
+
severity: event.severity,
|
|
93
|
+
agentId: event.agentId || 'unknown',
|
|
94
|
+
serverId: event.serverId || null,
|
|
95
|
+
description: event.description || `${event.type} detected (${event.severity})`,
|
|
96
|
+
evidence: event.evidence ? JSON.stringify(event.evidence).substring(0, 2000) : null,
|
|
97
|
+
strategy: strategyInfo.name,
|
|
98
|
+
strategyDescription: strategyInfo.description,
|
|
99
|
+
status: 'open',
|
|
100
|
+
actions: [],
|
|
101
|
+
forensics: null
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Preserve forensic evidence
|
|
105
|
+
incident.forensics = {
|
|
106
|
+
capturedAt: Date.now(),
|
|
107
|
+
eventSnapshot: { ...event, evidence: incident.evidence },
|
|
108
|
+
contextHash: crypto.createHash('sha256').update(JSON.stringify(event)).digest('hex')
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
this.incidents.push(incident);
|
|
112
|
+
this.stats.incidentsCreated++;
|
|
113
|
+
|
|
114
|
+
// Bound incidents
|
|
115
|
+
if (this.incidents.length > 1000) this.incidents = this.incidents.slice(-1000);
|
|
116
|
+
|
|
117
|
+
// Notify
|
|
118
|
+
if (this.onIncident) {
|
|
119
|
+
try { this.onIncident(incident); } catch { /* ignore */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Auto-respond
|
|
123
|
+
if (this.autoRespond) {
|
|
124
|
+
this._executeStrategy(incident);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return incident;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get all open incidents.
|
|
132
|
+
* @returns {Array<object>}
|
|
133
|
+
*/
|
|
134
|
+
getOpenIncidents() {
|
|
135
|
+
return this.incidents.filter(i => i.status === 'open');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Close an incident.
|
|
140
|
+
* @param {string} incidentId
|
|
141
|
+
* @param {string} [resolution] - How it was resolved.
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
closeIncident(incidentId, resolution) {
|
|
145
|
+
const incident = this.incidents.find(i => i.incidentId === incidentId);
|
|
146
|
+
if (!incident) return false;
|
|
147
|
+
incident.status = 'closed';
|
|
148
|
+
incident.closedAt = Date.now();
|
|
149
|
+
incident.resolution = resolution || 'Manually closed.';
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generate an incident report.
|
|
155
|
+
* @param {string} [incidentId] - Specific incident, or null for summary.
|
|
156
|
+
* @returns {object}
|
|
157
|
+
*/
|
|
158
|
+
generateReport(incidentId) {
|
|
159
|
+
if (incidentId) {
|
|
160
|
+
const incident = this.incidents.find(i => i.incidentId === incidentId);
|
|
161
|
+
if (!incident) return null;
|
|
162
|
+
return {
|
|
163
|
+
title: `Incident Report: ${incident.incidentId}`,
|
|
164
|
+
incident,
|
|
165
|
+
timeline: incident.actions,
|
|
166
|
+
forensics: incident.forensics,
|
|
167
|
+
recommendation: this._getRemediation(incident)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Summary report
|
|
172
|
+
const open = this.incidents.filter(i => i.status === 'open');
|
|
173
|
+
const closed = this.incidents.filter(i => i.status === 'closed');
|
|
174
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
175
|
+
for (const i of this.incidents) bySeverity[i.severity] = (bySeverity[i.severity] || 0) + 1;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
title: 'Incident Summary Report',
|
|
179
|
+
totalIncidents: this.incidents.length,
|
|
180
|
+
open: open.length,
|
|
181
|
+
closed: closed.length,
|
|
182
|
+
bySeverity,
|
|
183
|
+
recentIncidents: this.incidents.slice(-10),
|
|
184
|
+
stats: { ...this.stats },
|
|
185
|
+
generatedAt: Date.now()
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get stats.
|
|
191
|
+
* @returns {object}
|
|
192
|
+
*/
|
|
193
|
+
getStats() {
|
|
194
|
+
return { ...this.stats, totalIncidents: this.incidents.length, openIncidents: this.incidents.filter(i => i.status === 'open').length };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
// Private
|
|
199
|
+
// -----------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
/** @private */
|
|
202
|
+
_executeStrategy(incident) {
|
|
203
|
+
const actions = [];
|
|
204
|
+
|
|
205
|
+
switch (incident.strategy) {
|
|
206
|
+
case 'quarantine':
|
|
207
|
+
actions.push({ action: 'isolate_agent', target: incident.agentId, timestamp: Date.now() });
|
|
208
|
+
actions.push({ action: 'preserve_forensics', evidenceHash: incident.forensics.contextHash, timestamp: Date.now() });
|
|
209
|
+
actions.push({ action: 'notify_soc', severity: incident.severity, timestamp: Date.now() });
|
|
210
|
+
this.stats.agentsIsolated++;
|
|
211
|
+
this.stats.actionsBlocked++;
|
|
212
|
+
break;
|
|
213
|
+
case 'block':
|
|
214
|
+
actions.push({ action: 'block_action', target: incident.agentId, timestamp: Date.now() });
|
|
215
|
+
actions.push({ action: 'notify_soc', severity: incident.severity, timestamp: Date.now() });
|
|
216
|
+
this.stats.actionsBlocked++;
|
|
217
|
+
break;
|
|
218
|
+
case 'isolate':
|
|
219
|
+
actions.push({ action: 'isolate_agent', target: incident.agentId, timestamp: Date.now() });
|
|
220
|
+
this.stats.agentsIsolated++;
|
|
221
|
+
break;
|
|
222
|
+
case 'alert':
|
|
223
|
+
actions.push({ action: 'log_alert', severity: incident.severity, timestamp: Date.now() });
|
|
224
|
+
break;
|
|
225
|
+
case 'rollback':
|
|
226
|
+
actions.push({ action: 'rollback_config', target: incident.agentId, timestamp: Date.now() });
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
incident.actions = actions;
|
|
231
|
+
this.stats.actionsExecuted += actions.length;
|
|
232
|
+
|
|
233
|
+
for (const action of actions) {
|
|
234
|
+
this.actions.push({ ...action, incidentId: incident.incidentId });
|
|
235
|
+
if (this.onAction) {
|
|
236
|
+
try { this.onAction(action, incident); } catch { /* ignore */ }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** @private */
|
|
242
|
+
_getRemediation(incident) {
|
|
243
|
+
const remediations = {
|
|
244
|
+
prompt_injection: 'Review and harden system prompt. Add input validation. Enable semantic isolation.',
|
|
245
|
+
ssrf: 'Block private IP ranges. Validate all URLs against allowlist. Apply CVE-2026-26118 patches.',
|
|
246
|
+
tool_poisoning: 'Re-attest tool definitions. Pin tool versions. Enable full-schema scanning.',
|
|
247
|
+
data_exfiltration: 'Enable DLP scanning on outbound calls. Review tool permissions. Add URL allowlists.',
|
|
248
|
+
config_poisoning: 'Audit .claude/ and .cursor/ config files. Block ANTHROPIC_BASE_URL overrides.',
|
|
249
|
+
role_hijack: 'Strengthen system prompt. Enable prompt hardening. Add role boundary monitoring.',
|
|
250
|
+
memory_poisoning: 'Clear agent memory. Enable memory write validation. Add persistence monitoring.',
|
|
251
|
+
cross_agent_injection: 'Enable cross-server isolation. Add message signing between agents.',
|
|
252
|
+
};
|
|
253
|
+
return remediations[incident.type] || 'Review security configuration and enable additional detection layers.';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// =========================================================================
|
|
258
|
+
// EXPORTS
|
|
259
|
+
// =========================================================================
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
IncidentResponse,
|
|
263
|
+
RESPONSE_STRATEGIES,
|
|
264
|
+
SEVERITY_TO_STRATEGY
|
|
265
|
+
};
|
|
@@ -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
|
+
};
|