@ninelives/agent-governance-sdk 5.0.1

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 (126) hide show
  1. package/README.md +363 -0
  2. package/dist/audit.d.ts +26 -0
  3. package/dist/audit.d.ts.map +1 -0
  4. package/dist/audit.js +107 -0
  5. package/dist/audit.js.map +1 -0
  6. package/dist/cascade-containment.d.ts +84 -0
  7. package/dist/cascade-containment.d.ts.map +1 -0
  8. package/dist/cascade-containment.js +387 -0
  9. package/dist/cascade-containment.js.map +1 -0
  10. package/dist/client.d.ts +34 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +112 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/context-poisoning.d.ts +58 -0
  15. package/dist/context-poisoning.d.ts.map +1 -0
  16. package/dist/context-poisoning.js +410 -0
  17. package/dist/context-poisoning.js.map +1 -0
  18. package/dist/credential-vault.d.ts +213 -0
  19. package/dist/credential-vault.d.ts.map +1 -0
  20. package/dist/credential-vault.js +567 -0
  21. package/dist/credential-vault.js.map +1 -0
  22. package/dist/discovery.d.ts +80 -0
  23. package/dist/discovery.d.ts.map +1 -0
  24. package/dist/discovery.js +404 -0
  25. package/dist/discovery.js.map +1 -0
  26. package/dist/encryption/channel.d.ts +29 -0
  27. package/dist/encryption/channel.d.ts.map +1 -0
  28. package/dist/encryption/channel.js +78 -0
  29. package/dist/encryption/channel.js.map +1 -0
  30. package/dist/encryption/index.d.ts +9 -0
  31. package/dist/encryption/index.d.ts.map +1 -0
  32. package/dist/encryption/index.js +23 -0
  33. package/dist/encryption/index.js.map +1 -0
  34. package/dist/encryption/mesh-client.d.ts +281 -0
  35. package/dist/encryption/mesh-client.d.ts.map +1 -0
  36. package/dist/encryption/mesh-client.js +1110 -0
  37. package/dist/encryption/mesh-client.js.map +1 -0
  38. package/dist/encryption/ratchet.d.ts +38 -0
  39. package/dist/encryption/ratchet.d.ts.map +1 -0
  40. package/dist/encryption/ratchet.js +223 -0
  41. package/dist/encryption/ratchet.js.map +1 -0
  42. package/dist/encryption/registry-client.d.ts +136 -0
  43. package/dist/encryption/registry-client.d.ts.map +1 -0
  44. package/dist/encryption/registry-client.js +369 -0
  45. package/dist/encryption/registry-client.js.map +1 -0
  46. package/dist/encryption/x3dh.d.ts +80 -0
  47. package/dist/encryption/x3dh.d.ts.map +1 -0
  48. package/dist/encryption/x3dh.js +238 -0
  49. package/dist/encryption/x3dh.js.map +1 -0
  50. package/dist/framework-adapter.d.ts +63 -0
  51. package/dist/framework-adapter.d.ts.map +1 -0
  52. package/dist/framework-adapter.js +253 -0
  53. package/dist/framework-adapter.js.map +1 -0
  54. package/dist/identity.d.ts +124 -0
  55. package/dist/identity.d.ts.map +1 -0
  56. package/dist/identity.js +446 -0
  57. package/dist/identity.js.map +1 -0
  58. package/dist/index.d.ts +39 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +95 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/kill-switch.d.ts +22 -0
  63. package/dist/kill-switch.d.ts.map +1 -0
  64. package/dist/kill-switch.js +60 -0
  65. package/dist/kill-switch.js.map +1 -0
  66. package/dist/lifecycle.d.ts +50 -0
  67. package/dist/lifecycle.d.ts.map +1 -0
  68. package/dist/lifecycle.js +113 -0
  69. package/dist/lifecycle.js.map +1 -0
  70. package/dist/mcp.d.ts +41 -0
  71. package/dist/mcp.d.ts.map +1 -0
  72. package/dist/mcp.js +233 -0
  73. package/dist/mcp.js.map +1 -0
  74. package/dist/metrics.d.ts +123 -0
  75. package/dist/metrics.d.ts.map +1 -0
  76. package/dist/metrics.js +289 -0
  77. package/dist/metrics.js.map +1 -0
  78. package/dist/oci-manifest.d.ts +49 -0
  79. package/dist/oci-manifest.d.ts.map +1 -0
  80. package/dist/oci-manifest.js +182 -0
  81. package/dist/oci-manifest.js.map +1 -0
  82. package/dist/policy-backends/cedar.d.ts +15 -0
  83. package/dist/policy-backends/cedar.d.ts.map +1 -0
  84. package/dist/policy-backends/cedar.js +88 -0
  85. package/dist/policy-backends/cedar.js.map +1 -0
  86. package/dist/policy-backends/opa.d.ts +17 -0
  87. package/dist/policy-backends/opa.d.ts.map +1 -0
  88. package/dist/policy-backends/opa.js +74 -0
  89. package/dist/policy-backends/opa.js.map +1 -0
  90. package/dist/policy.d.ts +77 -0
  91. package/dist/policy.d.ts.map +1 -0
  92. package/dist/policy.js +760 -0
  93. package/dist/policy.js.map +1 -0
  94. package/dist/prompt-defense.d.ts +37 -0
  95. package/dist/prompt-defense.d.ts.map +1 -0
  96. package/dist/prompt-defense.js +229 -0
  97. package/dist/prompt-defense.js.map +1 -0
  98. package/dist/protocol-facets.d.ts +87 -0
  99. package/dist/protocol-facets.d.ts.map +1 -0
  100. package/dist/protocol-facets.js +728 -0
  101. package/dist/protocol-facets.js.map +1 -0
  102. package/dist/rings.d.ts +17 -0
  103. package/dist/rings.d.ts.map +1 -0
  104. package/dist/rings.js +61 -0
  105. package/dist/rings.js.map +1 -0
  106. package/dist/sandbox.d.ts +61 -0
  107. package/dist/sandbox.d.ts.map +1 -0
  108. package/dist/sandbox.js +199 -0
  109. package/dist/sandbox.js.map +1 -0
  110. package/dist/surface-parity.d.ts +45 -0
  111. package/dist/surface-parity.d.ts.map +1 -0
  112. package/dist/surface-parity.js +216 -0
  113. package/dist/surface-parity.js.map +1 -0
  114. package/dist/trust.d.ts +28 -0
  115. package/dist/trust.d.ts.map +1 -0
  116. package/dist/trust.js +209 -0
  117. package/dist/trust.js.map +1 -0
  118. package/dist/types.d.ts +422 -0
  119. package/dist/types.d.ts.map +1 -0
  120. package/dist/types.js +29 -0
  121. package/dist/types.js.map +1 -0
  122. package/dist/verify.d.ts +135 -0
  123. package/dist/verify.d.ts.map +1 -0
  124. package/dist/verify.js +356 -0
  125. package/dist/verify.js.map +1 -0
  126. package/package.json +66 -0
