sovr-mcp-proxy 7.0.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,502 @@
1
+ // src/teamPolicyManager.ts
2
+ var ROLE_HIERARCHY = {
3
+ admin: 100,
4
+ lead: 75,
5
+ developer: 50,
6
+ viewer: 25,
7
+ custom: 0
8
+ };
9
+ var DEFAULT_ROLE_RULES = {
10
+ admin: [
11
+ {
12
+ id: "admin-full-access",
13
+ name: "Admin full access",
14
+ action: "allow",
15
+ tools: ["*"],
16
+ priority: 1e3,
17
+ enabled: true,
18
+ description: "Admins have full access to all tools"
19
+ }
20
+ ],
21
+ lead: [
22
+ {
23
+ id: "lead-read-write",
24
+ name: "Lead read/write access",
25
+ action: "allow",
26
+ tools: ["*"],
27
+ conditions: [{ field: "risk_score", operator: "lt", value: 70 }],
28
+ priority: 900,
29
+ enabled: true,
30
+ description: "Leads can use tools with risk < 70"
31
+ },
32
+ {
33
+ id: "lead-high-risk-approval",
34
+ name: "Lead high-risk requires approval",
35
+ action: "require-approval",
36
+ tools: ["*"],
37
+ conditions: [{ field: "risk_score", operator: "gte", value: 70 }],
38
+ priority: 901,
39
+ enabled: true,
40
+ description: "High-risk actions require approval for leads"
41
+ }
42
+ ],
43
+ developer: [
44
+ {
45
+ id: "dev-safe-access",
46
+ name: "Developer safe access",
47
+ action: "allow",
48
+ tools: ["*"],
49
+ conditions: [{ field: "risk_score", operator: "lt", value: 40 }],
50
+ priority: 800,
51
+ enabled: true,
52
+ description: "Developers can use safe tools freely"
53
+ },
54
+ {
55
+ id: "dev-moderate-approval",
56
+ name: "Developer moderate risk requires approval",
57
+ action: "require-approval",
58
+ tools: ["*"],
59
+ conditions: [{ field: "risk_score", operator: "gte", value: 40 }],
60
+ priority: 801,
61
+ enabled: true,
62
+ description: "Moderate+ risk requires approval for developers"
63
+ }
64
+ ],
65
+ viewer: [
66
+ {
67
+ id: "viewer-readonly",
68
+ name: "Viewer read-only",
69
+ action: "allow",
70
+ tools: ["read_*", "get_*", "list_*", "search_*", "view_*"],
71
+ priority: 700,
72
+ enabled: true,
73
+ description: "Viewers can only use read-only tools"
74
+ },
75
+ {
76
+ id: "viewer-block-write",
77
+ name: "Viewer block writes",
78
+ action: "block",
79
+ tools: ["*"],
80
+ priority: 699,
81
+ enabled: true,
82
+ description: "Block all non-read tools for viewers"
83
+ }
84
+ ],
85
+ custom: []
86
+ };
87
+ var TeamPolicyManager = class {
88
+ policySet;
89
+ constructor(name, settings) {
90
+ this.policySet = {
91
+ name,
92
+ activeVersion: 0,
93
+ versions: [],
94
+ members: [],
95
+ settings: {
96
+ defaultAction: settings?.defaultAction ?? "require-approval",
97
+ conflictResolution: settings?.conflictResolution ?? "most-restrictive",
98
+ enableCanary: settings?.enableCanary ?? false,
99
+ canaryPercentage: settings?.canaryPercentage ?? 10,
100
+ requireApprovalForChanges: settings?.requireApprovalForChanges ?? true,
101
+ notificationWebhook: settings?.notificationWebhook
102
+ }
103
+ };
104
+ this.createVersion("system", "Initial default policy", this.flattenDefaultRules());
105
+ }
106
+ // ─── Member Management ─────────────────────────────────────────────────
107
+ /** Add a team member */
108
+ addMember(member) {
109
+ if (this.policySet.members.find((m) => m.id === member.id)) {
110
+ throw new Error(`Member ${member.id} already exists`);
111
+ }
112
+ this.policySet.members.push({ ...member, active: true });
113
+ }
114
+ /** Update a team member */
115
+ updateMember(id, updates) {
116
+ const member = this.policySet.members.find((m) => m.id === id);
117
+ if (!member) throw new Error(`Member ${id} not found`);
118
+ Object.assign(member, updates);
119
+ }
120
+ /** Remove a team member (soft delete) */
121
+ deactivateMember(id) {
122
+ const member = this.policySet.members.find((m) => m.id === id);
123
+ if (!member) throw new Error(`Member ${id} not found`);
124
+ member.active = false;
125
+ }
126
+ /** Get all active members */
127
+ getMembers() {
128
+ return this.policySet.members.filter((m) => m.active);
129
+ }
130
+ /** Get member by ID */
131
+ getMember(id) {
132
+ return this.policySet.members.find((m) => m.id === id && m.active);
133
+ }
134
+ // ─── Policy Version Management ─────────────────────────────────────────
135
+ /** Create a new policy version */
136
+ createVersion(createdBy, description, rules, roleRules) {
137
+ const version = this.policySet.versions.length + 1;
138
+ const effectiveRoleRules = roleRules ?? this.generateRoleRules(rules);
139
+ const policyVersion = {
140
+ version,
141
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
142
+ createdBy,
143
+ description,
144
+ rules,
145
+ roleRules: effectiveRoleRules,
146
+ active: true,
147
+ hash: this.computeHash(rules)
148
+ };
149
+ for (const v of this.policySet.versions) {
150
+ v.active = false;
151
+ }
152
+ this.policySet.versions.push(policyVersion);
153
+ this.policySet.activeVersion = version;
154
+ return policyVersion;
155
+ }
156
+ /** Rollback to a previous version */
157
+ rollback(targetVersion) {
158
+ const version = this.policySet.versions.find((v) => v.version === targetVersion);
159
+ if (!version) throw new Error(`Version ${targetVersion} not found`);
160
+ for (const v of this.policySet.versions) {
161
+ v.active = false;
162
+ }
163
+ version.active = true;
164
+ this.policySet.activeVersion = targetVersion;
165
+ return version;
166
+ }
167
+ /** Get all versions */
168
+ getVersions() {
169
+ return [...this.policySet.versions];
170
+ }
171
+ /** Get active version */
172
+ getActiveVersion() {
173
+ return this.policySet.versions.find((v) => v.active);
174
+ }
175
+ /** Compare two versions */
176
+ diffVersions(v1, v2) {
177
+ const ver1 = this.policySet.versions.find((v) => v.version === v1);
178
+ const ver2 = this.policySet.versions.find((v) => v.version === v2);
179
+ if (!ver1 || !ver2) throw new Error("Version not found");
180
+ const rules1Map = new Map(ver1.rules.map((r) => [r.id, r]));
181
+ const rules2Map = new Map(ver2.rules.map((r) => [r.id, r]));
182
+ const added = ver2.rules.filter((r) => !rules1Map.has(r.id));
183
+ const removed = ver1.rules.filter((r) => !rules2Map.has(r.id));
184
+ const modified = [];
185
+ for (const [id, rule1] of rules1Map) {
186
+ const rule2 = rules2Map.get(id);
187
+ if (rule2) {
188
+ const changes = [];
189
+ if (rule1.action !== rule2.action) changes.push(`action: ${rule1.action} \u2192 ${rule2.action}`);
190
+ if (rule1.priority !== rule2.priority) changes.push(`priority: ${rule1.priority} \u2192 ${rule2.priority}`);
191
+ if (rule1.enabled !== rule2.enabled) changes.push(`enabled: ${rule1.enabled} \u2192 ${rule2.enabled}`);
192
+ if (JSON.stringify(rule1.tools) !== JSON.stringify(rule2.tools)) changes.push("tools changed");
193
+ if (JSON.stringify(rule1.conditions) !== JSON.stringify(rule2.conditions)) changes.push("conditions changed");
194
+ if (changes.length > 0) modified.push({ ruleId: id, changes });
195
+ }
196
+ }
197
+ return { added, removed, modified };
198
+ }
199
+ // ─── Policy Evaluation ─────────────────────────────────────────────────
200
+ /** Evaluate a tool call against the policy for a specific member */
201
+ evaluate(memberId, toolName, context) {
202
+ const startTime = Date.now();
203
+ const member = this.getMember(memberId);
204
+ const activeVersion = this.getActiveVersion();
205
+ if (!activeVersion) {
206
+ return {
207
+ decision: this.policySet.settings.defaultAction,
208
+ matchedRules: [],
209
+ appliedOverrides: [],
210
+ effectiveRole: member?.role ?? "viewer",
211
+ policyVersion: 0,
212
+ durationMs: Date.now() - startTime
213
+ };
214
+ }
215
+ const effectiveRole = member?.role ?? "viewer";
216
+ const applicableRuleIds = this.getInheritedRuleIds(effectiveRole, activeVersion.roleRules);
217
+ const applicableRules = activeVersion.rules.filter((r) => applicableRuleIds.includes(r.id) && r.enabled).sort((a, b) => b.priority - a.priority);
218
+ const matchedRules = [];
219
+ const decisions = [];
220
+ for (const rule of applicableRules) {
221
+ if (this.matchesTool(toolName, rule.tools) && this.matchesConditions(rule.conditions, context)) {
222
+ matchedRules.push({ ruleId: rule.id, ruleName: rule.name, action: rule.action });
223
+ decisions.push({ action: rule.action, priority: rule.priority });
224
+ }
225
+ }
226
+ const appliedOverrides = [];
227
+ if (member?.overrides) {
228
+ for (const override of member.overrides) {
229
+ if (override.expiresAt && new Date(override.expiresAt) < /* @__PURE__ */ new Date()) continue;
230
+ const matchedRule = matchedRules.find((r) => r.ruleId === override.ruleId);
231
+ if (matchedRule) {
232
+ matchedRule.action = override.action;
233
+ appliedOverrides.push(override);
234
+ const decisionIdx = decisions.findIndex((d) => d.priority === applicableRules.find((r) => r.id === override.ruleId)?.priority);
235
+ if (decisionIdx >= 0) {
236
+ decisions[decisionIdx].action = override.action;
237
+ }
238
+ }
239
+ }
240
+ }
241
+ let finalDecision;
242
+ if (decisions.length === 0) {
243
+ finalDecision = this.policySet.settings.defaultAction;
244
+ } else {
245
+ finalDecision = this.resolveConflict(decisions);
246
+ }
247
+ return {
248
+ decision: finalDecision,
249
+ matchedRules,
250
+ appliedOverrides,
251
+ effectiveRole,
252
+ policyVersion: activeVersion.version,
253
+ durationMs: Date.now() - startTime
254
+ };
255
+ }
256
+ // ─── Canary Deployment ─────────────────────────────────────────────────
257
+ /** Check if a member should use the canary policy */
258
+ isInCanaryGroup(memberId) {
259
+ if (!this.policySet.settings.enableCanary) return false;
260
+ const member = this.getMember(memberId);
261
+ return member?.canaryGroup === "canary";
262
+ }
263
+ /** Assign members to canary group */
264
+ assignCanaryGroup(memberIds) {
265
+ for (const id of memberIds) {
266
+ const member = this.policySet.members.find((m) => m.id === id);
267
+ if (member) member.canaryGroup = "canary";
268
+ }
269
+ }
270
+ /** Promote canary to stable (all members get the canary policy) */
271
+ promoteCanary() {
272
+ for (const member of this.policySet.members) {
273
+ member.canaryGroup = void 0;
274
+ }
275
+ this.policySet.settings.enableCanary = false;
276
+ }
277
+ // ─── Import/Export ─────────────────────────────────────────────────────
278
+ /** Export policy as YAML-compatible object */
279
+ exportPolicy() {
280
+ const active = this.getActiveVersion();
281
+ return {
282
+ name: this.policySet.name,
283
+ version: active?.version ?? 0,
284
+ settings: this.policySet.settings,
285
+ rules: active?.rules.map((r) => ({
286
+ id: r.id,
287
+ name: r.name,
288
+ action: r.action,
289
+ tools: r.tools,
290
+ conditions: r.conditions,
291
+ priority: r.priority,
292
+ enabled: r.enabled,
293
+ description: r.description
294
+ })) ?? [],
295
+ roles: active?.roleRules ?? {},
296
+ members: this.policySet.members.map((m) => ({
297
+ id: m.id,
298
+ name: m.name,
299
+ role: m.role,
300
+ active: m.active,
301
+ tags: m.tags
302
+ }))
303
+ };
304
+ }
305
+ /** Import policy from object */
306
+ importPolicy(data, importedBy) {
307
+ if (data.settings) {
308
+ Object.assign(this.policySet.settings, data.settings);
309
+ }
310
+ if (data.members) {
311
+ for (const member of data.members) {
312
+ if (!this.policySet.members.find((m) => m.id === member.id)) {
313
+ this.addMember(member);
314
+ }
315
+ }
316
+ }
317
+ return this.createVersion(
318
+ importedBy,
319
+ "Imported policy",
320
+ data.rules,
321
+ data.roles
322
+ );
323
+ }
324
+ /** Export as JSON string */
325
+ toJSON() {
326
+ return JSON.stringify(this.exportPolicy(), null, 2);
327
+ }
328
+ // ─── Getters ───────────────────────────────────────────────────────────
329
+ get name() {
330
+ return this.policySet.name;
331
+ }
332
+ get settings() {
333
+ return { ...this.policySet.settings };
334
+ }
335
+ get memberCount() {
336
+ return this.policySet.members.filter((m) => m.active).length;
337
+ }
338
+ get versionCount() {
339
+ return this.policySet.versions.length;
340
+ }
341
+ // ─── Private ─────────────────────────────────────────────────────────────
342
+ flattenDefaultRules() {
343
+ const allRules = [];
344
+ for (const [, rules] of Object.entries(DEFAULT_ROLE_RULES)) {
345
+ for (const rule of rules) {
346
+ if (!allRules.find((r) => r.id === rule.id)) {
347
+ allRules.push(rule);
348
+ }
349
+ }
350
+ }
351
+ return allRules;
352
+ }
353
+ generateRoleRules(rules) {
354
+ const result = {
355
+ admin: [],
356
+ lead: [],
357
+ developer: [],
358
+ viewer: [],
359
+ custom: []
360
+ };
361
+ result.admin = rules.map((r) => r.id);
362
+ for (const role of ["lead", "developer", "viewer"]) {
363
+ const defaultIds = DEFAULT_ROLE_RULES[role].map((r) => r.id);
364
+ const customIds = rules.filter((r) => !Object.values(DEFAULT_ROLE_RULES).flat().find((dr) => dr.id === r.id)).map((r) => r.id);
365
+ result[role] = [...defaultIds, ...customIds];
366
+ }
367
+ return result;
368
+ }
369
+ getInheritedRuleIds(role, roleRules) {
370
+ const ruleIds = /* @__PURE__ */ new Set();
371
+ const ownRules = roleRules[role] ?? [];
372
+ for (const id of ownRules) ruleIds.add(id);
373
+ const roleLevel = ROLE_HIERARCHY[role];
374
+ for (const [r, level] of Object.entries(ROLE_HIERARCHY)) {
375
+ if (level < roleLevel) {
376
+ const lowerRules = roleRules[r] ?? [];
377
+ for (const id of lowerRules) ruleIds.add(id);
378
+ }
379
+ }
380
+ return [...ruleIds];
381
+ }
382
+ matchesTool(toolName, patterns) {
383
+ for (const pattern of patterns) {
384
+ if (pattern === "*") return true;
385
+ if (pattern.endsWith("*")) {
386
+ if (toolName.startsWith(pattern.slice(0, -1))) return true;
387
+ } else if (pattern.startsWith("*")) {
388
+ if (toolName.endsWith(pattern.slice(1))) return true;
389
+ } else if (pattern === toolName) {
390
+ return true;
391
+ }
392
+ }
393
+ return false;
394
+ }
395
+ matchesConditions(conditions, context) {
396
+ if (!conditions || conditions.length === 0) return true;
397
+ for (const condition of conditions) {
398
+ let fieldValue;
399
+ switch (condition.field) {
400
+ case "risk_score":
401
+ fieldValue = context.riskScore;
402
+ break;
403
+ case "time_of_day":
404
+ fieldValue = (/* @__PURE__ */ new Date()).getHours();
405
+ break;
406
+ case "day_of_week":
407
+ fieldValue = (/* @__PURE__ */ new Date()).getDay();
408
+ break;
409
+ default:
410
+ fieldValue = void 0;
411
+ }
412
+ if (fieldValue === void 0) continue;
413
+ if (!this.evaluateCondition(fieldValue, condition.operator, condition.value)) {
414
+ return false;
415
+ }
416
+ }
417
+ return true;
418
+ }
419
+ evaluateCondition(fieldValue, operator, conditionValue) {
420
+ switch (operator) {
421
+ case "eq":
422
+ return fieldValue === conditionValue;
423
+ case "neq":
424
+ return fieldValue !== conditionValue;
425
+ case "gt":
426
+ return fieldValue > conditionValue;
427
+ case "lt":
428
+ return fieldValue < conditionValue;
429
+ case "gte":
430
+ return fieldValue >= conditionValue;
431
+ case "lte":
432
+ return fieldValue <= conditionValue;
433
+ case "contains":
434
+ return String(fieldValue).includes(String(conditionValue));
435
+ case "matches":
436
+ return new RegExp(String(conditionValue)).test(String(fieldValue));
437
+ case "in":
438
+ return Array.isArray(conditionValue) && conditionValue.includes(String(fieldValue));
439
+ default:
440
+ return false;
441
+ }
442
+ }
443
+ resolveConflict(decisions) {
444
+ if (decisions.length === 0) return this.policySet.settings.defaultAction;
445
+ switch (this.policySet.settings.conflictResolution) {
446
+ case "most-restrictive": {
447
+ const restrictiveness = {
448
+ "block": 100,
449
+ "require-approval": 75,
450
+ "transform": 50,
451
+ "log-only": 25,
452
+ "allow": 0
453
+ };
454
+ return decisions.reduce(
455
+ (most, d) => (restrictiveness[d.action] ?? 0) > (restrictiveness[most.action] ?? 0) ? d : most
456
+ ).action;
457
+ }
458
+ case "least-restrictive": {
459
+ const permissiveness = {
460
+ "allow": 100,
461
+ "log-only": 75,
462
+ "transform": 50,
463
+ "require-approval": 25,
464
+ "block": 0
465
+ };
466
+ return decisions.reduce(
467
+ (least, d) => (permissiveness[d.action] ?? 0) > (permissiveness[least.action] ?? 0) ? d : least
468
+ ).action;
469
+ }
470
+ case "priority-based":
471
+ default:
472
+ return decisions.sort((a, b) => b.priority - a.priority)[0].action;
473
+ }
474
+ }
475
+ computeHash(rules) {
476
+ const str = JSON.stringify(rules);
477
+ let hash = 0;
478
+ for (let i = 0; i < str.length; i++) {
479
+ const char = str.charCodeAt(i);
480
+ hash = (hash << 5) - hash + char;
481
+ hash |= 0;
482
+ }
483
+ return `v${Math.abs(hash).toString(16).padStart(8, "0")}`;
484
+ }
485
+ };
486
+ function createTeamPolicyManager(name, settings) {
487
+ return new TeamPolicyManager(name, settings);
488
+ }
489
+ function createEnterprisePolicyManager(name) {
490
+ return new TeamPolicyManager(name, {
491
+ defaultAction: "block",
492
+ conflictResolution: "most-restrictive",
493
+ requireApprovalForChanges: true,
494
+ enableCanary: true,
495
+ canaryPercentage: 10
496
+ });
497
+ }
498
+ export {
499
+ TeamPolicyManager,
500
+ createEnterprisePolicyManager,
501
+ createTeamPolicyManager
502
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sovr-mcp-proxy",
3
- "version": "7.0.0",
3
+ "version": "7.2.0",
4
4
  "description": "SOVR MCP Proxy — intercepts MCP tool calls and evaluates them against the unified Policy Engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -25,7 +25,7 @@
25
25
  "README.md"
26
26
  ],
27
27
  "scripts": {
28
- "build": "tsup src/index.ts src/cli.ts src/init.ts src/usageTracker.ts src/apiKeyManager.ts src/daemon.ts src/engine-export.ts src/toolReplacement.ts src/whitelistEngine.ts src/commandNormalizer.ts src/hooksAdapter.ts --format cjs,esm --dts --clean",
28
+ "build": "tsup src/index.ts src/cli.ts src/init.ts src/usageTracker.ts src/apiKeyManager.ts src/daemon.ts src/engine-export.ts src/toolReplacement.ts src/whitelistEngine.ts src/commandNormalizer.ts src/hooksAdapter.ts src/mcpProxyInterceptor.ts src/semanticAnalyzer.ts src/auditDashboard.ts src/teamPolicyManager.ts --format cjs,esm --dts --clean",
29
29
  "test": "vitest run --config vitest.config.ts",
30
30
  "lint": "tsc --noEmit",
31
31
  "prepublishOnly": "pnpm build"