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,132 @@
1
+ const SENSITIVE_FIELD_NAMES = /^(?:secret|password|passphrase|private[_-]?key|access[_-]?token|refresh[_-]?token|authorization|auth[_-]?token|client[_-]?secret|api[_-]?secret)$/i;
2
+
3
+ const DEFAULT_RULES = Object.freeze({
4
+ api_key: { action: 'BLOCK', patterns: [/(?:sk-[A-Za-z0-9_-]{16,})/g, /(?:AKIA|ASIA)[A-Z0-9]{16}/g], replacement: '[REDACTED:API_KEY]' },
5
+ private_key: { action: 'BLOCK', patterns: [/-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/g], replacement: '[REDACTED:PRIVATE_KEY]' },
6
+ bearer_token: { action: 'BLOCK', patterns: [/\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/gi], replacement: 'Bearer [REDACTED:TOKEN]' },
7
+ jwt: { action: 'BLOCK', patterns: [/(?:eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,})/g], replacement: '[REDACTED:JWT]' },
8
+ email: { action: 'REDACT', patterns: [/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi], replacement: '[REDACTED:EMAIL]' },
9
+ phone: { action: 'REDACT', patterns: [/(?<!\d)(?:\+?\d[\d\s().-]{7,}\d)(?!\d)/g], replacement: '[REDACTED:PHONE]' },
10
+ credit_card: { action: 'BLOCK', patterns: [/(?<!\d)(?:\d[ -]*?){13,19}(?!\d)/g], replacement: '[REDACTED:CARD]' }
11
+ });
12
+
13
+ export const EGRESS_ACTIONS = Object.freeze({ ALLOW: 'ALLOW', REDACT: 'REDACT', BLOCK: 'BLOCK' });
14
+
15
+ function clone(value) {
16
+ try { return structuredClone(value); } catch { return JSON.parse(JSON.stringify(value)); }
17
+ }
18
+
19
+ function stringifyForScan(value) {
20
+ if (typeof value === 'string') return value;
21
+ try { return JSON.stringify(value); } catch { return String(value); }
22
+ }
23
+
24
+ function walkAndTransform(value, transform, path = '$') {
25
+ if (typeof value === 'string') return transform(value, path);
26
+ if (Array.isArray(value)) return value.map((item, i) => walkAndTransform(item, transform, `${path}[${i}]`));
27
+ if (value && typeof value === 'object') {
28
+ const out = {};
29
+ for (const [key, item] of Object.entries(value)) out[key] = walkAndTransform(item, transform, `${path}.${key}`);
30
+ return out;
31
+ }
32
+ return value;
33
+ }
34
+
35
+ function normalizeRules(rules = {}) {
36
+ const merged = { ...DEFAULT_RULES };
37
+ for (const [type, rule] of Object.entries(rules || {})) {
38
+ if (rule === false) { delete merged[type]; continue; }
39
+ if (rule && typeof rule === 'object') merged[type] = { ...(merged[type] || {}), ...rule };
40
+ }
41
+ return merged;
42
+ }
43
+
44
+ function sensitiveFieldFindings(value, path = '$', findings = []) {
45
+ if (!value || typeof value !== 'object') return findings;
46
+ if (Array.isArray(value)) {
47
+ value.forEach((item, index) => sensitiveFieldFindings(item, `${path}[${index}]`, findings));
48
+ return findings;
49
+ }
50
+ for (const [key, item] of Object.entries(value)) {
51
+ const childPath = `${path}.${key}`;
52
+ if (SENSITIVE_FIELD_NAMES.test(key) && item !== null && item !== undefined && String(item).length > 0) {
53
+ findings.push({ type: 'generic_secret', action: EGRESS_ACTIONS.BLOCK, count: 1, path: childPath });
54
+ }
55
+ sensitiveFieldFindings(item, childPath, findings);
56
+ }
57
+ return findings;
58
+ }
59
+
60
+ function sensitiveFieldFindingFromText(text) {
61
+ const findings = [];
62
+ const pattern = /[\"'](?:secret|password|passphrase|private[_-]?key|access[_-]?token|refresh[_-]?token|authorization|auth[_-]?token|client[_-]?secret|api[_-]?secret)[\"']\s*:\s*[\"']([^\"']+)[\"']/gi;
63
+ for (const match of text.matchAll(pattern)) findings.push({ type: 'generic_secret', action: EGRESS_ACTIONS.BLOCK, count: 1 });
64
+ return findings;
65
+ }
66
+
67
+ export function inspectEgress(value, options = {}) {
68
+ const rules = normalizeRules(options.rules);
69
+ const findings = sensitiveFieldFindings(value);
70
+ const text = stringifyForScan(value);
71
+ if (!findings.length || typeof value === 'string') findings.push(...sensitiveFieldFindingFromText(text));
72
+ for (const [type, rule] of Object.entries(rules)) {
73
+ for (const pattern of rule.patterns || []) {
74
+ pattern.lastIndex = 0;
75
+ const matches = text.match(pattern);
76
+ if (matches?.length) findings.push({ type, action: rule.action || EGRESS_ACTIONS.BLOCK, count: matches.length });
77
+ }
78
+ }
79
+ const action = findings.some(f => f.action === EGRESS_ACTIONS.BLOCK)
80
+ ? EGRESS_ACTIONS.BLOCK
81
+ : findings.length ? EGRESS_ACTIONS.REDACT : EGRESS_ACTIONS.ALLOW;
82
+ return { action, findings, risk: action === EGRESS_ACTIONS.BLOCK ? 95 : action === EGRESS_ACTIONS.REDACT ? 70 : 0 };
83
+ }
84
+
85
+ export function guardEgress(value, options = {}) {
86
+ const rules = normalizeRules(options.rules);
87
+ const findings = [];
88
+ let blocked = false;
89
+ const transformed = walkAndTransform(clone(value), (input, path) => {
90
+ let output = input;
91
+ const fieldName = path.split('.').pop() || '';
92
+ if (SENSITIVE_FIELD_NAMES.test(fieldName) && output !== null && typeof output !== 'object' && String(output).length > 0) {
93
+ findings.push({ type: 'generic_secret', action: EGRESS_ACTIONS.BLOCK, path, count: 1 });
94
+ blocked = true;
95
+ output = '[REDACTED:SECRET]';
96
+ }
97
+ for (const [type, rule] of Object.entries(rules)) {
98
+ for (const pattern of rule.patterns || []) {
99
+ pattern.lastIndex = 0;
100
+ if (!pattern.test(output)) continue;
101
+ pattern.lastIndex = 0;
102
+ const matches = output.match(pattern) || [];
103
+ const action = rule.action || EGRESS_ACTIONS.BLOCK;
104
+ findings.push({ type, action, path, count: matches.length });
105
+ if (action === EGRESS_ACTIONS.BLOCK) blocked = true;
106
+ if (action === EGRESS_ACTIONS.REDACT || action === EGRESS_ACTIONS.BLOCK) {
107
+ output = output.replace(pattern, rule.replacement || `[REDACTED:${type.toUpperCase()}]`);
108
+ }
109
+ }
110
+ }
111
+ return output;
112
+ });
113
+ const action = blocked ? EGRESS_ACTIONS.BLOCK : findings.length ? EGRESS_ACTIONS.REDACT : EGRESS_ACTIONS.ALLOW;
114
+ return {
115
+ action,
116
+ allowed: action !== EGRESS_ACTIONS.BLOCK,
117
+ blocked,
118
+ value: transformed,
119
+ findings,
120
+ risk: action === EGRESS_ACTIONS.BLOCK ? 95 : action === EGRESS_ACTIONS.REDACT ? 70 : 0,
121
+ reason: action === EGRESS_ACTIONS.BLOCK ? 'Sensitive data egress blocked' : action === EGRESS_ACTIONS.REDACT ? 'Sensitive data redacted before egress' : 'No sensitive data detected'
122
+ };
123
+ }
124
+
125
+ export function createEgressGuard(options = {}) {
126
+ const config = { ...options };
127
+ return {
128
+ inspect(value) { return inspectEgress(value, config); },
129
+ guard(value) { return guardEgress(value, config); },
130
+ rules: normalizeRules(config.rules)
131
+ };
132
+ }
@@ -0,0 +1,10 @@
1
+ import { EventEmitter } from 'node:events';
2
+ export const RUNTIME_EVENTS = Object.freeze({
3
+ RUN_RECORDED:'run.recorded', RUN_DECIDED:'run.decided', APPROVAL_CREATED:'approval.created', APPROVAL_RESOLVED:'approval.resolved', POLICY_ACTIVATED:'policy.activated', WEBHOOK_DELIVERED:'webhook.delivered'
4
+ });
5
+ export class RuntimeEventBus extends EventEmitter {
6
+ publish(event, payload={}) { const message={id:cryptoRandom(),event,payload,timestamp:new Date().toISOString()}; this.emit(event,message); this.emit('*',message); return message; }
7
+ subscribe(event, handler){ this.on(event,handler); return ()=>this.off(event,handler); }
8
+ }
9
+ function cryptoRandom(){ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`; }
10
+ export const createEventBus=(options={})=>new RuntimeEventBus(options);
@@ -0,0 +1,109 @@
1
+ export const AUTH_DECISIONS = Object.freeze({ ALLOW: 'ALLOW', BLOCK: 'BLOCK' });
2
+
3
+ function arr(value) {
4
+ if (value == null) return [];
5
+ return Array.isArray(value) ? value.map(String) : [String(value)];
6
+ }
7
+
8
+ function matches(value, expected) {
9
+ const actual = String(value ?? '');
10
+ return arr(expected).some(pattern => {
11
+ if (pattern === '*') return true;
12
+ if (!pattern.includes('*')) return actual === pattern;
13
+ const escaped = pattern.split('*').map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*');
14
+ return new RegExp(`^${escaped}$`).test(actual);
15
+ });
16
+ }
17
+
18
+ function listMatches(actual, expected) {
19
+ const values = arr(actual);
20
+ return !expected || arr(expected).length === 0 || values.some(v => matches(v, expected));
21
+ }
22
+
23
+ function attributesMatch(actual = {}, expected = {}) {
24
+ return Object.entries(expected || {}).every(([key, wanted]) => {
25
+ const value = actual?.[key];
26
+ if (Array.isArray(wanted)) return wanted.map(String).includes(String(value));
27
+ return matches(value, wanted);
28
+ });
29
+ }
30
+
31
+ function ruleMatches(request, rule = {}) {
32
+ const identity = request.identity || {};
33
+ if (rule.actions && !matches(request.action, rule.actions)) return false;
34
+ if (rule.tools && !matches(request.tool, rule.tools)) return false;
35
+ if (rule.resources && !matches(request.resource, rule.resources)) return false;
36
+ if (rule.agents && !matches(identity.agentId || identity.agent || request.agent, rule.agents)) return false;
37
+ if (rule.users && !matches(identity.userId || identity.user, rule.users)) return false;
38
+ if (rule.roles && !listMatches(identity.roles || identity.role, rule.roles)) return false;
39
+ if (rule.environments && !matches(request.environment, rule.environments)) return false;
40
+ if (rule.attributes && !attributesMatch(identity.attributes || {}, rule.attributes)) return false;
41
+ return true;
42
+ }
43
+
44
+ export function normalizeIdentity(input = {}) {
45
+ const identity = input.identity && typeof input.identity === 'object' ? input.identity : {};
46
+ const agent = identity.agentId || identity.agent || input.agent || null;
47
+ const user = identity.userId || identity.user || input.userId || input.user || null;
48
+ return {
49
+ agentId: agent,
50
+ userId: user,
51
+ roles: arr(identity.roles || identity.role || input.roles || input.role),
52
+ attributes: { ...(input.attributes || {}), ...(identity.attributes || {}) },
53
+ authenticated: identity.authenticated !== false && Boolean(agent || user || (identity.roles || input.roles || identity.role || input.role)),
54
+ source: identity.source || input.identitySource || 'runtime'
55
+ };
56
+ }
57
+
58
+ export function authorize(request = {}, authorization = {}) {
59
+ if (!authorization || Object.keys(authorization).length === 0) {
60
+ return { decision: AUTH_DECISIONS.ALLOW, reason: 'Authorization layer not configured', matchedRule: null, identity: normalizeIdentity(request), enforced: false };
61
+ }
62
+
63
+ const identity = normalizeIdentity(request);
64
+ const base = { decision: AUTH_DECISIONS.BLOCK, identity, enforced: true, matchedRule: null };
65
+
66
+ if (authorization.requireIdentity !== false && !identity.authenticated) {
67
+ return { ...base, reason: 'Authenticated identity is required' };
68
+ }
69
+
70
+ const rules = Array.isArray(authorization.rules) ? authorization.rules : [];
71
+ for (const rule of rules) {
72
+ if (!ruleMatches({ ...request, identity }, rule)) continue;
73
+ const effect = String(rule.effect || 'deny').toLowerCase();
74
+ if (effect === 'allow') return { ...base, decision: AUTH_DECISIONS.ALLOW, reason: rule.reason || 'Authorization rule allowed request', matchedRule: rule.id || null };
75
+ return { ...base, reason: rule.reason || 'Authorization rule denied request', matchedRule: rule.id || null };
76
+ }
77
+
78
+ const roles = authorization.roles || {};
79
+ for (const role of identity.roles) {
80
+ const rolePolicy = roles[role];
81
+ if (!rolePolicy) continue;
82
+ if (rolePolicy.deny && (matches(request.action, rolePolicy.deny.actions) || matches(request.tool, rolePolicy.deny.tools) || matches(request.resource, rolePolicy.deny.resources))) {
83
+ return { ...base, reason: `Role ${role} explicitly denies this action`, matchedRule: `role:${role}:deny` };
84
+ }
85
+ if (rolePolicy.allow && (matches(request.action, rolePolicy.allow.actions) || matches(request.tool, rolePolicy.allow.tools) || matches(request.resource, rolePolicy.allow.resources))) {
86
+ return { ...base, decision: AUTH_DECISIONS.ALLOW, reason: `Role ${role} allows this action`, matchedRule: `role:${role}:allow` };
87
+ }
88
+ }
89
+
90
+ if (authorization.defaultEffect === 'allow') {
91
+ return { ...base, decision: AUTH_DECISIONS.ALLOW, reason: 'Authorization default effect allows request', matchedRule: 'default' };
92
+ }
93
+ return { ...base, reason: 'No authorization rule matched; deny by default' };
94
+ }
95
+
96
+ export function createIdentityRegistry(options = {}) {
97
+ const items = options.store || new Map();
98
+ function register(identity = {}) {
99
+ const normalized = normalizeIdentity(identity);
100
+ const id = normalized.agentId || normalized.userId;
101
+ if (!id) throw new TypeError('registerIdentity() requires agentId or userId');
102
+ const record = { ...normalized, id, updatedAt: new Date().toISOString(), createdAt: items.get(id)?.createdAt || new Date().toISOString() };
103
+ if (items instanceof Map) items.set(id, record); else items.add(record);
104
+ return record;
105
+ }
106
+ function get(id) { return items instanceof Map ? items.get(id) || null : items.get(id); }
107
+ function list() { return items instanceof Map ? [...items.values()] : items.list(); }
108
+ return Object.freeze({ register, get, list });
109
+ }
package/src/index.js ADDED
@@ -0,0 +1,44 @@
1
+ export { evaluate, protect, DECISIONS } from './policy-engine.js';
2
+ export { createMiddleware } from './middleware.js';
3
+ export { createRuntime, RunStore } from './runtime.js';
4
+ export { runAttackLab, runGatewayAttackLab, summarizeAttackResults, ATTACK_CASES } from './attack-lab.js';
5
+
6
+ export { createMCPGateway, createMCPGatewayServer, MCP_PROTOCOL_VERSION } from './mcp-gateway.js';
7
+
8
+ export { generatePolicySuggestions, mergePolicies } from './policy-builder.js';
9
+ export { ApprovalStore, createApprovalStore, createApprovalRequest, APPROVAL_STATUS } from './approval.js';
10
+
11
+ export { createAgentGate } from './agentgate.js';
12
+ export { generateSecurityReport, renderSecurityReportHTML } from './security-report.js';
13
+
14
+ export { analyzeBehavior, summarizeBehavior, calculateBlastRadius, analyzeBlastRadius } from './behavior.js';
15
+
16
+ export { PersistentCollectionStore, createPersistentRunStore, createPersistentApprovalStore, createPersistentAgentStore } from './persistent-store.js';
17
+
18
+ export { authorize, normalizeIdentity, createIdentityRegistry, AUTH_DECISIONS } from './identity.js';
19
+
20
+ export { PolicyRegistry, createPolicyRegistry, POLICY_STATES } from './policy-registry.js';
21
+ export { TenantRegistry, WebhookRegistry, DEFAULT_SCOPES } from './multi-tenant.js';
22
+ export { AUTH_HEADERS, extractAPIKey, createAuthMiddleware } from './auth.js';
23
+ export { signWebhook, createWebhookDispatcher } from './webhook-delivery.js';
24
+ export { PostgresStoreAdapter, createSupabaseAdapter, POSTGRES_SCHEMA } from './postgres-adapter.js';
25
+ export { RuntimeEventBus, createEventBus, RUNTIME_EVENTS } from './event-bus.js';
26
+ export { PolicyBundleRegistry, createPolicyBundleRegistry, BUNDLE_STATES } from './policy-bundles.js';
27
+ export { ADMIN_ROLES, adminAuthorize, requireAdmin } from './admin-rbac.js';
28
+ export { mapOIDCClaims, validateOIDCIdentity } from './oidc.js';
29
+
30
+ export { RuntimeTelemetry, createTelemetry, SlidingWindowLimiter } from './telemetry.js';
31
+ export { decodeJWT, verifyOIDCJWT } from './oidc.js';
32
+
33
+ export { PLANS, OrganizationRegistry, UsageMeter, EntitlementService, BillingAdapter, createSaaSControl } from './saas.js';
34
+
35
+ export * from './mcp-scanner.js';
36
+
37
+ export { EGRESS_ACTIONS, inspectEgress, guardEgress, createEgressGuard } from './egress-guard.js';
38
+
39
+ export { COST_DECISIONS, PricingRegistry, calculateCost, CostController, analyzeObservability, createObservability, buildUnifiedTrace, calculateAgentEfficiency, analyzeAgentEfficiency, forecastCost, correlateBehaviorCostSecurity, analyzeCostAnalytics } from './observability.js';
40
+ export { validateAgentGateConfig, simulatePolicyMatrix, runDoctorChecks } from './local-experience.js';
41
+ export { runSecurityValidation } from './security-validation.js';
42
+ export { listPolicyPacks, getPolicyPack, getDefaultPolicyPack } from './policy-packs.js';
43
+
44
+ export { recordShadowEvent, analyzeShadowEvents } from './shadow-mode.js';
@@ -0,0 +1,46 @@
1
+ import { evaluate } from './policy-engine.js';
2
+
3
+ const REQUIRED_POLICY_FIELDS = ['productionBlock', 'autoApproveAmount', 'approvalAmount'];
4
+
5
+ export function validateAgentGateConfig(config = {}) {
6
+ const errors = [];
7
+ const warnings = [];
8
+ if (!config || typeof config !== 'object' || Array.isArray(config)) errors.push('Configuration must be an object');
9
+ const c = config && typeof config === 'object' ? config : {};
10
+ if (c.mode && !['observe', 'enforce'].includes(c.mode)) errors.push('mode must be observe or enforce');
11
+ if (c.agent !== undefined && (typeof c.agent !== 'string' || !c.agent.trim())) errors.push('agent must be a non-empty string');
12
+ const p = c.policies || {};
13
+ if (p.autoApproveAmount !== undefined && (!Number.isFinite(Number(p.autoApproveAmount)) || Number(p.autoApproveAmount) < 0)) errors.push('policies.autoApproveAmount must be a non-negative number');
14
+ if (p.approvalAmount !== undefined && (!Number.isFinite(Number(p.approvalAmount)) || Number(p.approvalAmount) < 0)) errors.push('policies.approvalAmount must be a non-negative number');
15
+ if (Number.isFinite(Number(p.autoApproveAmount)) && Number.isFinite(Number(p.approvalAmount)) && Number(p.autoApproveAmount) > Number(p.approvalAmount)) errors.push('autoApproveAmount cannot exceed approvalAmount');
16
+ if (c.mode === 'observe') warnings.push('observe mode records decisions but does not enforce them');
17
+ if (c.authRequired === false) warnings.push('authRequired=false is unsafe for non-local deployments');
18
+ if (c.localDevSession === true && c.authRequired === false) warnings.push('localDevSession should not be combined with authRequired=false');
19
+ return { valid: errors.length === 0, errors, warnings, checked: [...REQUIRED_POLICY_FIELDS] };
20
+ }
21
+
22
+ export function simulatePolicyMatrix({ policies = {}, cases = [] } = {}) {
23
+ const matrix = cases.length ? cases : [
24
+ { name: 'read', action: 'read', amount: 0, environment: 'production' },
25
+ { name: 'small refund', action: 'refund', amount: 250, environment: 'production' },
26
+ { name: 'large refund', action: 'refund', amount: 1200, environment: 'production' },
27
+ { name: 'huge refund', action: 'refund', amount: 6000, environment: 'production' },
28
+ { name: 'production delete', action: 'delete', amount: 0, environment: 'production' },
29
+ { name: 'export', action: 'export_all', amount: 0, environment: 'production' }
30
+ ];
31
+ return matrix.map((input, index) => {
32
+ const decision = evaluate(input, policies);
33
+ return { index, name: input.name || `case-${index + 1}`, input, decision: decision.decision, risk: decision.risk, reason: decision.reason, winningRule: decision.winningRule || null, ruleTrace: decision.ruleTrace || [] };
34
+ });
35
+ }
36
+
37
+ export function runDoctorChecks({ config = {}, gateway = null } = {}) {
38
+ const configResult = validateAgentGateConfig(config);
39
+ const checks = [
40
+ { name: 'config', ok: configResult.valid, details: configResult },
41
+ { name: 'runtime-mode', ok: gateway ? ['observe', 'enforce'].includes(gateway.mode) : ['observe', 'enforce'].includes(config.mode || 'enforce'), details: gateway ? { mode: gateway.mode } : { mode: config.mode || 'enforce', source: 'config-only preflight' } },
42
+ { name: 'policy-engine', ok: simulatePolicyMatrix({ policies: gateway?.policies || config.policies || {} }).every(x => ['ALLOW', 'ASK', 'BLOCK'].includes(x.decision)), details: 'decision matrix' },
43
+ { name: 'fail-closed-auth', ok: config.authRequired !== false || config.localDevSession === true, details: { authRequired: config.authRequired !== false } }
44
+ ];
45
+ return { ok: checks.every(x => x.ok), checks, warnings: configResult.warnings };
46
+ }