package/dist/policy.js ADDED
@@ -0,0 +1,760 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PolicyEngine = exports.PolicyConflictResolver = void 0;
4
+ // Copyright (c) Microsoft Corporation.
5
+ // Licensed under the MIT License.
6
+ const fs_1 = require("fs");
7
+ const types_1 = require("./types");
8
+ const protocol_facets_1 = require("./protocol-facets");
9
+ const VALID_POLICY_ACTIONS = new Set(['allow', 'deny', 'warn', 'require_approval', 'log']);
10
+ function asPolicyAction(value) {
11
+ if (typeof value === 'string' && VALID_POLICY_ACTIONS.has(value)) {
12
+ return value;
13
+ }
14
+ return undefined;
15
+ }
16
+ // ΓöÇΓöÇ Conflict Resolution ΓöÇΓöÇ
17
+ const SCOPE_SPECIFICITY = {
18
+ [types_1.PolicyScope.Global]: 0,
19
+ [types_1.PolicyScope.Tenant]: 1,
20
+ [types_1.PolicyScope.Agent]: 2,
21
+ };
22
+ /**
23
+ * Resolves conflicts between competing policy candidate decisions.
24
+ */
25
+ class PolicyConflictResolver {
26
+ strategy;
27
+ constructor(strategy) {
28
+ this.strategy = strategy;
29
+ }
30
+ resolve(candidates) {
31
+ if (candidates.length === 0) {
32
+ throw new Error('Cannot resolve conflict with zero candidates');
33
+ }
34
+ if (candidates.length === 1) {
35
+ return {
36
+ winningDecision: candidates[0],
37
+ strategyUsed: this.strategy,
38
+ candidatesEvaluated: 1,
39
+ conflictDetected: false,
40
+ resolutionTrace: [`Single candidate: ${candidates[0].ruleName} → ${candidates[0].action}`],
41
+ };
42
+ }
43
+ const actions = new Set(candidates.map((c) => c.action));
44
+ const conflictDetected = actions.has('allow') && actions.has('deny');
45
+ const dispatch = {
46
+ [types_1.ConflictResolutionStrategy.DenyOverrides]: (cs) => this.denyOverrides(cs),
47
+ [types_1.ConflictResolutionStrategy.AllowOverrides]: (cs) => this.allowOverrides(cs),
48
+ [types_1.ConflictResolutionStrategy.PriorityFirstMatch]: (cs) => this.priorityFirstMatch(cs),
49
+ [types_1.ConflictResolutionStrategy.MostSpecificWins]: (cs) => this.mostSpecificWins(cs),
50
+ };
51
+ const { winner, trace } = dispatch[this.strategy](candidates);
52
+ return {
53
+ winningDecision: winner,
54
+ strategyUsed: this.strategy,
55
+ candidatesEvaluated: candidates.length,
56
+ conflictDetected,
57
+ resolutionTrace: trace,
58
+ };
59
+ }
60
+ denyOverrides(candidates) {
61
+ const denies = candidates.filter((c) => c.action === 'deny');
62
+ if (denies.length > 0) {
63
+ denies.sort((a, b) => b.priority - a.priority);
64
+ return {
65
+ winner: denies[0],
66
+ trace: [
67
+ `DENY_OVERRIDES: ${denies.length} deny rule(s) found`,
68
+ `Winner: ${denies[0].ruleName} (priority=${denies[0].priority})`,
69
+ ],
70
+ };
71
+ }
72
+ const sorted = [...candidates].sort((a, b) => b.priority - a.priority);
73
+ return {
74
+ winner: sorted[0],
75
+ trace: ['DENY_OVERRIDES: no deny rules, selecting highest-priority allow'],
76
+ };
77
+ }
78
+ allowOverrides(candidates) {
79
+ const allows = candidates.filter((c) => c.action === 'allow');
80
+ if (allows.length > 0) {
81
+ allows.sort((a, b) => b.priority - a.priority);
82
+ return {
83
+ winner: allows[0],
84
+ trace: [
85
+ `ALLOW_OVERRIDES: ${allows.length} allow rule(s) found`,
86
+ `Winner: ${allows[0].ruleName} (priority=${allows[0].priority})`,
87
+ ],
88
+ };
89
+ }
90
+ const sorted = [...candidates].sort((a, b) => b.priority - a.priority);
91
+ return {
92
+ winner: sorted[0],
93
+ trace: ['ALLOW_OVERRIDES: no allow rules, selecting highest-priority deny'],
94
+ };
95
+ }
96
+ priorityFirstMatch(candidates) {
97
+ const sorted = [...candidates].sort((a, b) => b.priority - a.priority);
98
+ return {
99
+ winner: sorted[0],
100
+ trace: [
101
+ `PRIORITY_FIRST_MATCH: ${candidates.length} candidates`,
102
+ `Winner: ${sorted[0].ruleName} (priority=${sorted[0].priority}, action=${sorted[0].action})`,
103
+ ],
104
+ };
105
+ }
106
+ mostSpecificWins(candidates) {
107
+ const sorted = [...candidates].sort((a, b) => {
108
+ const specDiff = (SCOPE_SPECIFICITY[b.scope] ?? 0) - (SCOPE_SPECIFICITY[a.scope] ?? 0);
109
+ if (specDiff !== 0)
110
+ return specDiff;
111
+ return b.priority - a.priority;
112
+ });
113
+ return {
114
+ winner: sorted[0],
115
+ trace: [
116
+ `MOST_SPECIFIC_WINS: ${candidates.length} candidates`,
117
+ `Winner: ${sorted[0].ruleName} (scope=${sorted[0].scope}, priority=${sorted[0].priority})`,
118
+ ],
119
+ };
120
+ }
121
+ }
122
+ exports.PolicyConflictResolver = PolicyConflictResolver;
123
+ // ΓöÇΓöÇ Expression Evaluator ΓöÇΓöÇ
124
+ function getNestedValue(obj, path) {
125
+ const parts = path.split('.');
126
+ let current = obj;
127
+ for (const part of parts) {
128
+ if (current === null || current === undefined)
129
+ return undefined;
130
+ if (typeof current === 'object') {
131
+ current = current[part];
132
+ }
133
+ else {
134
+ return undefined;
135
+ }
136
+ }
137
+ return current;
138
+ }
139
+ /**
140
+ * Split `expr` on the literal separator `sep` only at positions that are
141
+ * outside of single- or double-quoted string literals. A naive
142
+ * `String.split(' or ')` mis-splits expressions like
143
+ * `name == 'foo or bar'` because the operator substring also appears inside
144
+ * the string literal; this helper walks the expression once, tracking quote
145
+ * state, and only treats separator occurrences outside quotes as boundaries.
146
+ * If no out-of-quote occurrence is found, the result has length 1 and the
147
+ * caller falls through to the comparison-operator branches.
148
+ */
149
+ function splitOutsideQuotes(expr, sep) {
150
+ const parts = [];
151
+ let start = 0;
152
+ let inSingle = false;
153
+ let inDouble = false;
154
+ for (let i = 0; i <= expr.length - sep.length; i++) {
155
+ const ch = expr[i];
156
+ if (ch === "'" && !inDouble) {
157
+ inSingle = !inSingle;
158
+ continue;
159
+ }
160
+ if (ch === '"' && !inSingle) {
161
+ inDouble = !inDouble;
162
+ continue;
163
+ }
164
+ if (!inSingle && !inDouble && expr.startsWith(sep, i)) {
165
+ parts.push(expr.slice(start, i));
166
+ start = i + sep.length;
167
+ i += sep.length - 1;
168
+ }
169
+ }
170
+ parts.push(expr.slice(start));
171
+ return parts;
172
+ }
173
+ /**
174
+ * Evaluate a condition expression string against a context dictionary.
175
+ * Supports: equality, inequality, numeric comparisons, `in` operator,
176
+ * boolean attributes, and compound `and`/`or`.
177
+ */
178
+ function evaluateExpression(expr, context) {
179
+ const trimmed = expr.trim();
180
+ // OR conditions (lowest precedence) — only split on out-of-quote separators
181
+ // so `name == 'foo or bar'` is not mis-tokenised.
182
+ const orParts = splitOutsideQuotes(trimmed, ' or ');
183
+ if (orParts.length > 1) {
184
+ return orParts.some((p) => evaluateExpression(p.trim(), context));
185
+ }
186
+ // AND conditions — same out-of-quote split.
187
+ const andParts = splitOutsideQuotes(trimmed, ' and ');
188
+ if (andParts.length > 1) {
189
+ return andParts.every((p) => evaluateExpression(p.trim(), context));
190
+ }
191
+ // NOT IN: path not in ['a', 'b']
192
+ const notInMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s+not\s+in\s+\[([^\]]*)\]$/);
193
+ if (notInMatch) {
194
+ const [, path, listStr] = notInMatch;
195
+ const actual = getNestedValue(context, path);
196
+ const items = parseListLiteral(listStr);
197
+ return !items.includes(String(actual));
198
+ }
199
+ // IN: path in ['a', 'b']
200
+ const inMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s+in\s+\[([^\]]*)\]$/);
201
+ if (inMatch) {
202
+ const [, path, listStr] = inMatch;
203
+ const actual = getNestedValue(context, path);
204
+ const items = parseListLiteral(listStr);
205
+ return items.includes(String(actual));
206
+ }
207
+ // NOT EQUALS: path != 'value' or path != number
208
+ const neqStrMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s*!=\s*['"]([^'"]*)['"]\s*$/);
209
+ if (neqStrMatch) {
210
+ const [, path, value] = neqStrMatch;
211
+ return getNestedValue(context, path) !== value;
212
+ }
213
+ const neqNumMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s*!=\s*(\d+(?:\.\d+)?)\s*$/);
214
+ if (neqNumMatch) {
215
+ const [, path, numStr] = neqNumMatch;
216
+ return Number(getNestedValue(context, path)) !== Number(numStr);
217
+ }
218
+ // EQUALS: path == 'value'
219
+ const eqStrMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s*==\s*['"]([^'"]*)['"]\s*$/);
220
+ if (eqStrMatch) {
221
+ const [, path, value] = eqStrMatch;
222
+ return getNestedValue(context, path) === value;
223
+ }
224
+ // EQUALS: path == number
225
+ const eqNumMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s*==\s*(\d+(?:\.\d+)?)\s*$/);
226
+ if (eqNumMatch) {
227
+ const [, path, numStr] = eqNumMatch;
228
+ return Number(getNestedValue(context, path)) === Number(numStr);
229
+ }
230
+ // EQUALS: path == true/false
231
+ const eqBoolMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s*==\s*(true|false)\s*$/);
232
+ if (eqBoolMatch) {
233
+ const [, path, boolStr] = eqBoolMatch;
234
+ return getNestedValue(context, path) === (boolStr === 'true');
235
+ }
236
+ // GREATER THAN OR EQUAL: path >= number
237
+ const gteMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s*>=\s*(\d+(?:\.\d+)?)\s*$/);
238
+ if (gteMatch) {
239
+ const [, path, numStr] = gteMatch;
240
+ return Number(getNestedValue(context, path)) >= Number(numStr);
241
+ }
242
+ // LESS THAN OR EQUAL: path <= number
243
+ const lteMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s*<=\s*(\d+(?:\.\d+)?)\s*$/);
244
+ if (lteMatch) {
245
+ const [, path, numStr] = lteMatch;
246
+ return Number(getNestedValue(context, path)) <= Number(numStr);
247
+ }
248
+ // GREATER THAN: path > number
249
+ const gtMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s*>\s*(\d+(?:\.\d+)?)\s*$/);
250
+ if (gtMatch) {
251
+ const [, path, numStr] = gtMatch;
252
+ return Number(getNestedValue(context, path)) > Number(numStr);
253
+ }
254
+ // LESS THAN: path < number
255
+ const ltMatch = trimmed.match(/^(\w+(?:\.\w+)*)\s*<\s*(\d+(?:\.\d+)?)\s*$/);
256
+ if (ltMatch) {
257
+ const [, path, numStr] = ltMatch;
258
+ return Number(getNestedValue(context, path)) < Number(numStr);
259
+ }
260
+ // BOOLEAN attribute: just a path (truthy check)
261
+ const boolAttrMatch = trimmed.match(/^(\w+(?:\.\w+)*)$/);
262
+ if (boolAttrMatch) {
263
+ const path = boolAttrMatch[1];
264
+ return Boolean(getNestedValue(context, path));
265
+ }
266
+ return false;
267
+ }
268
+ function parseListLiteral(s) {
269
+ return s
270
+ .split(',')
271
+ .map((item) => item.trim().replace(/^['"]|['"]$/g, ''))
272
+ .filter((item) => item.length > 0);
273
+ }
274
+ function parseLimit(limit) {
275
+ const parts = limit.split('/');
276
+ const count = parseInt(parts[0], 10);
277
+ const periodMap = {
278
+ second: 1_000,
279
+ minute: 60_000,
280
+ hour: 3_600_000,
281
+ day: 86_400_000,
282
+ };
283
+ const periodMs = periodMap[parts[1]] ?? 3_600_000;
284
+ return { count, periodMs };
285
+ }
286
+ // ΓöÇΓöÇ Policy Engine ΓöÇΓöÇ
287
+ /**
288
+ * Declarative policy engine with full parity to the Python/NET SDK.
289
+ *
290
+ * Features:
291
+ * - Rich policy document model with apiVersion, scope, default_action
292
+ * - Expression-based condition evaluation (equality, comparison, in, and/or)
293
+ * - Configurable conflict resolution (deny-overrides, allow-overrides, priority-first-match, most-specific-wins)
294
+ * - Rate limiting support
295
+ * - Approval workflows
296
+ * - Backward-compatible with v0.1 flat rules
297
+ */
298
+ class PolicyEngine {
299
+ _policies = new Map();
300
+ _rateLimits = new Map();
301
+ _resolver;
302
+ _backends = [];
303
+ /** Legacy flat rules for backward compatibility. */
304
+ _legacyRules = [];
305
+ constructor(rules, conflictStrategy) {
306
+ if (rules) {
307
+ this._legacyRules = [...rules];
308
+ }
309
+ this._resolver = new PolicyConflictResolver(conflictStrategy ?? types_1.ConflictResolutionStrategy.PriorityFirstMatch);
310
+ }
311
+ // ΓöÇΓöÇ Rich Policy API ΓöÇΓöÇ
312
+ /** Load a Policy document into the engine. */
313
+ loadPolicy(policy) {
314
+ this._policies.set(policy.name, policy);
315
+ }
316
+ /** Parse and load a YAML string as a Policy document. */
317
+ loadYaml(yamlContent) {
318
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
319
+ const yaml = require('js-yaml');
320
+ const data = yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA });
321
+ const policy = dataToPolicy(data);
322
+ this.loadPolicy(policy);
323
+ return policy;
324
+ }
325
+ /** Parse and load a JSON string as a Policy document. */
326
+ loadJson(jsonContent) {
327
+ const data = JSON.parse(jsonContent);
328
+ const policy = dataToPolicy(data);
329
+ this.loadPolicy(policy);
330
+ return policy;
331
+ }
332
+ /** Get a loaded policy by name. */
333
+ getPolicy(name) {
334
+ return this._policies.get(name);
335
+ }
336
+ /** List all loaded policy names. */
337
+ listPolicies() {
338
+ return [...this._policies.keys()];
339
+ }
340
+ /** Remove a policy by name. Returns true if found and removed. */
341
+ removePolicy(name) {
342
+ return this._policies.delete(name);
343
+ }
344
+ /** Clear all loaded policies. */
345
+ clearPolicies() {
346
+ this._policies.clear();
347
+ this._rateLimits.clear();
348
+ }
349
+ registerBackend(backend) {
350
+ this._backends.push(backend);
351
+ }
352
+ listBackends() {
353
+ return this._backends.map((backend) => backend.name);
354
+ }
355
+ clearBackends() {
356
+ this._backends = [];
357
+ }
358
+ /**
359
+ * Evaluate all applicable policies for an agent action.
360
+ * Returns a rich PolicyDecisionResult with matched rule, timing, etc.
361
+ */
362
+ evaluatePolicy(agentDid, context) {
363
+ const start = performance.now();
364
+ // Enrich a defensive copy of the context with wire-protocol facets
365
+ // (sql.*, k8s.*, etc.) so policy rules can reference protocol-level
366
+ // semantics. We never mutate the caller's outer dict; FacetRegistry
367
+ // also replaces sub-object slots with merged copies so caller-owned
368
+ // sub-objects are untouched. Failures inside individual extractors
369
+ // are swallowed and logged so a broken parser cannot block
370
+ // evaluation.
371
+ const evalContext = { ...context };
372
+ try {
373
+ (0, protocol_facets_1.extractProtocolFacets)(evalContext);
374
+ }
375
+ catch {
376
+ // Defensive: the registry already swallows per-extractor errors.
377
+ }
378
+ const applicable = [...this._policies.values()].filter((p) => policyAppliesTo(p, agentDid));
379
+ if (applicable.length > 0) {
380
+ const candidates = [];
381
+ for (const policy of applicable) {
382
+ let scope;
383
+ try {
384
+ scope = policy.scope;
385
+ if (!Object.values(types_1.PolicyScope).includes(scope))
386
+ scope = types_1.PolicyScope.Global;
387
+ }
388
+ catch {
389
+ scope = types_1.PolicyScope.Global;
390
+ }
391
+ for (const rule of policy.rules) {
392
+ if (rule.enabled === false)
393
+ continue;
394
+ const ruleAction = rule.ruleAction ?? asPolicyAction(rule.effect) ?? 'deny';
395
+ if (evaluateRuleCondition(rule, evalContext)) {
396
+ candidates.push({
397
+ action: ruleAction,
398
+ priority: rule.priority ?? 0,
399
+ scope,
400
+ policyName: policy.name,
401
+ ruleName: rule.name ?? 'unnamed',
402
+ reason: rule.description ?? `Rule ${rule.name ?? 'unnamed'} matched`,
403
+ approvers: rule.approvers ?? [],
404
+ });
405
+ }
406
+ }
407
+ }
408
+ if (candidates.length > 0) {
409
+ const result = this._resolver.resolve(candidates);
410
+ const winner = result.winningDecision;
411
+ const elapsed = performance.now() - start;
412
+ // Check rate limiting
413
+ const matchedRule = this.findRule(winner.policyName, winner.ruleName);
414
+ if (matchedRule?.limit) {
415
+ if (this.isRateLimited(matchedRule)) {
416
+ return {
417
+ allowed: false,
418
+ action: 'deny',
419
+ matchedRule: matchedRule.name,
420
+ policyName: winner.policyName,
421
+ reason: `Rate limited: ${matchedRule.limit}`,
422
+ approvers: [],
423
+ rateLimited: true,
424
+ evaluatedAt: new Date(),
425
+ evaluationMs: elapsed,
426
+ };
427
+ }
428
+ this.incrementRateLimit(matchedRule);
429
+ }
430
+ return {
431
+ // warn and log are advisory actions that still permit the request,
432
+ // matching the .NET and Python SDKs (parity fix).
433
+ allowed: ['allow', 'warn', 'log'].includes(winner.action),
434
+ action: winner.action,
435
+ matchedRule: winner.ruleName,
436
+ policyName: winner.policyName,
437
+ reason: winner.reason,
438
+ approvers: winner.action === 'require_approval' ? winner.approvers : [],
439
+ rateLimited: false,
440
+ evaluatedAt: new Date(),
441
+ evaluationMs: elapsed,
442
+ };
443
+ }
444
+ }
445
+ // No rules matched - fail closed.
446
+ const defaultAction = applicable.length > 0 ? (applicable[0].default_action ?? 'deny') : 'deny';
447
+ const elapsed = performance.now() - start;
448
+ return {
449
+ allowed: defaultAction === 'allow',
450
+ action: defaultAction,
451
+ reason: 'No matching rules, using default',
452
+ approvers: [],
453
+ rateLimited: false,
454
+ evaluatedAt: new Date(),
455
+ evaluationMs: elapsed,
456
+ };
457
+ }
458
+ async evaluateWithBackends(action, context = {}) {
459
+ const localDecision = this.evaluate(action, context);
460
+ if (localDecision === 'deny') {
461
+ return {
462
+ localDecision,
463
+ backendResults: [],
464
+ effectiveDecision: 'deny',
465
+ deniedBy: ['local'],
466
+ };
467
+ }
468
+ const backendResults = await Promise.all(this._backends.map((backend) => this.runActionBackend(backend, action, context)));
469
+ const effectiveDecision = resolveEffectiveDecision(localDecision, backendResults);
470
+ const deniedBy = collectDeniedBy(backendResults, effectiveDecision === 'deny');
471
+ return {
472
+ localDecision,
473
+ backendResults,
474
+ effectiveDecision,
475
+ deniedBy,
476
+ };
477
+ }
478
+ async evaluatePolicyWithBackends(agentDid, context) {
479
+ const localDecision = this.evaluatePolicy(agentDid, context);
480
+ if (!localDecision.allowed) {
481
+ const effectiveDecision = localDecision.action === 'require_approval' ? 'review' : 'deny';
482
+ return {
483
+ localDecision,
484
+ backendResults: [],
485
+ effectiveDecision,
486
+ effectivePolicyResult: localDecision,
487
+ deniedBy: effectiveDecision === 'deny' ? ['local'] : [],
488
+ };
489
+ }
490
+ const backendResults = await Promise.all(this._backends.map((backend) => this.runPolicyBackend(backend, agentDid, context)));
491
+ const effectiveDecision = resolveEffectiveDecision('allow', backendResults);
492
+ const deniedBy = collectDeniedBy(backendResults, effectiveDecision === 'deny');
493
+ let effectivePolicyResult = localDecision;
494
+ if (effectiveDecision === 'deny') {
495
+ effectivePolicyResult = {
496
+ ...localDecision,
497
+ allowed: false,
498
+ action: 'deny',
499
+ reason: `Denied by policy backend: ${deniedBy.filter((name) => name !== 'local').join(', ')}`,
500
+ };
501
+ }
502
+ else if (effectiveDecision === 'review') {
503
+ effectivePolicyResult = {
504
+ ...localDecision,
505
+ allowed: false,
506
+ action: 'require_approval',
507
+ reason: 'Review required by policy backend',
508
+ };
509
+ }
510
+ return {
511
+ localDecision,
512
+ backendResults,
513
+ effectiveDecision,
514
+ effectivePolicyResult,
515
+ deniedBy,
516
+ };
517
+ }
518
+ // ΓöÇΓöÇ Legacy v0.1 API (backward compatible) ΓöÇΓöÇ
519
+ /** Load policy rules from a YAML file (legacy flat format). */
520
+ async loadFromYAML(yamlPath) {
521
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
522
+ const yaml = require('js-yaml');
523
+ const content = (0, fs_1.readFileSync)(yamlPath, 'utf-8');
524
+ const doc = yaml.load(content, { schema: yaml.JSON_SCHEMA });
525
+ if (doc?.rules && Array.isArray(doc.rules)) {
526
+ this._legacyRules.push(...doc.rules);
527
+ }
528
+ }
529
+ /**
530
+ * Evaluate using legacy flat rules (v0.1 compatible).
531
+ * First match wins; default is 'deny'.
532
+ */
533
+ evaluate(action, context = {}) {
534
+ // Enrich a defensive copy so the caller's dict is not mutated.
535
+ const evalContext = { ...context };
536
+ try {
537
+ (0, protocol_facets_1.extractProtocolFacets)(evalContext);
538
+ }
539
+ catch {
540
+ // Defensive: extractor failures must never block legacy evaluation.
541
+ }
542
+ for (const rule of this._legacyRules) {
543
+ if (this.matchAction(rule.action ?? '', action) &&
544
+ this.matchLegacyConditions((rule.condition ?? rule.conditions), evalContext)) {
545
+ return (rule.effect ?? 'deny');
546
+ }
547
+ }
548
+ return 'deny';
549
+ }
550
+ /** Append a legacy flat rule. */
551
+ addRule(rule) {
552
+ this._legacyRules.push(rule);
553
+ }
554
+ /** Return a snapshot of legacy flat rules. */
555
+ getRules() {
556
+ return [...this._legacyRules];
557
+ }
558
+ // ΓöÇΓöÇ Private helpers ΓöÇΓöÇ
559
+ matchAction(pattern, action) {
560
+ if (pattern === '*')
561
+ return true;
562
+ if (pattern.endsWith('.*')) {
563
+ const prefix = pattern.slice(0, -2);
564
+ return action === prefix || action.startsWith(prefix + '.');
565
+ }
566
+ return pattern === action;
567
+ }
568
+ matchLegacyConditions(conditions, context) {
569
+ if (!conditions)
570
+ return true;
571
+ for (const [key, expected] of Object.entries(conditions)) {
572
+ const actual = context[key];
573
+ if (actual !== expected)
574
+ return false;
575
+ }
576
+ return true;
577
+ }
578
+ findRule(policyName, ruleName) {
579
+ const policy = this._policies.get(policyName);
580
+ if (!policy)
581
+ return undefined;
582
+ return policy.rules.find((r) => r.name === ruleName);
583
+ }
584
+ isRateLimited(rule) {
585
+ if (!rule.limit || !rule.name)
586
+ return false;
587
+ const state = this._rateLimits.get(rule.name);
588
+ if (!state)
589
+ return false;
590
+ if (Date.now() > state.resetAt) {
591
+ this._rateLimits.delete(rule.name);
592
+ return false;
593
+ }
594
+ const { count } = parseLimit(rule.limit);
595
+ return state.count >= count;
596
+ }
597
+ incrementRateLimit(rule) {
598
+ if (!rule.limit || !rule.name)
599
+ return;
600
+ let state = this._rateLimits.get(rule.name);
601
+ if (!state) {
602
+ const { periodMs } = parseLimit(rule.limit);
603
+ state = { count: 0, resetAt: Date.now() + periodMs };
604
+ this._rateLimits.set(rule.name, state);
605
+ }
606
+ state.count++;
607
+ }
608
+ async runActionBackend(backend, action, context) {
609
+ if (!backend.evaluateAction) {
610
+ return {
611
+ backend: backend.name,
612
+ decision: 'allow',
613
+ reason: 'No action evaluator registered',
614
+ };
615
+ }
616
+ try {
617
+ const result = await backend.evaluateAction(action, context);
618
+ return normalizeBackendOutcome(backend.name, result);
619
+ }
620
+ catch (error) {
621
+ return {
622
+ backend: backend.name,
623
+ decision: 'deny',
624
+ error: error instanceof Error ? error.message : 'Unknown backend error',
625
+ };
626
+ }
627
+ }
628
+ async runPolicyBackend(backend, agentDid, context) {
629
+ if (!backend.evaluatePolicy) {
630
+ return {
631
+ backend: backend.name,
632
+ decision: 'allow',
633
+ reason: 'No policy evaluator registered',
634
+ };
635
+ }
636
+ try {
637
+ const result = await backend.evaluatePolicy(agentDid, context);
638
+ return normalizeBackendOutcome(backend.name, result);
639
+ }
640
+ catch (error) {
641
+ return {
642
+ backend: backend.name,
643
+ decision: 'deny',
644
+ error: error instanceof Error ? error.message : 'Unknown backend error',
645
+ };
646
+ }
647
+ }
648
+ }
649
+ exports.PolicyEngine = PolicyEngine;
650
+ // ΓöÇΓöÇ Helpers ΓöÇΓöÇ
651
+ function policyAppliesTo(policy, agentDid) {
652
+ if (policy.agent && policy.agent === agentDid)
653
+ return true;
654
+ if (policy.agents) {
655
+ if (policy.agents.includes(agentDid))
656
+ return true;
657
+ if (policy.agents.includes('*'))
658
+ return true;
659
+ }
660
+ // If no agent/agents specified, policy applies to all
661
+ if (!policy.agent && (!policy.agents || policy.agents.length === 0))
662
+ return true;
663
+ return false;
664
+ }
665
+ function evaluateRuleCondition(rule, context) {
666
+ if (!rule.condition)
667
+ return true;
668
+ // String expression (rich policy format)
669
+ if (typeof rule.condition === 'string') {
670
+ try {
671
+ return evaluateExpression(rule.condition, context);
672
+ }
673
+ catch {
674
+ return false;
675
+ }
676
+ }
677
+ // Legacy flat conditions object
678
+ if (typeof rule.condition === 'object') {
679
+ for (const [key, expected] of Object.entries(rule.condition)) {
680
+ if (context[key] !== expected)
681
+ return false;
682
+ }
683
+ return true;
684
+ }
685
+ return true;
686
+ }
687
+ function dataToPolicy(data) {
688
+ const rules = [];
689
+ const rawRules = data.rules;
690
+ if (rawRules && Array.isArray(rawRules)) {
691
+ for (const r of rawRules) {
692
+ rules.push({
693
+ name: r.name,
694
+ description: r.description,
695
+ condition: r.condition,
696
+ action: r.action,
697
+ effect: r.effect,
698
+ ruleAction: asPolicyAction(r.ruleAction),
699
+ limit: r.limit,
700
+ approvers: r.approvers,
701
+ priority: r.priority,
702
+ enabled: r.enabled,
703
+ surfaces: r.surfaces,
704
+ });
705
+ }
706
+ }
707
+ return {
708
+ apiVersion: data.apiVersion ?? 'governance.toolkit/v1',
709
+ version: data.version,
710
+ name: data.name,
711
+ description: data.description,
712
+ agent: data.agent,
713
+ agents: data.agents,
714
+ scope: data.scope ?? 'global',
715
+ rules,
716
+ default_action: data.default_action ?? 'deny',
717
+ };
718
+ }
719
+ function normalizeBackendOutcome(backendName, result) {
720
+ if (typeof result === 'string') {
721
+ return {
722
+ backend: backendName,
723
+ decision: result,
724
+ };
725
+ }
726
+ if ('backend' in result && 'decision' in result) {
727
+ return {
728
+ backend: result.backend || backendName,
729
+ decision: result.decision,
730
+ reason: result.reason,
731
+ error: result.error,
732
+ };
733
+ }
734
+ return {
735
+ backend: backendName,
736
+ decision: result.allowed ? 'allow' : 'deny',
737
+ reason: result.reason,
738
+ };
739
+ }
740
+ function resolveEffectiveDecision(localDecision, backendResults) {
741
+ if (localDecision === 'deny') {
742
+ return 'deny';
743
+ }
744
+ if (backendResults.some((result) => result.decision === 'deny' || result.error)) {
745
+ return 'deny';
746
+ }
747
+ if (localDecision === 'review' || backendResults.some((result) => result.decision === 'review')) {
748
+ return 'review';
749
+ }
750
+ return 'allow';
751
+ }
752
+ function collectDeniedBy(backendResults, includeBackends) {
753
+ if (!includeBackends) {
754
+ return [];
755
+ }
756
+ return backendResults
757
+ .filter((result) => result.decision === 'deny' || result.error)
758
+ .map((result) => result.backend);
759
+ }
760
+ //# sourceMappingURL=policy.js.map