sovr-mcp-proxy 6.0.1 → 7.0.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,403 @@
1
+ // src/whitelistEngine.ts
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ var PRESET_READONLY = {
5
+ version: "1.0",
6
+ mode: "whitelist",
7
+ rules: [
8
+ { pattern: "ls *", allow: true, description: "List directory" },
9
+ { pattern: "cat *", allow: true, description: "Read file" },
10
+ { pattern: "head *", allow: true, description: "Read file head" },
11
+ { pattern: "tail *", allow: true, description: "Read file tail" },
12
+ { pattern: "find *", allow: true, description: "Find files", max_args: 10 },
13
+ { pattern: "grep *", allow: true, description: "Search in files" },
14
+ { pattern: "wc *", allow: true, description: "Word count" },
15
+ { pattern: "file *", allow: true, description: "File type" },
16
+ { pattern: "pwd", allow: true, description: "Print working directory" },
17
+ { pattern: "whoami", allow: true, description: "Current user" },
18
+ { pattern: "date", allow: true, description: "Current date" },
19
+ { pattern: "echo *", allow: true, description: "Echo text" }
20
+ ],
21
+ settings: {
22
+ max_command_length: 500,
23
+ allow_env_expansion: false,
24
+ allow_subshell: false,
25
+ allow_pipes: true,
26
+ allow_chains: false,
27
+ allow_redirects: false
28
+ }
29
+ };
30
+ var PRESET_DEVELOPER = {
31
+ version: "1.0",
32
+ mode: "whitelist",
33
+ rules: [
34
+ // Read operations
35
+ ...PRESET_READONLY.rules,
36
+ // Git (safe operations)
37
+ { pattern: "git status", allow: true, description: "Git status" },
38
+ { pattern: "git diff *", allow: true, description: "Git diff" },
39
+ { pattern: "git log *", allow: true, description: "Git log" },
40
+ { pattern: "git branch *", allow: true, description: "Git branch" },
41
+ { pattern: "git add *", allow: true, description: "Git add" },
42
+ { pattern: "git commit *", allow: true, description: "Git commit" },
43
+ { pattern: "git push *", allow: true, require_approval: true, description: "Git push (requires approval)" },
44
+ { pattern: "git pull *", allow: true, description: "Git pull" },
45
+ { pattern: "git checkout *", allow: true, description: "Git checkout" },
46
+ { pattern: "git stash *", allow: true, description: "Git stash" },
47
+ // Node.js / npm
48
+ { pattern: "node *", allow: true, description: "Run Node.js" },
49
+ { pattern: "npm run *", allow: true, description: "Run npm script" },
50
+ { pattern: "npm test", allow: true, description: "Run tests" },
51
+ { pattern: "npm install *", allow: true, description: "Install packages" },
52
+ { pattern: "npx *", allow: true, description: "Run npx" },
53
+ { pattern: "pnpm *", allow: true, description: "Run pnpm" },
54
+ { pattern: "yarn *", allow: true, description: "Run yarn" },
55
+ // Python
56
+ { pattern: "python3 *", allow: true, description: "Run Python", max_args: 20 },
57
+ { pattern: "pip install *", allow: true, description: "Install Python packages" },
58
+ { pattern: "pip3 install *", allow: true, description: "Install Python packages" },
59
+ // Build tools
60
+ { pattern: "make *", allow: true, description: "Run make" },
61
+ { pattern: "cargo *", allow: true, description: "Run cargo" },
62
+ // File operations (limited)
63
+ { pattern: "mkdir *", allow: true, description: "Create directory" },
64
+ { pattern: "cp *", allow: true, description: "Copy files" },
65
+ { pattern: "mv *", allow: true, description: "Move files" },
66
+ { pattern: "touch *", allow: true, description: "Create empty file" },
67
+ // Dangerous operations — require approval
68
+ { pattern: "rm *", allow: true, require_approval: true, description: "Delete files (requires approval)" },
69
+ { pattern: "npm publish *", allow: true, require_approval: true, description: "Publish package (requires approval)" },
70
+ { pattern: "docker *", allow: true, require_approval: true, description: "Docker operations (requires approval)" }
71
+ ],
72
+ settings: {
73
+ max_command_length: 2e3,
74
+ allow_env_expansion: true,
75
+ allow_subshell: false,
76
+ allow_pipes: true,
77
+ allow_chains: true,
78
+ allow_redirects: true
79
+ }
80
+ };
81
+ var PRESET_PRODUCTION = {
82
+ version: "1.0",
83
+ mode: "whitelist",
84
+ rules: [
85
+ { pattern: "echo *", allow: true, description: "Echo" },
86
+ { pattern: "date", allow: true, description: "Date" },
87
+ { pattern: "uptime", allow: true, description: "Uptime" },
88
+ { pattern: "df *", allow: true, description: "Disk usage" },
89
+ { pattern: "free *", allow: true, description: "Memory usage" },
90
+ { pattern: "ps *", allow: true, description: "Process list" },
91
+ { pattern: "curl *", allow: true, require_approval: true, description: "HTTP request (requires approval)" }
92
+ ],
93
+ default_action: "deny",
94
+ settings: {
95
+ max_command_length: 500,
96
+ allow_env_expansion: false,
97
+ allow_subshell: false,
98
+ allow_pipes: false,
99
+ allow_chains: false,
100
+ allow_redirects: false
101
+ }
102
+ };
103
+ var PRESETS = {
104
+ readonly: PRESET_READONLY,
105
+ developer: PRESET_DEVELOPER,
106
+ production: PRESET_PRODUCTION
107
+ };
108
+ var WhitelistEngine = class {
109
+ policy;
110
+ constructor(policy) {
111
+ this.policy = policy;
112
+ this.policy.rules.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
113
+ }
114
+ /**
115
+ * Evaluate a command against the whitelist policy.
116
+ */
117
+ evaluate(command) {
118
+ const violations = [];
119
+ const structuralCheck = this.checkStructural(command);
120
+ violations.push(...structuralCheck.violations);
121
+ if (structuralCheck.blocked) {
122
+ return {
123
+ allowed: false,
124
+ require_approval: false,
125
+ reason: `Structural violation: ${violations.join(", ")}`,
126
+ risk: "critical",
127
+ violations
128
+ };
129
+ }
130
+ const enabledRules = this.policy.rules.filter((r) => r.enabled !== false);
131
+ for (const rule of enabledRules) {
132
+ if (matchPattern(command, rule.pattern)) {
133
+ if (rule.max_args !== void 0) {
134
+ const argCount = command.split(/\s+/).length - 1;
135
+ if (argCount > rule.max_args) {
136
+ violations.push(`Too many arguments: ${argCount} > ${rule.max_args}`);
137
+ return {
138
+ allowed: false,
139
+ require_approval: false,
140
+ matched_rule: rule,
141
+ reason: `Argument limit exceeded for pattern "${rule.pattern}"`,
142
+ risk: "high",
143
+ violations
144
+ };
145
+ }
146
+ }
147
+ if (rule.allow) {
148
+ return {
149
+ allowed: true,
150
+ require_approval: rule.require_approval ?? false,
151
+ matched_rule: rule,
152
+ reason: rule.description || `Matched whitelist pattern: ${rule.pattern}`,
153
+ risk: rule.require_approval ? "medium" : "low",
154
+ violations
155
+ };
156
+ } else {
157
+ return {
158
+ allowed: false,
159
+ require_approval: false,
160
+ matched_rule: rule,
161
+ reason: rule.description || `Matched blacklist pattern: ${rule.pattern}`,
162
+ risk: "high",
163
+ violations
164
+ };
165
+ }
166
+ }
167
+ }
168
+ switch (this.policy.mode) {
169
+ case "whitelist":
170
+ return {
171
+ allowed: false,
172
+ require_approval: false,
173
+ reason: `No whitelist rule matches command. In whitelist mode, unlisted commands are DENIED.`,
174
+ risk: "high",
175
+ violations
176
+ };
177
+ case "blacklist":
178
+ return {
179
+ allowed: true,
180
+ require_approval: false,
181
+ reason: "No blacklist rule matches. Command allowed by default.",
182
+ risk: "medium",
183
+ violations
184
+ };
185
+ case "hybrid": {
186
+ const defaultAction = this.policy.default_action || "deny";
187
+ return {
188
+ allowed: defaultAction === "allow",
189
+ require_approval: defaultAction === "escalate",
190
+ reason: `No rule matches. Hybrid mode default: ${defaultAction}`,
191
+ risk: defaultAction === "allow" ? "medium" : "high",
192
+ violations
193
+ };
194
+ }
195
+ }
196
+ }
197
+ /**
198
+ * Check structural constraints (pipes, chains, subshells, etc.)
199
+ */
200
+ checkStructural(command) {
201
+ const violations = [];
202
+ const settings = this.policy.settings || {};
203
+ if (settings.max_command_length && command.length > settings.max_command_length) {
204
+ violations.push(`Command too long: ${command.length} > ${settings.max_command_length}`);
205
+ }
206
+ if (settings.allow_subshell === false) {
207
+ if (/\$\(/.test(command) || /`[^`]+`/.test(command)) {
208
+ violations.push("Subshell execution not allowed");
209
+ }
210
+ }
211
+ if (settings.allow_pipes === false) {
212
+ if (/\|(?!\|)/.test(command)) {
213
+ violations.push("Pipe chains not allowed");
214
+ }
215
+ }
216
+ if (settings.allow_chains === false) {
217
+ if (/[;&]|&&|\|\|/.test(command)) {
218
+ violations.push("Command chaining not allowed");
219
+ }
220
+ }
221
+ if (settings.allow_redirects === false) {
222
+ if (/[<>]|>>/.test(command)) {
223
+ violations.push("Output redirection not allowed");
224
+ }
225
+ }
226
+ if (settings.allow_env_expansion === false) {
227
+ if (/\$[A-Za-z_]/.test(command) || /\$\{/.test(command)) {
228
+ violations.push("Environment variable expansion not allowed");
229
+ }
230
+ }
231
+ return {
232
+ blocked: violations.length > 0,
233
+ violations
234
+ };
235
+ }
236
+ /**
237
+ * Get the current policy.
238
+ */
239
+ getPolicy() {
240
+ return { ...this.policy };
241
+ }
242
+ /**
243
+ * Add a rule dynamically.
244
+ */
245
+ addRule(rule) {
246
+ this.policy.rules.push(rule);
247
+ this.policy.rules.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
248
+ }
249
+ /**
250
+ * Remove a rule by pattern.
251
+ */
252
+ removeRule(pattern) {
253
+ const idx = this.policy.rules.findIndex((r) => r.pattern === pattern);
254
+ if (idx >= 0) {
255
+ this.policy.rules.splice(idx, 1);
256
+ return true;
257
+ }
258
+ return false;
259
+ }
260
+ /**
261
+ * List all rules.
262
+ */
263
+ listRules() {
264
+ return [...this.policy.rules];
265
+ }
266
+ };
267
+ function matchPattern(command, pattern) {
268
+ const normalizedCmd = command.trim().replace(/\s+/g, " ");
269
+ const normalizedPattern = pattern.trim().replace(/\s+/g, " ");
270
+ const regexStr = "^" + normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
271
+ try {
272
+ return new RegExp(regexStr).test(normalizedCmd);
273
+ } catch {
274
+ return false;
275
+ }
276
+ }
277
+ function loadPolicy(filePath) {
278
+ if (!existsSync(filePath)) {
279
+ throw new Error(`Policy file not found: ${filePath}`);
280
+ }
281
+ const raw = readFileSync(filePath, "utf-8");
282
+ try {
283
+ return JSON.parse(raw);
284
+ } catch {
285
+ }
286
+ return parseSimpleYaml(raw);
287
+ }
288
+ function autoLoadPolicy(projectDir) {
289
+ const candidates = [
290
+ join(projectDir, ".sovr", "policy.json"),
291
+ join(projectDir, ".sovr", "policy.yaml"),
292
+ join(projectDir, ".sovr", "policy.yml"),
293
+ join(projectDir, "sovr.policy.json")
294
+ ];
295
+ for (const candidate of candidates) {
296
+ if (existsSync(candidate)) {
297
+ return loadPolicy(candidate);
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+ function parseSimpleYaml(raw) {
303
+ const lines = raw.split("\n");
304
+ const policy = {
305
+ version: "1.0",
306
+ mode: "whitelist",
307
+ rules: []
308
+ };
309
+ let currentRule = null;
310
+ let inRules = false;
311
+ for (const line of lines) {
312
+ const trimmed = line.trim();
313
+ if (!trimmed || trimmed.startsWith("#")) continue;
314
+ if (trimmed.startsWith("version:")) {
315
+ policy.version = trimmed.split(":").slice(1).join(":").trim().replace(/['"]/g, "");
316
+ } else if (trimmed.startsWith("mode:")) {
317
+ policy.mode = trimmed.split(":").slice(1).join(":").trim().replace(/['"]/g, "");
318
+ } else if (trimmed === "rules:") {
319
+ inRules = true;
320
+ } else if (inRules && trimmed.startsWith("- pattern:")) {
321
+ if (currentRule?.pattern) {
322
+ policy.rules.push(currentRule);
323
+ }
324
+ currentRule = {
325
+ pattern: trimmed.replace("- pattern:", "").trim().replace(/['"]/g, ""),
326
+ allow: true,
327
+ enabled: true
328
+ };
329
+ } else if (inRules && currentRule) {
330
+ if (trimmed.startsWith("allow:")) {
331
+ currentRule.allow = trimmed.includes("true");
332
+ } else if (trimmed.startsWith("description:")) {
333
+ currentRule.description = trimmed.split(":").slice(1).join(":").trim().replace(/['"]/g, "");
334
+ } else if (trimmed.startsWith("require_approval:")) {
335
+ currentRule.require_approval = trimmed.includes("true");
336
+ } else if (trimmed.startsWith("max_args:")) {
337
+ currentRule.max_args = parseInt(trimmed.split(":")[1].trim());
338
+ } else if (trimmed.startsWith("priority:")) {
339
+ currentRule.priority = parseInt(trimmed.split(":")[1].trim());
340
+ }
341
+ }
342
+ }
343
+ if (currentRule?.pattern) {
344
+ policy.rules.push(currentRule);
345
+ }
346
+ return policy;
347
+ }
348
+ function generatePolicyFile(preset, format = "yaml") {
349
+ const policy = PRESETS[preset];
350
+ if (!policy) {
351
+ throw new Error(`Unknown preset: ${preset}. Available: ${Object.keys(PRESETS).join(", ")}`);
352
+ }
353
+ if (format === "json") {
354
+ return JSON.stringify(policy, null, 2);
355
+ }
356
+ let yaml = `# SOVR Whitelist Policy
357
+ `;
358
+ yaml += `# Preset: ${preset}
359
+ `;
360
+ yaml += `# Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
361
+
362
+ `;
363
+ yaml += `version: "${policy.version}"
364
+ `;
365
+ yaml += `mode: ${policy.mode}
366
+
367
+ `;
368
+ yaml += `rules:
369
+ `;
370
+ for (const rule of policy.rules) {
371
+ yaml += ` - pattern: "${rule.pattern}"
372
+ `;
373
+ yaml += ` allow: ${rule.allow}
374
+ `;
375
+ if (rule.description) yaml += ` description: "${rule.description}"
376
+ `;
377
+ if (rule.require_approval) yaml += ` require_approval: true
378
+ `;
379
+ if (rule.max_args !== void 0) yaml += ` max_args: ${rule.max_args}
380
+ `;
381
+ yaml += `
382
+ `;
383
+ }
384
+ if (policy.settings) {
385
+ yaml += `settings:
386
+ `;
387
+ for (const [key, value] of Object.entries(policy.settings)) {
388
+ yaml += ` ${key}: ${value}
389
+ `;
390
+ }
391
+ }
392
+ return yaml;
393
+ }
394
+ export {
395
+ PRESETS,
396
+ PRESET_DEVELOPER,
397
+ PRESET_PRODUCTION,
398
+ PRESET_READONLY,
399
+ WhitelistEngine,
400
+ autoLoadPolicy,
401
+ generatePolicyFile,
402
+ loadPolicy
403
+ };
package/package.json CHANGED
@@ -1,52 +1,57 @@
1
1
  {
2
2
  "name": "sovr-mcp-proxy",
3
- "version": "6.0.1",
4
- "description": "Responsibility Layer for AI agents. Transparent MCP Proxy that intercepts all agent tool calls with policy engine verification and audit trail before forwarding. Supports stdio, SSE, and Streamable HTTP transports.",
5
- "keywords": [
6
- "mcp",
7
- "model-context-protocol",
8
- "ai-responsibility",
9
- "ai-governance",
10
- "audit-trail",
11
- "proxy",
12
- "gate-check",
13
- "audit",
14
- "trust",
15
- "sovr",
16
- "mcp-proxy",
17
- "sse",
18
- "streamable-http",
19
- "remote-mcp",
20
- "responsibility-layer",
21
- "decision-accountability"
22
- ],
23
- "homepage": "https://sovr.inc",
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/xie38388/sovr.git"
27
- },
28
- "bugs": {
29
- "url": "https://github.com/xie38388/sovr/issues"
30
- },
31
- "license": "BSL-1.1",
32
- "author": "SOVR AI <contact@sovrapp.com>",
33
- "type": "module",
3
+ "version": "7.0.0",
4
+ "description": "SOVR MCP Proxy intercepts MCP tool calls and evaluates them against the unified Policy Engine",
34
5
  "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
35
8
  "bin": {
36
9
  "sovr-mcp-proxy": "dist/cli.js"
37
10
  },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ },
17
+ "./engine": {
18
+ "types": "./dist/engine-export.d.ts",
19
+ "import": "./dist/engine-export.mjs",
20
+ "require": "./dist/engine-export.js"
21
+ }
22
+ },
38
23
  "files": [
39
24
  "dist",
40
- "README.md",
41
- "LICENSE",
42
- "server.json"
25
+ "README.md"
43
26
  ],
44
- "engines": {
45
- "node": ">=18.0.0"
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",
29
+ "test": "vitest run --config vitest.config.ts",
30
+ "lint": "tsc --noEmit",
31
+ "prepublishOnly": "pnpm build"
46
32
  },
47
- "publishConfig": {
48
- "access": "public"
33
+ "keywords": [
34
+ "sovr",
35
+ "mcp",
36
+ "proxy",
37
+ "ai-firewall",
38
+ "ai-safety",
39
+ "tool-call",
40
+ "model-context-protocol",
41
+ "whitelist",
42
+ "claude-code-hooks",
43
+ "command-normalizer"
44
+ ],
45
+ "author": "SOVR Inc. <sdk@sovr.inc>",
46
+ "license": "BSL-1.1",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/xie38388/sovr"
49
50
  },
50
- "mcpName": "io.github.xie38388/sovr-proxy",
51
- "scripts": {}
52
- }
51
+ "devDependencies": {
52
+ "@types/node": "^20.19.33",
53
+ "tsup": "^8.5.1",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^1.0.0"
56
+ }
57
+ }
package/server.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "name": "sovr-mcp-proxy",
3
- "version": "2.5.5",
4
- "description": "SOVR AI Responsibility Layer \u2014 Unbypassable MCP Proxy with dual-layer security",
5
- "transport": [
6
- "stdio",
7
- "sse"
8
- ],
9
- "capabilities": {
10
- "tools": true,
11
- "resources": true,
12
- "prompts": true
13
- }
14
- }