@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.
- package/README.md +363 -0
- package/dist/audit.d.ts +26 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +107 -0
- package/dist/audit.js.map +1 -0
- package/dist/cascade-containment.d.ts +84 -0
- package/dist/cascade-containment.d.ts.map +1 -0
- package/dist/cascade-containment.js +387 -0
- package/dist/cascade-containment.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +112 -0
- package/dist/client.js.map +1 -0
- package/dist/context-poisoning.d.ts +58 -0
- package/dist/context-poisoning.d.ts.map +1 -0
- package/dist/context-poisoning.js +410 -0
- package/dist/context-poisoning.js.map +1 -0
- package/dist/credential-vault.d.ts +213 -0
- package/dist/credential-vault.d.ts.map +1 -0
- package/dist/credential-vault.js +567 -0
- package/dist/credential-vault.js.map +1 -0
- package/dist/discovery.d.ts +80 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +404 -0
- package/dist/discovery.js.map +1 -0
- package/dist/encryption/channel.d.ts +29 -0
- package/dist/encryption/channel.d.ts.map +1 -0
- package/dist/encryption/channel.js +78 -0
- package/dist/encryption/channel.js.map +1 -0
- package/dist/encryption/index.d.ts +9 -0
- package/dist/encryption/index.d.ts.map +1 -0
- package/dist/encryption/index.js +23 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/mesh-client.d.ts +281 -0
- package/dist/encryption/mesh-client.d.ts.map +1 -0
- package/dist/encryption/mesh-client.js +1110 -0
- package/dist/encryption/mesh-client.js.map +1 -0
- package/dist/encryption/ratchet.d.ts +38 -0
- package/dist/encryption/ratchet.d.ts.map +1 -0
- package/dist/encryption/ratchet.js +223 -0
- package/dist/encryption/ratchet.js.map +1 -0
- package/dist/encryption/registry-client.d.ts +136 -0
- package/dist/encryption/registry-client.d.ts.map +1 -0
- package/dist/encryption/registry-client.js +369 -0
- package/dist/encryption/registry-client.js.map +1 -0
- package/dist/encryption/x3dh.d.ts +80 -0
- package/dist/encryption/x3dh.d.ts.map +1 -0
- package/dist/encryption/x3dh.js +238 -0
- package/dist/encryption/x3dh.js.map +1 -0
- package/dist/framework-adapter.d.ts +63 -0
- package/dist/framework-adapter.d.ts.map +1 -0
- package/dist/framework-adapter.js +253 -0
- package/dist/framework-adapter.js.map +1 -0
- package/dist/identity.d.ts +124 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +446 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/kill-switch.d.ts +22 -0
- package/dist/kill-switch.d.ts.map +1 -0
- package/dist/kill-switch.js +60 -0
- package/dist/kill-switch.js.map +1 -0
- package/dist/lifecycle.d.ts +50 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +113 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/mcp.d.ts +41 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +233 -0
- package/dist/mcp.js.map +1 -0
- package/dist/metrics.d.ts +123 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +289 -0
- package/dist/metrics.js.map +1 -0
- package/dist/oci-manifest.d.ts +49 -0
- package/dist/oci-manifest.d.ts.map +1 -0
- package/dist/oci-manifest.js +182 -0
- package/dist/oci-manifest.js.map +1 -0
- package/dist/policy-backends/cedar.d.ts +15 -0
- package/dist/policy-backends/cedar.d.ts.map +1 -0
- package/dist/policy-backends/cedar.js +88 -0
- package/dist/policy-backends/cedar.js.map +1 -0
- package/dist/policy-backends/opa.d.ts +17 -0
- package/dist/policy-backends/opa.d.ts.map +1 -0
- package/dist/policy-backends/opa.js +74 -0
- package/dist/policy-backends/opa.js.map +1 -0
- package/dist/policy.d.ts +77 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +760 -0
- package/dist/policy.js.map +1 -0
- package/dist/prompt-defense.d.ts +37 -0
- package/dist/prompt-defense.d.ts.map +1 -0
- package/dist/prompt-defense.js +229 -0
- package/dist/prompt-defense.js.map +1 -0
- package/dist/protocol-facets.d.ts +87 -0
- package/dist/protocol-facets.d.ts.map +1 -0
- package/dist/protocol-facets.js +728 -0
- package/dist/protocol-facets.js.map +1 -0
- package/dist/rings.d.ts +17 -0
- package/dist/rings.d.ts.map +1 -0
- package/dist/rings.js +61 -0
- package/dist/rings.js.map +1 -0
- package/dist/sandbox.d.ts +61 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +199 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/surface-parity.d.ts +45 -0
- package/dist/surface-parity.d.ts.map +1 -0
- package/dist/surface-parity.js +216 -0
- package/dist/surface-parity.js.map +1 -0
- package/dist/trust.d.ts +28 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +209 -0
- package/dist/trust.js.map +1 -0
- package/dist/types.d.ts +422 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +135 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +356 -0
- package/dist/verify.js.map +1 -0
- 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
|