sovr-mcp-proxy 6.0.1 → 7.1.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.
- package/LICENSE +54 -19
- package/README.md +387 -164
- package/dist/auditDashboard.d.mts +208 -0
- package/dist/auditDashboard.d.ts +208 -0
- package/dist/auditDashboard.js +398 -0
- package/dist/auditDashboard.mjs +370 -0
- package/dist/cli.js +1553 -24
- package/dist/cli.mjs +185 -18
- package/dist/commandNormalizer.d.mts +95 -0
- package/dist/commandNormalizer.d.ts +95 -0
- package/dist/commandNormalizer.js +365 -0
- package/dist/commandNormalizer.mjs +336 -0
- package/dist/hooksAdapter.d.mts +122 -0
- package/dist/hooksAdapter.d.ts +122 -0
- package/dist/hooksAdapter.js +321 -0
- package/dist/hooksAdapter.mjs +291 -0
- package/dist/index.js +1065 -2
- package/dist/index.mjs +98 -2
- package/dist/mcpProxyInterceptor.d.mts +256 -0
- package/dist/mcpProxyInterceptor.d.ts +256 -0
- package/dist/mcpProxyInterceptor.js +579 -0
- package/dist/mcpProxyInterceptor.mjs +552 -0
- package/dist/semanticAnalyzer.d.mts +133 -0
- package/dist/semanticAnalyzer.d.ts +133 -0
- package/dist/semanticAnalyzer.js +701 -0
- package/dist/semanticAnalyzer.mjs +674 -0
- package/dist/teamPolicyManager.d.mts +202 -0
- package/dist/teamPolicyManager.d.ts +202 -0
- package/dist/teamPolicyManager.js +529 -0
- package/dist/teamPolicyManager.mjs +502 -0
- package/dist/toolReplacement.d.mts +108 -0
- package/dist/toolReplacement.d.ts +108 -0
- package/dist/toolReplacement.js +234 -0
- package/dist/toolReplacement.mjs +204 -0
- package/dist/whitelistEngine.d.mts +167 -0
- package/dist/whitelistEngine.d.ts +167 -0
- package/dist/whitelistEngine.js +435 -0
- package/dist/whitelistEngine.mjs +403 -0
- package/package.json +46 -41
- package/server.json +0 -14
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sovr/proxy-mcp — Tool Replacement Mode (--mode=exclusive)
|
|
3
|
+
*
|
|
4
|
+
* Solves the FATAL bypass problem identified in Reddit feedback:
|
|
5
|
+
* "The LLM isn't forced to route commands through your MCP server."
|
|
6
|
+
*
|
|
7
|
+
* Solution: Instead of adding SOVR as an *additional* MCP tool alongside
|
|
8
|
+
* the native Bash tool, SOVR *replaces* the native Bash tool entirely.
|
|
9
|
+
* The LLM has no choice — every command goes through SOVR.
|
|
10
|
+
*
|
|
11
|
+
* How it works:
|
|
12
|
+
* 1. SOVR proxy intercepts the MCP `tools/list` response from upstream
|
|
13
|
+
* 2. In exclusive mode, it REMOVES dangerous native tools (Bash, shell, etc.)
|
|
14
|
+
* 3. It INJECTS SOVR-wrapped equivalents that enforce policy checks
|
|
15
|
+
* 4. When the LLM calls the SOVR tool, the proxy evaluates the command,
|
|
16
|
+
* then either forwards to the real tool or blocks it
|
|
17
|
+
*
|
|
18
|
+
* Supported modes:
|
|
19
|
+
* - "monitor" (default) — Log all tool calls, allow everything
|
|
20
|
+
* - "advisory" — Log + warn on risky calls, but allow
|
|
21
|
+
* - "enforce" — Block risky calls, allow safe ones
|
|
22
|
+
* - "exclusive" — Replace native tools entirely, ALL calls go through SOVR
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```
|
|
26
|
+
* # Start proxy in exclusive mode (recommended for production)
|
|
27
|
+
* npx sovr-mcp-proxy --upstream "..." --mode=exclusive
|
|
28
|
+
*
|
|
29
|
+
* # Start proxy in enforce mode (less intrusive)
|
|
30
|
+
* npx sovr-mcp-proxy --upstream "..." --mode=enforce
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
type ProxyMode = 'monitor' | 'advisory' | 'enforce' | 'exclusive';
|
|
34
|
+
/** Tools that are considered dangerous and should be replaced in exclusive mode */
|
|
35
|
+
interface ToolReplacementConfig {
|
|
36
|
+
/** Proxy enforcement mode */
|
|
37
|
+
mode: ProxyMode;
|
|
38
|
+
/** Tool names to intercept/replace (glob patterns supported) */
|
|
39
|
+
targetTools: string[];
|
|
40
|
+
/** Custom tool descriptions for replaced tools */
|
|
41
|
+
toolDescriptions?: Record<string, string>;
|
|
42
|
+
/** Whether to add a SOVR status tool */
|
|
43
|
+
addStatusTool: boolean;
|
|
44
|
+
/** Whether to add a SOVR audit tool */
|
|
45
|
+
addAuditTool: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** MCP Tool definition (from MCP spec) */
|
|
48
|
+
interface McpToolDef {
|
|
49
|
+
name: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object';
|
|
53
|
+
properties?: Record<string, unknown>;
|
|
54
|
+
required?: string[];
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Result of tool list transformation */
|
|
59
|
+
interface ToolListTransformResult {
|
|
60
|
+
/** The transformed tool list */
|
|
61
|
+
tools: McpToolDef[];
|
|
62
|
+
/** Tools that were removed */
|
|
63
|
+
removedTools: string[];
|
|
64
|
+
/** Tools that were added by SOVR */
|
|
65
|
+
addedTools: string[];
|
|
66
|
+
/** Tools that were wrapped (kept but intercepted) */
|
|
67
|
+
wrappedTools: string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Default dangerous tool patterns that should be replaced in exclusive mode.
|
|
71
|
+
* These are the native tools that AI coding agents typically have access to.
|
|
72
|
+
*/
|
|
73
|
+
declare const DEFAULT_TARGET_TOOLS: string[];
|
|
74
|
+
declare const DEFAULT_TOOL_REPLACEMENT_CONFIG: ToolReplacementConfig;
|
|
75
|
+
/**
|
|
76
|
+
* Transform the upstream MCP server's tool list based on the proxy mode.
|
|
77
|
+
*
|
|
78
|
+
* In exclusive mode:
|
|
79
|
+
* - Removes ALL native execution tools (Bash, shell, etc.)
|
|
80
|
+
* - Injects SOVR-wrapped equivalents
|
|
81
|
+
* - The LLM literally cannot bypass SOVR because the native tools don't exist
|
|
82
|
+
*
|
|
83
|
+
* In enforce mode:
|
|
84
|
+
* - Keeps native tools but wraps them with SOVR interception
|
|
85
|
+
* - Adds SOVR tools alongside native ones
|
|
86
|
+
*
|
|
87
|
+
* In advisory/monitor mode:
|
|
88
|
+
* - Keeps all native tools unchanged
|
|
89
|
+
* - Adds SOVR tools for optional use
|
|
90
|
+
*/
|
|
91
|
+
declare function transformToolList(upstreamTools: McpToolDef[], config: ToolReplacementConfig): ToolListTransformResult;
|
|
92
|
+
/**
|
|
93
|
+
* Determine if a tool call should be intercepted based on the proxy mode.
|
|
94
|
+
*/
|
|
95
|
+
declare function shouldIntercept(toolName: string, config: ToolReplacementConfig): {
|
|
96
|
+
intercept: boolean;
|
|
97
|
+
reason: string;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Generate a human-readable summary of the tool replacement configuration.
|
|
101
|
+
*/
|
|
102
|
+
declare function describeMode(config: ToolReplacementConfig): string;
|
|
103
|
+
/**
|
|
104
|
+
* Parse mode from CLI argument string.
|
|
105
|
+
*/
|
|
106
|
+
declare function parseMode(input: string): ProxyMode;
|
|
107
|
+
|
|
108
|
+
export { DEFAULT_TARGET_TOOLS, DEFAULT_TOOL_REPLACEMENT_CONFIG, type McpToolDef, type ProxyMode, type ToolListTransformResult, type ToolReplacementConfig, describeMode, parseMode, shouldIntercept, transformToolList };
|