agentgate-runtime-control 2.13.8

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/LICENSE +21 -0
  3. package/README.md +534 -0
  4. package/SECURITY.md +19 -0
  5. package/bin/agentgate.js +303 -0
  6. package/docs/case-study-technical-validation.md +25 -0
  7. package/docs/case-study-template.md +37 -0
  8. package/docs/data-protection.md +49 -0
  9. package/docs/design-partner-checklist.md +32 -0
  10. package/docs/design-partner-kit.md +51 -0
  11. package/docs/design-partner-rollout.md +35 -0
  12. package/docs/design-partner.md +67 -0
  13. package/docs/external-security-review-test-pack.md +132 -0
  14. package/docs/external-security-review.md +33 -0
  15. package/docs/incident-response.md +54 -0
  16. package/docs/integration-matrix.md +17 -0
  17. package/docs/managed-postgres-acceptance-test.md +138 -0
  18. package/docs/marketing-plan.md +33 -0
  19. package/docs/observability-alerting.md +44 -0
  20. package/docs/outreach.md +26 -0
  21. package/docs/partner-intake-template.md +26 -0
  22. package/docs/performance-baseline.md +23 -0
  23. package/docs/performance.md +27 -0
  24. package/docs/pricing.md +53 -0
  25. package/docs/production-deployment.md +70 -0
  26. package/docs/production-quickstart.md +58 -0
  27. package/docs/production-readiness.md +29 -0
  28. package/docs/quickstart.md +115 -0
  29. package/docs/release-checklist.md +33 -0
  30. package/docs/security-hardening-release-report.md +69 -0
  31. package/docs/threat-model.md +47 -0
  32. package/docs/website-copy.md +44 -0
  33. package/examples/basic.mjs +14 -0
  34. package/examples/control-plane.mjs +17 -0
  35. package/examples/design-partner-refund.mjs +33 -0
  36. package/examples/design-partner-shadow.mjs +27 -0
  37. package/examples/mcp-gateway.mjs +26 -0
  38. package/examples/policy-bundle.mjs +18 -0
  39. package/examples/refund-agent.mjs +20 -0
  40. package/examples/runtime.mjs +12 -0
  41. package/package.json +49 -0
  42. package/schema/postgres.sql +17 -0
  43. package/src/admin-rbac.js +3 -0
  44. package/src/agentgate.js +85 -0
  45. package/src/approval.js +30 -0
  46. package/src/attack-lab.js +94 -0
  47. package/src/auth.js +27 -0
  48. package/src/behavior.js +146 -0
  49. package/src/control-plane.js +215 -0
  50. package/src/egress-guard.js +132 -0
  51. package/src/event-bus.js +10 -0
  52. package/src/identity.js +109 -0
  53. package/src/index.js +44 -0
  54. package/src/local-experience.js +46 -0
  55. package/src/mcp-gateway.js +383 -0
  56. package/src/mcp-scanner.js +45 -0
  57. package/src/middleware.js +17 -0
  58. package/src/multi-tenant.js +29 -0
  59. package/src/observability.js +395 -0
  60. package/src/oidc.js +38 -0
  61. package/src/persistent-store.js +56 -0
  62. package/src/policy-builder.js +74 -0
  63. package/src/policy-bundles.js +17 -0
  64. package/src/policy-engine.js +67 -0
  65. package/src/policy-packs.js +115 -0
  66. package/src/policy-registry.js +58 -0
  67. package/src/postgres-adapter.js +76 -0
  68. package/src/runtime.js +138 -0
  69. package/src/saas.js +67 -0
  70. package/src/security-report.js +42 -0
  71. package/src/security-validation.js +92 -0
  72. package/src/shadow-mode.js +47 -0
  73. package/src/telemetry.js +28 -0
  74. package/src/webhook-delivery.js +70 -0
  75. package/standalone.html +86 -0
@@ -0,0 +1,395 @@
1
+ /**
2
+ * AgentGate Observability + Cost Control + Security-Aware Analytics.
3
+ * Deterministic analytics over runtime runs; no LLM is used for scoring or authorization.
4
+ */
5
+
6
+ import { analyzeBehavior } from './behavior.js';
7
+
8
+ export const COST_DECISIONS = Object.freeze({ ALLOW: 'ALLOW', ASK: 'ASK', BLOCK: 'BLOCK' });
9
+
10
+ const DEFAULT_PRICING = Object.freeze({});
11
+
12
+ export class PricingRegistry {
13
+ constructor(pricing = {}) { this.pricing = { ...DEFAULT_PRICING, ...normalizePricingMap(pricing) }; }
14
+ set(model, price = {}) {
15
+ const name = String(model || '').trim();
16
+ if (!name) throw new TypeError('Model name is required');
17
+ this.pricing[name] = normalizePrice(price);
18
+ return this.pricing[name];
19
+ }
20
+ get(model) { return this.pricing[String(model || '')] || null; }
21
+ list() { return structuredCloneSafe(this.pricing); }
22
+ }
23
+
24
+ export function calculateCost(input = {}, pricing = {}) {
25
+ const usage = normalizeUsage(input.usage || input);
26
+ const model = input.model || input.usage?.model || null;
27
+ const price = input.price || pricing[model] || null;
28
+ if (!price) return { currency: 'USD', total: 0, known: false, model, usage };
29
+ const inputRate = Number(price.inputPer1M ?? price.input ?? 0);
30
+ const outputRate = Number(price.outputPer1M ?? price.output ?? 0);
31
+ const cachedRate = Number(price.cachedInputPer1M ?? price.cachedInput ?? 0);
32
+ const inputTokens = Math.max(0, usage.inputTokens - usage.cachedInputTokens);
33
+ const cachedTokens = Math.max(0, usage.cachedInputTokens);
34
+ const outputTokens = Math.max(0, usage.outputTokens);
35
+ const total = (inputTokens / 1_000_000) * inputRate +
36
+ (cachedTokens / 1_000_000) * cachedRate +
37
+ (outputTokens / 1_000_000) * outputRate;
38
+ return {
39
+ currency: price.currency || 'USD',
40
+ total: roundMoney(total),
41
+ known: true,
42
+ model,
43
+ usage,
44
+ rates: { inputPer1M: inputRate, outputPer1M: outputRate, cachedInputPer1M: cachedRate }
45
+ };
46
+ }
47
+
48
+ export class CostController {
49
+ constructor(options = {}) {
50
+ this.budgets = normalizeBudgets(options.budgets || {});
51
+ this.events = [];
52
+ this.maxEvents = options.maxEvents || 5000;
53
+ }
54
+ configure(budgets = {}) { this.budgets = normalizeBudgets(budgets); return this.budgets; }
55
+ record(run, cost = {}) {
56
+ const amount = Number(cost.total || 0);
57
+ const event = {
58
+ runId: run.id,
59
+ tenantId: run.tenantId || null,
60
+ agent: run.request?.agent || 'unknown',
61
+ cost: amount,
62
+ currency: cost.currency || 'USD',
63
+ at: run.createdAt || new Date().toISOString()
64
+ };
65
+ this.events.unshift(event);
66
+ if (this.events.length > this.maxEvents) this.events.pop();
67
+ return event;
68
+ }
69
+ evaluate({ tenantId = null, agent = null, estimatedCost = 0, now = new Date(), history = null } = {}) {
70
+ const amount = Math.max(0, Number(estimatedCost) || 0);
71
+ const daily = this.spent({ tenantId, agent, period: 'daily', now, history }) + amount;
72
+ const monthly = this.spent({ tenantId, agent, period: 'monthly', now, history }) + amount;
73
+ const d = this.budgets.daily;
74
+ const m = this.budgets.monthly;
75
+ if ((d.hard > 0 && daily > d.hard) || (m.hard > 0 && monthly > m.hard)) {
76
+ return { decision: COST_DECISIONS.BLOCK, reason: 'Cost budget would be exceeded', daily, monthly };
77
+ }
78
+ if ((d.ask > 0 && daily > d.ask) || (m.ask > 0 && monthly > m.ask)) {
79
+ return { decision: COST_DECISIONS.ASK, reason: 'Cost approval threshold reached', daily, monthly };
80
+ }
81
+ return { decision: COST_DECISIONS.ALLOW, reason: 'Cost budget within configured thresholds', daily, monthly };
82
+ }
83
+ spent({ tenantId = null, agent = null, period = 'daily', now = new Date(), history = null } = {}) {
84
+ const date = now instanceof Date ? now : new Date(now);
85
+ const start = period === 'monthly'
86
+ ? new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1))
87
+ : new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
88
+ if (Array.isArray(history)) {
89
+ return history.filter(r => new Date(r.createdAt) >= start && (!tenantId || r.tenantId === tenantId) && (!agent || (r.request?.agent || 'unknown') === agent))
90
+ .reduce((sum, r) => sum + Number(r.cost?.total || 0), 0);
91
+ }
92
+ return this.events.filter(e => new Date(e.at) >= start && (!tenantId || e.tenantId === tenantId) && (!agent || e.agent === agent))
93
+ .reduce((sum, e) => sum + Number(e.cost || 0), 0);
94
+ }
95
+ snapshot({ tenantId = null, agent = null, now = new Date(), history = null } = {}) {
96
+ const daily = this.spent({ tenantId, agent, period: 'daily', now, history });
97
+ const monthly = this.spent({ tenantId, agent, period: 'monthly', now, history });
98
+ return {
99
+ currency: 'USD',
100
+ daily: roundMoney(daily),
101
+ monthly: roundMoney(monthly),
102
+ budgets: structuredCloneSafe(this.budgets),
103
+ remaining: {
104
+ daily: remaining(this.budgets.daily.hard, daily),
105
+ monthly: remaining(this.budgets.monthly.hard, monthly)
106
+ }
107
+ };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Full deterministic observability report. The report intentionally joins
113
+ * runtime, cost, security and behavior data instead of treating traces as logs.
114
+ */
115
+ export function analyzeObservability(runs = [], options = {}) {
116
+ const list = Array.isArray(runs) ? runs : [];
117
+ const latencies = list.map(r => Number(r.durationMs)).filter(Number.isFinite).sort((a, b) => a - b);
118
+ const decisions = countBy(list, r => r.decision || 'UNKNOWN');
119
+ const tools = groupStats(list, r => r.request?.tool || r.request?.action || 'unknown');
120
+ const agents = groupStats(list, r => r.request?.agent || 'unknown');
121
+ const models = groupStats(list, r => r.request?.model || r.cost?.model || 'unknown');
122
+ const security = {
123
+ blocked: decisions.BLOCK || 0,
124
+ approvalRequired: decisions.ASK || 0,
125
+ egressBlocked: list.filter(r => r.egressBlocked).length,
126
+ egressRedacted: list.filter(r => r.egress?.action === 'REDACT').length,
127
+ highRisk: list.filter(r => Number(r.risk) >= 70).length,
128
+ criticalRisk: list.filter(r => Number(r.risk) >= 90).length
129
+ };
130
+ const costs = list.map(r => Number(r.cost?.total || 0)).filter(Number.isFinite);
131
+ const knownCosts = list.filter(r => r.cost?.known);
132
+ const behavior = analyzeBehavior(list, options.behavior || {});
133
+ const costAnalytics = analyzeCostAnalytics(list, options);
134
+ const efficiency = analyzeAgentEfficiency(list, options);
135
+ const forecast = forecastCost(list, options);
136
+ const correlation = correlateBehaviorCostSecurity(list, behavior);
137
+ const traces = list.map(buildUnifiedTrace);
138
+
139
+ return {
140
+ runs: list.length,
141
+ decisions,
142
+ successRate: list.length ? round((list.filter(r => r.executed && !r.error && !r.egressBlocked).length / list.length) * 100, 2) : 0,
143
+ errorRate: list.length ? round((list.filter(r => r.error).length / list.length) * 100, 2) : 0,
144
+ latency: percentileSummary(latencies),
145
+ security,
146
+ cost: {
147
+ total: roundMoney(costs.reduce((a, b) => a + b, 0)),
148
+ average: costs.length ? roundMoney(costs.reduce((a, b) => a + b, 0) / costs.length) : 0,
149
+ knownRuns: knownCosts.length,
150
+ unknownRuns: list.length - knownCosts.length,
151
+ currency: options.currency || 'USD'
152
+ },
153
+ tools,
154
+ agents,
155
+ models,
156
+ efficiency,
157
+ forecast,
158
+ behavior,
159
+ correlation,
160
+ costAnalytics,
161
+ traces,
162
+ generatedAt: new Date().toISOString()
163
+ };
164
+ }
165
+
166
+ /** Build the security-aware trace shown by AgentGate instead of a generic log trace. */
167
+ export function buildUnifiedTrace(run = {}) {
168
+ const request = run.request || {};
169
+ const events = [];
170
+ const at = run.createdAt || new Date().toISOString();
171
+ const add = (stage, detail, status = 'INFO') => events.push({ stage, status, detail });
172
+
173
+ add('LLM', { agent: request.agent || 'unknown', model: request.model || run.cost?.model || 'unknown', usage: run.cost?.usage || request.usage || null });
174
+ add('TOOL', { tool: request.tool || request.action || 'unknown', action: request.action || request.tool || 'unknown', amount: request.amount ?? null });
175
+ add('POLICY', { decision: run.decision || 'UNKNOWN', risk: Number(run.risk || 0), reason: run.reason || null });
176
+
177
+ if (run.approvalId || run.approval || run.status === 'pending_approval' || run.decision === 'ASK') {
178
+ const approvalStatus = run.approval?.status || (run.status === 'pending_approval' ? 'PENDING' : 'REQUIRED');
179
+ add('HUMAN_APPROVAL', { approvalId: run.approvalId || null, status: approvalStatus }, approvalStatus === 'APPROVED' ? 'PASS' : 'CONTROL');
180
+ }
181
+
182
+ if (run.executed) add('TOOL_EXECUTION', { status: run.error ? 'ERROR' : 'EXECUTED', latencyMs: run.durationMs ?? null });
183
+ if (run.egress) add('RESPONSE_EGRESS', { action: run.egress.action, findings: run.egress.findings || [], risk: run.egress.risk || 0 }, run.egress.action === 'BLOCK' ? 'BLOCK' : run.egress.action === 'REDACT' ? 'CONTROL' : 'PASS');
184
+ if (run.error) add('ERROR', { message: run.error }, 'ERROR');
185
+
186
+ const behaviorStatus = Number(run.behaviorRisk || run.risk || 0) >= 70 ? 'HIGH_RISK' : 'NORMAL';
187
+ add('BEHAVIOR', { status: behaviorStatus, risk: Number(run.behaviorRisk || 0) || null });
188
+ add('COST', { amount: Number(run.cost?.total || 0), currency: run.cost?.currency || 'USD', known: Boolean(run.cost?.known), budget: run.cost?.budget || null });
189
+
190
+ return {
191
+ runId: run.id || null,
192
+ agent: request.agent || 'unknown',
193
+ model: request.model || run.cost?.model || null,
194
+ tenantId: run.tenantId || null,
195
+ decision: run.decision || 'UNKNOWN',
196
+ risk: Number(run.risk || 0),
197
+ status: run.status || (run.executed ? 'executed' : 'decided'),
198
+ latencyMs: Number.isFinite(Number(run.durationMs)) ? Number(run.durationMs) : null,
199
+ cost: run.cost || { total: 0, known: false },
200
+ security: {
201
+ policyDecision: run.decision || 'UNKNOWN',
202
+ egress: run.egress || null,
203
+ egressBlocked: Boolean(run.egressBlocked),
204
+ approvalId: run.approvalId || null,
205
+ risk: Number(run.risk || 0)
206
+ },
207
+ events,
208
+ createdAt: at
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Deterministic Agent Efficiency score. Formula is deliberately transparent:
214
+ * success 30%, error-free reliability 20%, tool economy 15%, latency 15%,
215
+ * control friction 10%, cost efficiency 10%. No model/LLM judgment is used.
216
+ */
217
+ export function calculateAgentEfficiency(runs = [], agent = null, options = {}) {
218
+ const scoped = (runs || []).filter(r => !agent || (r.request?.agent || 'unknown') === agent);
219
+ if (!scoped.length) return { agent: agent || 'unknown', score: 0, status: 'NO_DATA', breakdown: {}, metrics: {} };
220
+ const success = ratio(scoped, r => r.executed && !r.error && !r.egressBlocked) * 100;
221
+ const reliability = (1 - ratio(scoped, r => Boolean(r.error))) * 100;
222
+ const taskCalls = averageToolCallsPerTask(scoped);
223
+ const toolEconomy = scoreLowerIsBetter(taskCalls, Number(options.idealToolCalls || 4), Number(options.maxToolCalls || 12));
224
+ const latencies = scoped.map(r => Number(r.durationMs)).filter(Number.isFinite).sort((a,b)=>a-b);
225
+ const latencyP50 = percentileSummary(latencies).p50;
226
+ const latencyScore = scoreLowerIsBetter(latencyP50, Number(options.idealLatencyMs || 1000), Number(options.maxLatencyMs || 10000));
227
+ const approvalRate = ratio(scoped, r => r.decision === 'ASK') * 100;
228
+ const blockedRate = ratio(scoped, r => r.decision === 'BLOCK') * 100;
229
+ const controlFriction = clamp(100 - (approvalRate * 0.5) - (blockedRate * 2), 0, 100);
230
+ const successful = scoped.filter(r => r.executed && !r.error && !r.egressBlocked);
231
+ const successfulTaskIds = new Set(successful.map(r => taskIdOf(r)));
232
+ const taskCost = new Map();
233
+ for (const r of successful) taskCost.set(taskIdOf(r), (taskCost.get(taskIdOf(r)) || 0) + Number(r.cost?.total || 0));
234
+ const totalCost = [...taskCost.values()].reduce((s,r)=>s+r,0);
235
+ const costPerSuccess = successfulTaskIds.size ? totalCost / successfulTaskIds.size : 0;
236
+ const peerCosts = (runs || []).filter(r => r.executed && !r.error && !r.egressBlocked).map(r => Number(r.cost?.total||0)).filter(Number.isFinite).sort((a,b)=>a-b);
237
+ const peerMedian = peerCosts.length ? percentile(peerCosts, .5) : costPerSuccess;
238
+ const costEfficiency = peerMedian > 0 ? clamp(100 * (peerMedian / Math.max(costPerSuccess, 0.000001)), 0, 100) : (costPerSuccess === 0 ? 100 : 50);
239
+ const score = round(success*0.30 + reliability*0.20 + toolEconomy*0.15 + latencyScore*0.15 + controlFriction*0.10 + costEfficiency*0.10, 1);
240
+ return {
241
+ agent: agent || scoped[0]?.request?.agent || 'unknown',
242
+ score,
243
+ status: score >= 80 ? 'HEALTHY' : score >= 60 ? 'ATTENTION' : 'DEGRADED',
244
+ breakdown: { success: round(success), reliability: round(reliability), toolEconomy: round(toolEconomy), latency: round(latencyScore), controlFriction: round(controlFriction), costEfficiency: round(costEfficiency) },
245
+ metrics: { runs: scoped.length, successRate: round(success), successfulTasks: successfulTaskIds.size, avgToolCalls: round(taskCalls,2), p50LatencyMs: latencyP50, approvalRate: round(approvalRate), blockedRate: round(blockedRate), retryRate: round(ratio(scoped, r => Number(r.request?.retryCount || 0) > 0)*100), costPerSuccessfulTask: roundMoney(costPerSuccess) }
246
+ };
247
+ }
248
+
249
+ export function analyzeAgentEfficiency(runs = [], options = {}) {
250
+ const agents = [...new Set(runs.map(r => r.request?.agent || 'unknown'))];
251
+ const byAgent = Object.fromEntries(agents.map(agent => [agent, calculateAgentEfficiency(runs, agent, options)]));
252
+ return { byAgent, overall: calculateAgentEfficiency(runs, null, options) };
253
+ }
254
+
255
+ /** Forecast uses deterministic month-to-date daily run-rate; no probabilistic model. */
256
+ export function forecastCost(runs = [], options = {}) {
257
+ const now = options.now ? new Date(options.now) : new Date();
258
+ const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
259
+ const nextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth()+1, 1));
260
+ const daysInMonth = Math.round((nextMonth - monthStart) / 86400000);
261
+ const elapsedDays = Math.max(1, Math.min(daysInMonth, Math.floor((now - monthStart) / 86400000) + 1));
262
+ const monthly = runs.filter(r => new Date(r.createdAt || 0) >= monthStart).reduce((s,r)=>s+Number(r.cost?.total||0),0);
263
+ const dailyRate = monthly / elapsedDays;
264
+ const forecast = dailyRate * daysInMonth;
265
+ const budget = Number(options.monthlyBudget || 0);
266
+ return { currency: options.currency || 'USD', monthToDate: roundMoney(monthly), elapsedDays, daysInMonth, dailyRunRate: roundMoney(dailyRate), forecast: roundMoney(forecast), budget: budget > 0 ? budget : null, varianceVsBudget: budget > 0 ? roundMoney(forecast - budget) : null, method: 'month-to-date daily run-rate' };
267
+ }
268
+
269
+ /** Correlates behavior findings with cost, tool volume and security outcomes. */
270
+ export function correlateBehaviorCostSecurity(runs = [], behavior = null) {
271
+ const report = behavior || analyzeBehavior(runs);
272
+ const anomalyIds = new Set((report.findings || []).map(f => f.runId).filter(Boolean));
273
+ const anomalous = runs.filter(r => anomalyIds.has(r.id));
274
+ const normal = runs.filter(r => !anomalyIds.has(r.id));
275
+ const cost = xs => xs.reduce((s,r)=>s+Number(r.cost?.total||0),0);
276
+ const avg = (xs, fn) => xs.length ? fn(xs)/xs.length : 0;
277
+ const toolRate = xs => xs.length;
278
+ const blocked = anomalous.filter(r => r.decision === 'BLOCK').length;
279
+ const asked = anomalous.filter(r => r.decision === 'ASK').length;
280
+ const anomalyCost = cost(anomalous);
281
+ const normalCost = cost(normal);
282
+ return {
283
+ anomalyRuns: anomalous.length,
284
+ normalRuns: normal.length,
285
+ anomalyCost: roundMoney(anomalyCost),
286
+ normalCost: roundMoney(normalCost),
287
+ avgAnomalyCost: roundMoney(avg(anomalous, cost)),
288
+ avgNormalCost: roundMoney(avg(normal, cost)),
289
+ costDeltaPercent: normalCost > 0 && anomalous.length ? round(((anomalyCost / anomalous.length) / Math.max(normalCost / Math.max(normal.length,1),0.000001) - 1) * 100) : null,
290
+ blockedAnomalyRuns: blocked,
291
+ approvalAnomalyRuns: asked,
292
+ toolCallDelta: anomalous.length && normal.length ? round((toolRate(anomalous) / anomalous.length - toolRate(normal) / normal.length), 2) : null,
293
+ securityCorrelation: {
294
+ status: blocked > 0 || asked > 0 ? 'CONTROLLED_ANOMALY' : anomalous.length ? 'BEHAVIOR_ANOMALY' : 'NORMAL',
295
+ findings: (report.findings || []).slice(0, 20).map(f => ({ id:f.id, title:f.title, risk:f.risk, runId:f.runId, agent:f.agent }))
296
+ }
297
+ };
298
+ }
299
+
300
+ export function analyzeCostAnalytics(runs = [], options = {}) {
301
+ const group = (keyFn) => {
302
+ const map = new Map();
303
+ for (const r of runs) {
304
+ const key = keyFn(r) || 'unknown';
305
+ const x = map.get(key) || { runs:0, cost:0, blocked:0, approvals:0, errors:0, tokens:0 };
306
+ x.runs++;
307
+ x.cost += Number(r.cost?.total||0);
308
+ x.blocked += r.decision === 'BLOCK' ? 1 : 0;
309
+ x.approvals += r.decision === 'ASK' ? 1 : 0;
310
+ x.errors += r.error ? 1 : 0;
311
+ x.tokens += Number(r.cost?.usage?.totalTokens || 0);
312
+ map.set(key,x);
313
+ }
314
+ return Object.fromEntries([...map].map(([k,v])=>[k,{...v,cost:roundMoney(v.cost),avgCost:roundMoney(v.cost/v.runs)}]));
315
+ };
316
+ const successful = runs.filter(r=>r.executed&&!r.error&&!r.egressBlocked);
317
+ const securityCost = {
318
+ blockedCalls: roundMoney(runs.filter(r=>r.decision==='BLOCK').reduce((s,r)=>s+Number(r.cost?.total||0),0)),
319
+ approvalCalls: roundMoney(runs.filter(r=>r.decision==='ASK').reduce((s,r)=>s+Number(r.cost?.total||0),0)),
320
+ attackTests: roundMoney(runs.filter(r=>r.request?.attackTest===true || r.attackTest===true).reduce((s,r)=>s+Number(r.cost?.total||0),0)),
321
+ egressBlocks: roundMoney(runs.filter(r=>r.egressBlocked).reduce((s,r)=>s+Number(r.cost?.total||0),0))
322
+ };
323
+ return {
324
+ byAgent: group(r=>r.request?.agent),
325
+ byModel: group(r=>r.request?.model || r.cost?.model),
326
+ byTool: group(r=>r.request?.tool || r.request?.action),
327
+ byTenant: group(r=>r.tenantId),
328
+ byCustomer: group(r=>r.request?.customerId || r.request?.userId || r.request?.identity?.userId),
329
+ byAction: group(r=>r.request?.action || r.request?.tool),
330
+ securityCost,
331
+ successfulTaskCost: successful.length ? roundMoney(successful.reduce((s,r)=>s+Number(r.cost?.total||0),0)/successful.length) : 0,
332
+ totalTokens: runs.reduce((s,r)=>s+Number(r.cost?.usage?.totalTokens||0),0),
333
+ currency: options.currency || 'USD'
334
+ };
335
+ }
336
+
337
+ export function createObservability(options = {}) {
338
+ const pricing = new PricingRegistry(options.pricing || {});
339
+ const costControl = options.costControl || new CostController({ budgets: options.budgets, maxEvents: options.maxCostEvents });
340
+ const recordedRuns = new Set();
341
+ function enrichRun(run = {}) {
342
+ const usage = run.usage || run.request?.usage || null;
343
+ const model = run.model || run.request?.model || usage?.model || null;
344
+ const cost = calculateCost({ model, usage, price: run.price }, pricing.list());
345
+ run.cost = { ...run.cost, ...cost };
346
+ if (cost.known && run.id && !recordedRuns.has(run.id)) { costControl.record(run, cost); recordedRuns.add(run.id); }
347
+ return run;
348
+ }
349
+ return {
350
+ pricing,
351
+ costControl,
352
+ enrichRun,
353
+ analyze: runs => analyzeObservability(runs, options),
354
+ trace: run => buildUnifiedTrace(run),
355
+ efficiency: (runs, opts) => analyzeAgentEfficiency(runs, opts || options.efficiency),
356
+ forecast: (runs, opts) => forecastCost(runs, opts || options.forecast),
357
+ correlation: runs => correlateBehaviorCostSecurity(runs),
358
+ costAnalytics: (runs, opts) => analyzeCostAnalytics(runs, opts || {})
359
+ };
360
+ }
361
+
362
+ function normalizePricingMap(input) { return Object.fromEntries(Object.entries(input || {}).map(([k,v]) => [k, normalizePrice(v)])); }
363
+ function normalizePrice(price = {}) { return { currency: price.currency || 'USD', inputPer1M: Number(price.inputPer1M ?? price.input ?? 0), outputPer1M: Number(price.outputPer1M ?? price.output ?? 0), cachedInputPer1M: Number(price.cachedInputPer1M ?? price.cachedInput ?? 0) }; }
364
+ function normalizeUsage(usage = {}) {
365
+ const inputTokens = Number(usage.inputTokens ?? usage.promptTokens ?? usage.input_tokens ?? 0) || 0;
366
+ const outputTokens = Number(usage.outputTokens ?? usage.completionTokens ?? usage.output_tokens ?? 0) || 0;
367
+ const cachedInputTokens = Number(usage.cachedInputTokens ?? usage.cacheReadTokens ?? usage.cached_input_tokens ?? 0) || 0;
368
+ return { inputTokens, outputTokens, cachedInputTokens, totalTokens: Number(usage.totalTokens ?? usage.total_tokens ?? inputTokens + outputTokens) || 0 };
369
+ }
370
+ function normalizeBudgets(input = {}) { const normalize = x => ({ ask: Math.max(0, Number(x?.ask ?? 0) || 0), hard: Math.max(0, Number(x?.hard ?? x?.block ?? 0) || 0) }); return { daily: normalize(input.daily), monthly: normalize(input.monthly) }; }
371
+ function remaining(limit, spent) { return limit > 0 ? roundMoney(Math.max(0, limit - spent)) : null; }
372
+ function countBy(items, fn) { return items.reduce((m, x) => { const k = fn(x); m[k] = (m[k] || 0) + 1; return m; }, {}); }
373
+ function groupStats(items, keyFn) {
374
+ const map = new Map();
375
+ for (const r of items) {
376
+ const key = keyFn(r); const x = map.get(key) || { runs:0, blocked:0, asked:0, errors:0, cost:0, latency:[] };
377
+ x.runs++; if (r.decision === 'BLOCK') x.blocked++; if (r.decision === 'ASK') x.asked++; if (r.error) x.errors++;
378
+ x.cost += Number(r.cost?.total || 0); if (Number.isFinite(Number(r.durationMs))) x.latency.push(Number(r.durationMs)); map.set(key,x);
379
+ }
380
+ return Object.fromEntries([...map].map(([k,v])=>[k,{runs:v.runs,blocked:v.blocked,asked:v.asked,errors:v.errors,cost:roundMoney(v.cost),latency:percentileSummary(v.latency.sort((a,b)=>a-b))}]));
381
+ }
382
+ function percentileSummary(values) { if (!values.length) return {count:0,avgMs:0,p50:0,p95:0,p99:0,maxMs:0}; const sum=values.reduce((a,b)=>a+b,0); return {count:values.length,avgMs:round(sum/values.length,3),p50:percentile(values,.5),p95:percentile(values,.95),p99:percentile(values,.99),maxMs:values[values.length-1]}; }
383
+ function percentile(values,p) { const i=(values.length-1)*p,lo=Math.floor(i),hi=Math.ceil(i); return lo===hi?values[lo]:round(values[lo]+(values[hi]-values[lo])*(i-lo),3); }
384
+ function ratio(items,predicate){ return items.length ? items.filter(predicate).length/items.length : 0; }
385
+ function clamp(n,min,max){return Math.max(min,Math.min(max,n));}
386
+ function scoreLowerIsBetter(value,ideal,max){ if(!Number.isFinite(value)||value<=ideal)return 100; if(value>=max)return 0; return 100*(1-(value-ideal)/(max-ideal)); }
387
+ function taskIdOf(r){ return r.request?.context?.taskId || r.request?.taskId || r.request?.sessionId || r.id; }
388
+ function averageToolCallsPerTask(runs){
389
+ const tasks=new Map();
390
+ for(const r of runs){const id=taskIdOf(r); tasks.set(id,(tasks.get(id)||0)+1);}
391
+ return tasks.size ? [...tasks.values()].reduce((a,b)=>a+b,0)/tasks.size : 0;
392
+ }
393
+ function round(n,d=2){const p=10**d;return Math.round((Number(n)||0)*p)/p;}
394
+ function roundMoney(n){return round(n,6);}
395
+ function structuredCloneSafe(v){try{return structuredClone(v);}catch{return JSON.parse(JSON.stringify(v));}}
package/src/oidc.js ADDED
@@ -0,0 +1,38 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ export function mapOIDCClaims(claims={}, options={}) {
4
+ const rolesClaim=options.rolesClaim||'roles'; const tenantClaim=options.tenantClaim||'tenant_id'; const subjectClaim=options.subjectClaim||'sub';
5
+ return { userId:claims[subjectClaim]||null, tenantId:claims[tenantClaim]||null, roles:Array.isArray(claims[rolesClaim])?claims[rolesClaim]:[], attributes:{issuer:claims.iss||null,email:claims.email||null}, claims };
6
+ }
7
+ export function validateOIDCIdentity(identity, tenantId){ return Boolean(identity?.userId&&identity?.tenantId&&identity.tenantId===tenantId); }
8
+
9
+ export function decodeJWT(token) {
10
+ const parts = String(token || '').split('.');
11
+ if (parts.length !== 3) throw new Error('Invalid JWT format');
12
+ const decode = value => JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
13
+ return { header: decode(parts[0]), payload: decode(parts[1]), signingInput: `${parts[0]}.${parts[1]}`, signature: Buffer.from(parts[2], 'base64url') };
14
+ }
15
+
16
+ export function verifyOIDCJWT(token, options = {}) {
17
+ const { header, payload, signingInput, signature } = decodeJWT(token);
18
+ const algorithms = options.algorithms || ['RS256', 'ES256', 'HS256'];
19
+ if (!algorithms.includes(header.alg)) throw new Error('JWT algorithm is not allowed');
20
+ if (options.issuer && payload.iss !== options.issuer) throw new Error('JWT issuer mismatch');
21
+ if (options.audience) {
22
+ const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
23
+ if (!audiences.includes(options.audience)) throw new Error('JWT audience mismatch');
24
+ }
25
+ const now = Math.floor(Date.now() / 1000);
26
+ const tolerance = Math.max(0, Number(options.clockTolerance || 0));
27
+ if (payload.exp != null && now >= Number(payload.exp) + tolerance) throw new Error('JWT expired');
28
+ if (payload.nbf != null && now < Number(payload.nbf) - tolerance) throw new Error('JWT not active');
29
+ if (options.maxTokenAge != null && payload.iat != null && now - Number(payload.iat) > Number(options.maxTokenAge) + tolerance) throw new Error('JWT too old');
30
+ const key = options.publicKey || options.secret;
31
+ if (!key) throw new Error('JWT verification key is required');
32
+ let valid = false;
33
+ if (header.alg === 'HS256') { const expected = crypto.createHmac('sha256', key).update(signingInput).digest(); valid = signature.length === expected.length && crypto.timingSafeEqual(signature, expected); }
34
+ else if (header.alg === 'RS256') valid = crypto.verify('RSA-SHA256', Buffer.from(signingInput), key, signature);
35
+ else if (header.alg === 'ES256') { if (signature.length !== 64) throw new Error('Invalid ES256 signature'); const r = signature.subarray(0, 32); const sPart = signature.subarray(32); const int = b => { let x = Buffer.from(b); while (x.length > 1 && x[0] === 0) x = x.subarray(1); if (x[0] & 0x80) x = Buffer.concat([Buffer.from([0]), x]); return Buffer.concat([Buffer.from([0x02, x.length]), x]); }; const body = Buffer.concat([int(r), int(sPart)]); const der = Buffer.concat([Buffer.from([0x30, body.length]), body]); valid = crypto.verify('sha256', Buffer.from(signingInput), { key, dsaEncoding: 'der' }, der); }
36
+ if (!valid) throw new Error('JWT signature verification failed');
37
+ return payload;
38
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export class PersistentCollectionStore {
5
+ constructor(filePath, { key = 'id', limit = 25000, seed = [] } = {}) {
6
+ this.filePath = path.resolve(filePath);
7
+ this.key = key;
8
+ this.limit = limit;
9
+ this.items = this.#load(seed);
10
+ }
11
+
12
+ add(item) {
13
+ this.items.unshift(item);
14
+ this.#trim();
15
+ this.save();
16
+ return item;
17
+ }
18
+ get(id) { return this.items.find(item => item?.[this.key] === id) || null; }
19
+ list(filter) {
20
+ const values = [...this.items];
21
+ if (typeof filter === 'function') return values.filter(filter);
22
+ return values;
23
+ }
24
+ update(id, patch) {
25
+ const item = this.get(id);
26
+ if (!item) return null;
27
+ Object.assign(item, patch);
28
+ this.save();
29
+ return item;
30
+ }
31
+ replace(items) { this.items = Array.isArray(items) ? items.slice(0, this.limit) : []; this.save(); return this.list(); }
32
+ save() {
33
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
34
+ const temp = `${this.filePath}.tmp`;
35
+ fs.writeFileSync(temp, JSON.stringify(this.items, null, 2), 'utf8');
36
+ fs.renameSync(temp, this.filePath);
37
+ }
38
+ #load(seed) {
39
+ try {
40
+ if (!fs.existsSync(this.filePath)) return Array.isArray(seed) ? seed.slice(0, this.limit) : [];
41
+ const parsed = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
42
+ return Array.isArray(parsed) ? parsed.slice(0, this.limit) : [];
43
+ } catch { return Array.isArray(seed) ? seed.slice(0, this.limit) : []; }
44
+ }
45
+ #trim() { if (this.items.length > this.limit) this.items.splice(this.limit); }
46
+ }
47
+
48
+ export function createPersistentRunStore(options = {}) {
49
+ return new PersistentCollectionStore(options.filePath || '.agentgate/runs.json', { limit: options.limit || 25000 });
50
+ }
51
+ export function createPersistentApprovalStore(options = {}) {
52
+ return new PersistentCollectionStore(options.filePath || '.agentgate/approvals.json', { limit: options.limit || 25000 });
53
+ }
54
+ export function createPersistentAgentStore(options = {}) {
55
+ return new PersistentCollectionStore(options.filePath || '.agentgate/agents.json', { limit: options.limit || 1000 });
56
+ }
@@ -0,0 +1,74 @@
1
+ import { DECISIONS } from './policy-engine.js';
2
+
3
+ /**
4
+ * Convert attack results into deterministic, reviewable policy suggestions.
5
+ * Suggestions are data only; AgentGate never auto-authorizes from them.
6
+ */
7
+ export function generatePolicySuggestions(results = []) {
8
+ const suggestions = [];
9
+ const blocked = new Set();
10
+ const approval = new Set();
11
+
12
+ for (const result of results) {
13
+ const action = String(result.action || '').toLowerCase();
14
+ if (!action) continue;
15
+
16
+ if (result.decision === DECISIONS.BLOCK) {
17
+ blocked.add(action);
18
+ suggestions.push({
19
+ id: `block-${action}`,
20
+ type: 'block_action',
21
+ action,
22
+ reason: result.reason || 'Attack was blocked by AgentGate',
23
+ confidence: 0.98
24
+ });
25
+ } else if (result.decision === DECISIONS.ASK) {
26
+ approval.add(action);
27
+ suggestions.push({
28
+ id: `approval-${action}`,
29
+ type: 'require_approval',
30
+ action,
31
+ reason: result.reason || 'Attack requires human approval',
32
+ confidence: 0.92
33
+ });
34
+ } else if (result.risk >= 70) {
35
+ approval.add(action);
36
+ suggestions.push({
37
+ id: `review-${action}`,
38
+ type: 'require_approval',
39
+ action,
40
+ reason: 'High-risk action should require review',
41
+ confidence: 0.85
42
+ });
43
+ }
44
+ }
45
+
46
+ const policy = {};
47
+ if (blocked.size) policy.blockActions = [...blocked].sort();
48
+ if (approval.size) policy.approvalActions = [...approval].filter(a => !blocked.has(a)).sort();
49
+
50
+ return { policy, suggestions: dedupe(suggestions) };
51
+ }
52
+
53
+ export function mergePolicies(base = {}, generated = {}) {
54
+ return {
55
+ ...base,
56
+ ...generated,
57
+ blockActions: unique([...(base.blockActions || []), ...(generated.blockActions || [])]),
58
+ approvalActions: unique([...(base.approvalActions || []), ...(generated.approvalActions || [])])
59
+ .filter(action => !(generated.blockActions || []).includes(action) && !(base.blockActions || []).includes(action))
60
+ };
61
+ }
62
+
63
+ function unique(values) {
64
+ return [...new Set(values.map(value => String(value).toLowerCase()))];
65
+ }
66
+
67
+ function dedupe(items) {
68
+ const seen = new Set();
69
+ return items.filter(item => {
70
+ if (seen.has(item.id)) return false;
71
+ seen.add(item.id);
72
+ return true;
73
+ });
74
+ }
@@ -0,0 +1,17 @@
1
+ import crypto from 'node:crypto';
2
+ import { PersistentCollectionStore } from './persistent-store.js';
3
+ import { evaluate } from './policy-engine.js';
4
+ export const BUNDLE_STATES=Object.freeze({DRAFT:'draft',TESTED:'tested',ACTIVE:'active',ARCHIVED:'archived'});
5
+ export class PolicyBundleRegistry {
6
+ constructor(options={}){ this.store=options.store||new PersistentCollectionStore(options.filePath||'.agentgate/policy-bundles.json',{limit:options.limit||2000}); }
7
+ create(name,policies={},metadata={},tenantId=null){ const versions=this.store.list(x=>x.name===name && (tenantId==null ? !x.tenantId : x.tenantId===tenantId)); const version=(Math.max(0,...versions.map(x=>x.version||0))+1); const item={id:`bundle_${crypto.randomBytes(8).toString('hex')}`,name,version,state:BUNDLE_STATES.DRAFT,tenantId:tenantId||null,policies:{...policies},metadata,hash:hash(policies),createdAt:new Date().toISOString()}; return this.store.add(item); }
8
+ list(name,tenantId=null){ return this.store.list(x=>(!name||x.name===name) && (tenantId==null ? !x.tenantId : x.tenantId===tenantId)); }
9
+ get(name,version,tenantId=null){ return this.store.list(x=>x.name===name&&x.version===Number(version) && (tenantId==null ? !x.tenantId : x.tenantId===tenantId))[0]||null; }
10
+ test(name,version,cases=[],tenantId=null){ const b=this.get(name,version,tenantId); if(!b) throw new Error('Policy bundle not found'); const results=cases.map(c=>{const r=evaluate(c.input||{},b.policies); return {...c,actual:r.decision,passed:r.decision===c.expected};}); const passed=results.every(x=>x.passed); if(passed){b.state=BUNDLE_STATES.TESTED;b.testedAt=new Date().toISOString();b.testResults=results;this.store.save();} return {passed,results}; }
11
+ activate(name,version,tenantId=null){ const b=this.get(name,version,tenantId); if(!b) throw new Error('Policy bundle not found'); if(b.state!==BUNDLE_STATES.TESTED&&b.state!==BUNDLE_STATES.ACTIVE) throw new Error('Bundle must pass tests before activation'); for(const x of this.store.list(i=>i.name===name&&i.state===BUNDLE_STATES.ACTIVE && (tenantId==null ? !i.tenantId : i.tenantId===tenantId))){x.state=BUNDLE_STATES.ARCHIVED;} b.state=BUNDLE_STATES.ACTIVE;b.activatedAt=new Date().toISOString();this.store.save();return b; }
12
+ active(name,tenantId=null){ return this.store.list(x=>x.name===name&&x.state===BUNDLE_STATES.ACTIVE && (tenantId==null ? !x.tenantId : x.tenantId===tenantId))[0]||null; }
13
+ activeAll(tenantId=null){ return this.store.list(x=>x.state===BUNDLE_STATES.ACTIVE && (tenantId==null ? !x.tenantId : x.tenantId===tenantId)); }
14
+ diff(name,a,b,tenantId=null){const x=this.get(name,a,tenantId),y=this.get(name,b,tenantId);if(!x||!y)throw new Error('Policy bundle version not found');const keys=new Set([...Object.keys(x.policies),...Object.keys(y.policies)]);return [...keys].filter(k=>JSON.stringify(x.policies[k])!==JSON.stringify(y.policies[k])).map(k=>({key:k,from:x.policies[k],to:y.policies[k]}));}
15
+ }
16
+ function hash(v){return crypto.createHash('sha256').update(JSON.stringify(v,Object.keys(v).sort())).digest('hex');}
17
+ export const createPolicyBundleRegistry=o=>new PolicyBundleRegistry(o);
@@ -0,0 +1,67 @@
1
+ import { authorize } from './identity.js';
2
+
3
+ export const DECISIONS = Object.freeze({ ALLOW: 'ALLOW', ASK: 'ASK', BLOCK: 'BLOCK' });
4
+
5
+ const destructive = new Set(['delete','refund','publish','deploy','export_all','update_production']);
6
+ const readOnly = new Set(['read','search','list','get','fetch']);
7
+
8
+ export function evaluate(input = {}, policies = {}) {
9
+ const auth = authorize(input, policies.authorization);
10
+ const trace = [{ id: 'authorization', matched: auth.decision === 'BLOCK', decision: auth.decision, reason: auth.reason, matchedRule: auth.matchedRule || null }];
11
+ if (auth.decision === 'BLOCK') return decision('BLOCK', auth.reason, 96, { authorization: auth, ruleTrace: trace, winningRule: auth.matchedRule || 'authorization' });
12
+ const action = String(input.action || '').toLowerCase();
13
+ const hasAmount = Object.prototype.hasOwnProperty.call(input, 'amount') && input.amount !== undefined && input.amount !== null;
14
+ if (hasAmount && (typeof input.amount !== 'number' || !Number.isFinite(input.amount) || input.amount < 0)) {
15
+ return decision('BLOCK', 'Amount must be a finite non-negative number', 98, { ruleTrace: [...trace, { id: 'invalid-amount', matched: true, decision: 'BLOCK', reason: 'Amount must be a finite non-negative number' }], winningRule: 'invalid-amount' });
16
+ }
17
+ const amount = hasAmount ? input.amount : 0;
18
+ const env = input.environment || 'production';
19
+
20
+ if (policies.blockActions?.includes(action) || action === 'export_all') {
21
+ return decision('BLOCK', 'Action explicitly blocked by policy', 95, { ruleTrace: [...trace, { id: 'explicit-block', matched: true, decision: 'BLOCK', reason: 'Action explicitly blocked by policy' }], winningRule: 'explicit-block' });
22
+ }
23
+ if (policies.productionBlock && env === 'production' && destructive.has(action)) {
24
+ return decision('BLOCK', 'Destructive production action is blocked', 90, { ruleTrace: [...trace, { id: 'production-block', matched: true, decision: 'BLOCK', reason: 'Destructive production action is blocked' }], winningRule: 'production-block' });
25
+ }
26
+ if (amount > 0 && amount > (policies.approvalAmount ?? 5000)) {
27
+ return decision('BLOCK', 'Amount exceeds the maximum permitted threshold', 92, { ruleTrace: [...trace, { id: 'approval-amount-hard', matched: true, decision: 'BLOCK', reason: 'Amount exceeds the maximum permitted threshold' }], winningRule: 'approval-amount-hard' });
28
+ }
29
+ if (policies.approvalActions?.includes(action)) {
30
+ return decision('ASK', 'Approval required for sensitive action', 70, { ruleTrace: [...trace, { id: 'approval-action', matched: true, decision: 'ASK', reason: 'Approval required for sensitive action' }], winningRule: 'approval-action' });
31
+ }
32
+ if (amount > 0 && amount > (policies.autoApproveAmount ?? 500)) {
33
+ return decision('ASK', 'Amount exceeds the automatic approval threshold', 65, { ruleTrace: [...trace, { id: 'auto-approval-threshold', matched: true, decision: 'ASK', reason: 'Amount exceeds the automatic approval threshold' }], winningRule: 'auto-approval-threshold' });
34
+ }
35
+ if (readOnly.has(action)) return decision('ALLOW', 'Read-only action allowed', 5, { ruleTrace: [...trace, { id: 'readonly-allow', matched: true, decision: 'ALLOW', reason: 'Read-only action allowed' }], winningRule: 'readonly-allow' });
36
+ if (destructive.has(action) && policies.requireApprovalForDestructive !== false) {
37
+ return decision('ASK', 'Destructive action requires approval', 75, { ruleTrace: [...trace, { id: 'destructive-approval', matched: true, decision: 'ASK', reason: 'Destructive action requires approval' }], winningRule: 'destructive-approval' });
38
+ }
39
+ return decision('ALLOW', 'No blocking policy matched', 10, { ruleTrace: [...trace, { id: 'default-allow', matched: true, decision: 'ALLOW', reason: 'No blocking policy matched' }], winningRule: 'default-allow' });
40
+ }
41
+
42
+ function decision(decision, reason, risk, extra = {}) {
43
+ return { decision, reason, risk, timestamp: new Date().toISOString(), ...extra };
44
+ }
45
+
46
+ export function protect(tool, options = {}) {
47
+ if (typeof tool !== 'function') throw new TypeError('protect() expects a function');
48
+ const policies = {
49
+ autoApproveAmount: 500,
50
+ approvalAmount: 5000,
51
+ ...options
52
+ };
53
+ return async function protectedTool(input = {}, context = {}) {
54
+ const result = evaluate({ ...input, ...context }, policies);
55
+ if (result.decision === 'BLOCK') {
56
+ const error = new Error(`AgentGate blocked action: ${result.reason}`);
57
+ error.code = 'AGENTGATE_BLOCKED';
58
+ error.agentgate = result;
59
+ throw error;
60
+ }
61
+ if (result.decision === 'ASK') {
62
+ return { status: 'approval_required', agentgate: result };
63
+ }
64
+ const value = await tool(input);
65
+ return { status: 'executed', value, agentgate: result };
66
+ };
67
+ }