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.
package/dist/cli.js CHANGED
@@ -9775,6 +9775,973 @@ Failed: ${this.stats.failedChecks}`
9775
9775
  }
9776
9776
  });
9777
9777
 
9778
+ // src/toolReplacement.ts
9779
+ var toolReplacement_exports = {};
9780
+ __export(toolReplacement_exports, {
9781
+ DEFAULT_TARGET_TOOLS: () => DEFAULT_TARGET_TOOLS,
9782
+ DEFAULT_TOOL_REPLACEMENT_CONFIG: () => DEFAULT_TOOL_REPLACEMENT_CONFIG,
9783
+ describeMode: () => describeMode,
9784
+ parseMode: () => parseMode,
9785
+ shouldIntercept: () => shouldIntercept,
9786
+ transformToolList: () => transformToolList
9787
+ });
9788
+ function transformToolList(upstreamTools, config) {
9789
+ const removedTools = [];
9790
+ const addedTools = [];
9791
+ const wrappedTools = [];
9792
+ let resultTools = [];
9793
+ switch (config.mode) {
9794
+ case "exclusive": {
9795
+ for (const tool of upstreamTools) {
9796
+ if (isTargetTool(tool.name, config.targetTools)) {
9797
+ removedTools.push(tool.name);
9798
+ } else {
9799
+ resultTools.push(tool);
9800
+ }
9801
+ }
9802
+ resultTools.push(SOVR_EXEC_TOOL);
9803
+ addedTools.push(SOVR_EXEC_TOOL.name);
9804
+ break;
9805
+ }
9806
+ case "enforce": {
9807
+ for (const tool of upstreamTools) {
9808
+ if (isTargetTool(tool.name, config.targetTools)) {
9809
+ resultTools.push({
9810
+ ...tool,
9811
+ description: `[SOVR-ENFORCED] ${tool.description || ""} \u2014 All calls are policy-checked by SOVR before execution.`
9812
+ });
9813
+ wrappedTools.push(tool.name);
9814
+ } else {
9815
+ resultTools.push(tool);
9816
+ }
9817
+ }
9818
+ resultTools.push(SOVR_EXEC_TOOL);
9819
+ addedTools.push(SOVR_EXEC_TOOL.name);
9820
+ break;
9821
+ }
9822
+ case "advisory": {
9823
+ resultTools = [...upstreamTools];
9824
+ resultTools.push(SOVR_EXEC_TOOL);
9825
+ addedTools.push(SOVR_EXEC_TOOL.name);
9826
+ break;
9827
+ }
9828
+ case "monitor":
9829
+ default: {
9830
+ resultTools = [...upstreamTools];
9831
+ break;
9832
+ }
9833
+ }
9834
+ if (config.addStatusTool) {
9835
+ resultTools.push(SOVR_STATUS_TOOL);
9836
+ addedTools.push(SOVR_STATUS_TOOL.name);
9837
+ }
9838
+ if (config.addAuditTool) {
9839
+ resultTools.push(SOVR_AUDIT_TOOL);
9840
+ addedTools.push(SOVR_AUDIT_TOOL.name);
9841
+ }
9842
+ return { tools: resultTools, removedTools, addedTools, wrappedTools };
9843
+ }
9844
+ function isTargetTool(toolName, targets) {
9845
+ const lowerName = toolName.toLowerCase();
9846
+ return targets.some((target) => {
9847
+ const lowerTarget = target.toLowerCase();
9848
+ if (lowerName === lowerTarget) return true;
9849
+ if (lowerTarget.includes("*")) {
9850
+ const regex = new RegExp(
9851
+ "^" + lowerTarget.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
9852
+ );
9853
+ return regex.test(lowerName);
9854
+ }
9855
+ return false;
9856
+ });
9857
+ }
9858
+ function shouldIntercept(toolName, config) {
9859
+ switch (config.mode) {
9860
+ case "exclusive":
9861
+ if (toolName === "sovr_exec") {
9862
+ return { intercept: true, reason: "exclusive-mode: all exec calls routed through SOVR" };
9863
+ }
9864
+ if (isTargetTool(toolName, config.targetTools)) {
9865
+ return { intercept: true, reason: "exclusive-mode: native tool blocked, use sovr_exec" };
9866
+ }
9867
+ return { intercept: false, reason: "non-exec tool, passthrough" };
9868
+ case "enforce":
9869
+ if (toolName === "sovr_exec" || isTargetTool(toolName, config.targetTools)) {
9870
+ return { intercept: true, reason: "enforce-mode: policy check required" };
9871
+ }
9872
+ return { intercept: false, reason: "non-exec tool, passthrough" };
9873
+ case "advisory":
9874
+ if (toolName === "sovr_exec") {
9875
+ return { intercept: true, reason: "advisory-mode: SOVR exec call" };
9876
+ }
9877
+ if (isTargetTool(toolName, config.targetTools)) {
9878
+ return { intercept: false, reason: "advisory-mode: native tool allowed with warning" };
9879
+ }
9880
+ return { intercept: false, reason: "non-exec tool, passthrough" };
9881
+ case "monitor":
9882
+ default:
9883
+ return { intercept: false, reason: "monitor-mode: all tools passthrough" };
9884
+ }
9885
+ }
9886
+ function describeMode(config) {
9887
+ switch (config.mode) {
9888
+ case "exclusive":
9889
+ return `EXCLUSIVE MODE: Native execution tools (${config.targetTools.join(", ")}) are REMOVED. The LLM can ONLY execute commands through sovr_exec. Bypass is impossible.`;
9890
+ case "enforce":
9891
+ return `ENFORCE MODE: Native execution tools are wrapped with SOVR policy checks. All command executions are evaluated before running. Dangerous commands are blocked.`;
9892
+ case "advisory":
9893
+ return `ADVISORY MODE: Native tools remain available. SOVR tools are added alongside. Warnings are logged for risky commands but execution is not blocked.`;
9894
+ case "monitor":
9895
+ return `MONITOR MODE: All tools pass through unchanged. SOVR only logs activity. No enforcement or blocking.`;
9896
+ }
9897
+ }
9898
+ function parseMode(input) {
9899
+ const normalized = input.toLowerCase().trim();
9900
+ if (["exclusive", "enforce", "advisory", "monitor"].includes(normalized)) {
9901
+ return normalized;
9902
+ }
9903
+ throw new Error(
9904
+ `Invalid mode: "${input}". Valid modes: exclusive, enforce, advisory, monitor`
9905
+ );
9906
+ }
9907
+ var DEFAULT_TARGET_TOOLS, SOVR_EXEC_TOOL, SOVR_STATUS_TOOL, SOVR_AUDIT_TOOL, DEFAULT_TOOL_REPLACEMENT_CONFIG;
9908
+ var init_toolReplacement = __esm({
9909
+ "src/toolReplacement.ts"() {
9910
+ "use strict";
9911
+ DEFAULT_TARGET_TOOLS = [
9912
+ // Claude Code native tools
9913
+ "Bash",
9914
+ "bash",
9915
+ "shell",
9916
+ "execute_command",
9917
+ "run_command",
9918
+ "terminal",
9919
+ // Cursor / Windsurf / Continue
9920
+ "run_terminal_command",
9921
+ "execute_shell",
9922
+ "shell_exec",
9923
+ // Generic patterns
9924
+ "exec",
9925
+ "system",
9926
+ "subprocess"
9927
+ ];
9928
+ SOVR_EXEC_TOOL = {
9929
+ name: "sovr_exec",
9930
+ description: "Execute a shell command through the SOVR Responsibility Layer. All commands are evaluated against security policies before execution. Dangerous commands (rm -rf, DROP TABLE, etc.) will be blocked. This is the ONLY way to execute commands \u2014 use this for ALL shell operations.",
9931
+ inputSchema: {
9932
+ type: "object",
9933
+ properties: {
9934
+ command: {
9935
+ type: "string",
9936
+ description: "The shell command to execute. Will be policy-checked before running."
9937
+ },
9938
+ workdir: {
9939
+ type: "string",
9940
+ description: "Working directory for the command (optional)."
9941
+ },
9942
+ timeout: {
9943
+ type: "number",
9944
+ description: "Timeout in milliseconds (default: 300000 = 5 min)."
9945
+ }
9946
+ },
9947
+ required: ["command"]
9948
+ }
9949
+ };
9950
+ SOVR_STATUS_TOOL = {
9951
+ name: "sovr_status",
9952
+ description: "Check the current SOVR security status, including active policies, recent decisions, and system health. Use this to understand what commands are allowed or blocked.",
9953
+ inputSchema: {
9954
+ type: "object",
9955
+ properties: {
9956
+ verbose: {
9957
+ type: "boolean",
9958
+ description: "Include detailed policy information (default: false)."
9959
+ }
9960
+ }
9961
+ }
9962
+ };
9963
+ SOVR_AUDIT_TOOL = {
9964
+ name: "sovr_audit",
9965
+ description: "View the SOVR audit log of recent command evaluations. Shows what was allowed, blocked, or escalated.",
9966
+ inputSchema: {
9967
+ type: "object",
9968
+ properties: {
9969
+ limit: {
9970
+ type: "number",
9971
+ description: "Number of recent entries to show (default: 10)."
9972
+ },
9973
+ filter: {
9974
+ type: "string",
9975
+ enum: ["all", "allowed", "blocked", "escalated"],
9976
+ description: "Filter by decision type (default: all)."
9977
+ }
9978
+ }
9979
+ }
9980
+ };
9981
+ DEFAULT_TOOL_REPLACEMENT_CONFIG = {
9982
+ mode: "enforce",
9983
+ targetTools: DEFAULT_TARGET_TOOLS,
9984
+ addStatusTool: true,
9985
+ addAuditTool: true
9986
+ };
9987
+ }
9988
+ });
9989
+
9990
+ // src/commandNormalizer.ts
9991
+ var commandNormalizer_exports = {};
9992
+ __export(commandNormalizer_exports, {
9993
+ getBaseCommand: () => getBaseCommand,
9994
+ hasDestructiveEquivalent: () => hasDestructiveEquivalent,
9995
+ hasEncodingTricks: () => hasEncodingTricks,
9996
+ normalize: () => normalize,
9997
+ summarize: () => summarize
9998
+ });
9999
+ function normalize(command) {
10000
+ const original = command;
10001
+ const segments = [];
10002
+ const suspicion_reasons = [];
10003
+ const trimmed = command.trim();
10004
+ if (!trimmed) {
10005
+ return { segments: [], suspicious: false, suspicion_reasons: [], original };
10006
+ }
10007
+ for (const enc of ENCODING_PATTERNS) {
10008
+ if (enc.pattern.test(trimmed)) {
10009
+ suspicion_reasons.push(`${enc.name}: ${enc.risk}`);
10010
+ }
10011
+ }
10012
+ for (const deq of DESTRUCTIVE_EQUIVALENTS) {
10013
+ if (deq.pattern.test(trimmed)) {
10014
+ suspicion_reasons.push(`Destructive equivalent detected: ${deq.description}`);
10015
+ }
10016
+ }
10017
+ const chainSegments = splitChains(trimmed);
10018
+ for (const chainSeg of chainSegments) {
10019
+ const pipeSegments = splitPipes(chainSeg);
10020
+ for (const pipeSeg of pipeSegments) {
10021
+ const unwrapped = unwrapCommand(pipeSeg.trim());
10022
+ if (unwrapped.unwrapped) {
10023
+ segments.push({
10024
+ raw: pipeSeg.trim(),
10025
+ effective: unwrapped.effective,
10026
+ type: "unwrapped",
10027
+ warnings: unwrapped.warnings
10028
+ });
10029
+ const inner = normalize(unwrapped.effective);
10030
+ for (const innerSeg of inner.segments) {
10031
+ if (innerSeg.effective !== unwrapped.effective) {
10032
+ segments.push(innerSeg);
10033
+ }
10034
+ }
10035
+ suspicion_reasons.push(...inner.suspicion_reasons);
10036
+ } else {
10037
+ const type = pipeSegments.length > 1 ? "pipe-segment" : chainSegments.length > 1 ? "chain-segment" : "direct";
10038
+ segments.push({
10039
+ raw: pipeSeg.trim(),
10040
+ effective: pipeSeg.trim(),
10041
+ type,
10042
+ warnings: []
10043
+ });
10044
+ }
10045
+ }
10046
+ }
10047
+ const subshells = extractSubshells(trimmed);
10048
+ for (const sub of subshells) {
10049
+ segments.push({
10050
+ raw: trimmed,
10051
+ effective: sub,
10052
+ type: "subshell",
10053
+ warnings: ["Subshell command extracted for evaluation"]
10054
+ });
10055
+ }
10056
+ return {
10057
+ segments,
10058
+ suspicious: suspicion_reasons.length > 0,
10059
+ suspicion_reasons,
10060
+ original
10061
+ };
10062
+ }
10063
+ function splitChains(command) {
10064
+ return splitRespectingQuotes(command, /\s*(?:&&|\|\||;)\s*/);
10065
+ }
10066
+ function splitPipes(command) {
10067
+ return splitRespectingQuotes(command, /\s*\|(?!\|)\s*/);
10068
+ }
10069
+ function splitRespectingQuotes(input, delimiter) {
10070
+ const segments = [];
10071
+ let current = "";
10072
+ let inSingleQuote = false;
10073
+ let inDoubleQuote = false;
10074
+ let escaped = false;
10075
+ let i = 0;
10076
+ while (i < input.length) {
10077
+ const char = input[i];
10078
+ if (escaped) {
10079
+ current += char;
10080
+ escaped = false;
10081
+ i++;
10082
+ continue;
10083
+ }
10084
+ if (char === "\\") {
10085
+ escaped = true;
10086
+ current += char;
10087
+ i++;
10088
+ continue;
10089
+ }
10090
+ if (char === "'" && !inDoubleQuote) {
10091
+ inSingleQuote = !inSingleQuote;
10092
+ current += char;
10093
+ i++;
10094
+ continue;
10095
+ }
10096
+ if (char === '"' && !inSingleQuote) {
10097
+ inDoubleQuote = !inDoubleQuote;
10098
+ current += char;
10099
+ i++;
10100
+ continue;
10101
+ }
10102
+ if (!inSingleQuote && !inDoubleQuote) {
10103
+ const remaining = input.slice(i);
10104
+ const match = remaining.match(delimiter);
10105
+ if (match && match.index === 0) {
10106
+ if (current.trim()) {
10107
+ segments.push(current.trim());
10108
+ }
10109
+ current = "";
10110
+ i += match[0].length;
10111
+ continue;
10112
+ }
10113
+ }
10114
+ current += char;
10115
+ i++;
10116
+ }
10117
+ if (current.trim()) {
10118
+ segments.push(current.trim());
10119
+ }
10120
+ return segments.length > 0 ? segments : [input];
10121
+ }
10122
+ function unwrapCommand(command) {
10123
+ for (const wrapper of SHELL_WRAPPERS) {
10124
+ const match = command.match(wrapper.pattern);
10125
+ if (match) {
10126
+ const inner = wrapper.extract(match);
10127
+ if (inner) {
10128
+ return {
10129
+ unwrapped: true,
10130
+ effective: inner.trim(),
10131
+ wrapper: wrapper.name,
10132
+ warnings: [`Unwrapped from ${wrapper.name}: "${command}" \u2192 "${inner.trim()}"`]
10133
+ };
10134
+ }
10135
+ }
10136
+ }
10137
+ return { unwrapped: false, effective: command, warnings: [] };
10138
+ }
10139
+ function extractSubshells(command) {
10140
+ const subshells = [];
10141
+ const dollarParen = /\$\(([^)]+)\)/g;
10142
+ let match;
10143
+ while ((match = dollarParen.exec(command)) !== null) {
10144
+ if (match[1]) subshells.push(match[1].trim());
10145
+ }
10146
+ const backtick = /`([^`]+)`/g;
10147
+ while ((match = backtick.exec(command)) !== null) {
10148
+ if (match[1]) subshells.push(match[1].trim());
10149
+ }
10150
+ return subshells;
10151
+ }
10152
+ function getBaseCommand(command) {
10153
+ const trimmed = command.trim();
10154
+ const firstSpace = trimmed.indexOf(" ");
10155
+ return firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
10156
+ }
10157
+ function hasDestructiveEquivalent(command) {
10158
+ const matches = [];
10159
+ for (const deq of DESTRUCTIVE_EQUIVALENTS) {
10160
+ if (deq.pattern.test(command)) {
10161
+ matches.push({ equivalent: deq.equivalent, description: deq.description });
10162
+ }
10163
+ }
10164
+ return { found: matches.length > 0, matches };
10165
+ }
10166
+ function hasEncodingTricks(command) {
10167
+ const tricks = [];
10168
+ for (const enc of ENCODING_PATTERNS) {
10169
+ if (enc.pattern.test(command)) {
10170
+ tricks.push({ name: enc.name, risk: enc.risk });
10171
+ }
10172
+ }
10173
+ return { found: tricks.length > 0, tricks };
10174
+ }
10175
+ function summarize(result) {
10176
+ const lines = [];
10177
+ lines.push(`Original: ${result.original}`);
10178
+ lines.push(`Segments: ${result.segments.length}`);
10179
+ lines.push(`Suspicious: ${result.suspicious}`);
10180
+ if (result.suspicion_reasons.length > 0) {
10181
+ lines.push(`Suspicion reasons:`);
10182
+ for (const reason of result.suspicion_reasons) {
10183
+ lines.push(` - ${reason}`);
10184
+ }
10185
+ }
10186
+ lines.push(`Segments:`);
10187
+ for (const seg of result.segments) {
10188
+ lines.push(` [${seg.type}] ${seg.effective}`);
10189
+ for (const w of seg.warnings) {
10190
+ lines.push(` \u26A0 ${w}`);
10191
+ }
10192
+ }
10193
+ return lines.join("\n");
10194
+ }
10195
+ var SHELL_WRAPPERS, ENCODING_PATTERNS, DESTRUCTIVE_EQUIVALENTS;
10196
+ var init_commandNormalizer = __esm({
10197
+ "src/commandNormalizer.ts"() {
10198
+ "use strict";
10199
+ SHELL_WRAPPERS = [
10200
+ // bash -c "cmd" / sh -c "cmd"
10201
+ {
10202
+ pattern: /^(?:bash|sh|zsh|dash|ksh)\s+-c\s+(?:"([^"]+)"|'([^']+)'|(\S+))/,
10203
+ extract: (m) => m[1] || m[2] || m[3] || "",
10204
+ name: "shell -c"
10205
+ },
10206
+ // env cmd args...
10207
+ {
10208
+ pattern: /^env\s+(?:-\S+\s+)*(?:\S+=\S+\s+)*(.+)/,
10209
+ extract: (m) => m[1] || "",
10210
+ name: "env"
10211
+ },
10212
+ // xargs cmd
10213
+ {
10214
+ pattern: /^xargs\s+(?:-\S+\s+)*(.+)/,
10215
+ extract: (m) => m[1] || "",
10216
+ name: "xargs"
10217
+ },
10218
+ // sudo cmd
10219
+ {
10220
+ pattern: /^sudo\s+(?:-\S+\s+)*(.+)/,
10221
+ extract: (m) => m[1] || "",
10222
+ name: "sudo"
10223
+ },
10224
+ // nohup cmd
10225
+ {
10226
+ pattern: /^nohup\s+(.+?)(?:\s*&\s*)?$/,
10227
+ extract: (m) => m[1] || "",
10228
+ name: "nohup"
10229
+ },
10230
+ // timeout N cmd
10231
+ {
10232
+ pattern: /^timeout\s+\d+[smhd]?\s+(.+)/,
10233
+ extract: (m) => m[1] || "",
10234
+ name: "timeout"
10235
+ },
10236
+ // nice -n N cmd
10237
+ {
10238
+ pattern: /^nice\s+(?:-n\s+\d+\s+)?(.+)/,
10239
+ extract: (m) => m[1] || "",
10240
+ name: "nice"
10241
+ },
10242
+ // strace / ltrace cmd
10243
+ {
10244
+ pattern: /^(?:strace|ltrace)\s+(?:-\S+\s+)*(.+)/,
10245
+ extract: (m) => m[1] || "",
10246
+ name: "trace"
10247
+ },
10248
+ // watch -n N cmd
10249
+ {
10250
+ pattern: /^watch\s+(?:-n\s+\d+\s+)?(.+)/,
10251
+ extract: (m) => m[1] || "",
10252
+ name: "watch"
10253
+ }
10254
+ ];
10255
+ ENCODING_PATTERNS = [
10256
+ // echo "base64" | base64 -d | bash
10257
+ {
10258
+ pattern: /base64\s+(?:-d|--decode)/,
10259
+ name: "base64-decode",
10260
+ risk: "Command may be hidden via base64 encoding"
10261
+ },
10262
+ // printf '\x72\x6d' (hex encoding)
10263
+ {
10264
+ pattern: /printf\s+.*\\x[0-9a-fA-F]{2}/,
10265
+ name: "hex-printf",
10266
+ risk: "Command may be hidden via hex encoding in printf"
10267
+ },
10268
+ // $'\x72\x6d' (ANSI-C quoting)
10269
+ {
10270
+ pattern: /\$'[^']*\\x[0-9a-fA-F]{2}/,
10271
+ name: "ansi-c-quoting",
10272
+ risk: "Command may be hidden via ANSI-C quoting"
10273
+ },
10274
+ // python -c "import os; os.system('rm -rf /')"
10275
+ {
10276
+ pattern: /python[23]?\s+-c\s+/,
10277
+ name: "python-exec",
10278
+ risk: "Arbitrary code execution via Python"
10279
+ },
10280
+ // perl -e "system('rm -rf /')"
10281
+ {
10282
+ pattern: /perl\s+-e\s+/,
10283
+ name: "perl-exec",
10284
+ risk: "Arbitrary code execution via Perl"
10285
+ },
10286
+ // ruby -e "system('rm -rf /')"
10287
+ {
10288
+ pattern: /ruby\s+-e\s+/,
10289
+ name: "ruby-exec",
10290
+ risk: "Arbitrary code execution via Ruby"
10291
+ },
10292
+ // eval "cmd"
10293
+ {
10294
+ pattern: /\beval\s+/,
10295
+ name: "eval",
10296
+ risk: "Dynamic command evaluation"
10297
+ },
10298
+ // curl ... | bash
10299
+ {
10300
+ pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/,
10301
+ name: "curl-pipe-shell",
10302
+ risk: "Remote code execution via curl | bash"
10303
+ },
10304
+ // wget ... -O - | bash
10305
+ {
10306
+ pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/,
10307
+ name: "wget-pipe-shell",
10308
+ risk: "Remote code execution via wget | bash"
10309
+ }
10310
+ ];
10311
+ DESTRUCTIVE_EQUIVALENTS = [
10312
+ // find / -delete (equivalent to rm -rf)
10313
+ { pattern: /find\s+.*-delete/, equivalent: "rm -rf", description: "find -delete is equivalent to rm -rf" },
10314
+ // find / -exec rm {} (equivalent to rm -rf)
10315
+ { pattern: /find\s+.*-exec\s+rm/, equivalent: "rm -rf", description: "find -exec rm is equivalent to rm -rf" },
10316
+ // TRUNCATE TABLE (equivalent to DELETE FROM)
10317
+ { pattern: /TRUNCATE\s+TABLE/i, equivalent: "DELETE FROM", description: "TRUNCATE TABLE is equivalent to DELETE FROM" },
10318
+ // dd if=/dev/zero of=/ (disk wipe)
10319
+ { pattern: /dd\s+.*of=\//, equivalent: "disk-wipe", description: "dd writing to root is destructive" },
10320
+ // mkfs (format disk)
10321
+ { pattern: /mkfs/, equivalent: "disk-format", description: "mkfs formats a disk" },
10322
+ // shred (secure delete)
10323
+ { pattern: /shred\s+/, equivalent: "secure-delete", description: "shred securely deletes files" },
10324
+ // chmod 777 / (open permissions)
10325
+ { pattern: /chmod\s+777\s+\//, equivalent: "open-permissions", description: "chmod 777 / opens all permissions" },
10326
+ // chown root (change ownership)
10327
+ { pattern: /chown\s+.*\//, equivalent: "change-ownership", description: "chown on root paths is dangerous" },
10328
+ // iptables -F (flush firewall)
10329
+ { pattern: /iptables\s+-F/, equivalent: "flush-firewall", description: "iptables -F flushes all firewall rules" }
10330
+ ];
10331
+ }
10332
+ });
10333
+
10334
+ // src/whitelistEngine.ts
10335
+ var whitelistEngine_exports = {};
10336
+ __export(whitelistEngine_exports, {
10337
+ PRESETS: () => PRESETS,
10338
+ PRESET_DEVELOPER: () => PRESET_DEVELOPER,
10339
+ PRESET_PRODUCTION: () => PRESET_PRODUCTION,
10340
+ PRESET_READONLY: () => PRESET_READONLY,
10341
+ WhitelistEngine: () => WhitelistEngine,
10342
+ autoLoadPolicy: () => autoLoadPolicy,
10343
+ generatePolicyFile: () => generatePolicyFile,
10344
+ loadPolicy: () => loadPolicy
10345
+ });
10346
+ function matchPattern(command, pattern) {
10347
+ const normalizedCmd = command.trim().replace(/\s+/g, " ");
10348
+ const normalizedPattern = pattern.trim().replace(/\s+/g, " ");
10349
+ const regexStr = "^" + normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
10350
+ try {
10351
+ return new RegExp(regexStr).test(normalizedCmd);
10352
+ } catch {
10353
+ return false;
10354
+ }
10355
+ }
10356
+ function loadPolicy(filePath) {
10357
+ if (!(0, import_node_fs5.existsSync)(filePath)) {
10358
+ throw new Error(`Policy file not found: ${filePath}`);
10359
+ }
10360
+ const raw = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
10361
+ try {
10362
+ return JSON.parse(raw);
10363
+ } catch {
10364
+ }
10365
+ return parseSimpleYaml(raw);
10366
+ }
10367
+ function autoLoadPolicy(projectDir) {
10368
+ const candidates = [
10369
+ (0, import_node_path5.join)(projectDir, ".sovr", "policy.json"),
10370
+ (0, import_node_path5.join)(projectDir, ".sovr", "policy.yaml"),
10371
+ (0, import_node_path5.join)(projectDir, ".sovr", "policy.yml"),
10372
+ (0, import_node_path5.join)(projectDir, "sovr.policy.json")
10373
+ ];
10374
+ for (const candidate of candidates) {
10375
+ if ((0, import_node_fs5.existsSync)(candidate)) {
10376
+ return loadPolicy(candidate);
10377
+ }
10378
+ }
10379
+ return null;
10380
+ }
10381
+ function parseSimpleYaml(raw) {
10382
+ const lines = raw.split("\n");
10383
+ const policy = {
10384
+ version: "1.0",
10385
+ mode: "whitelist",
10386
+ rules: []
10387
+ };
10388
+ let currentRule = null;
10389
+ let inRules = false;
10390
+ for (const line of lines) {
10391
+ const trimmed = line.trim();
10392
+ if (!trimmed || trimmed.startsWith("#")) continue;
10393
+ if (trimmed.startsWith("version:")) {
10394
+ policy.version = trimmed.split(":").slice(1).join(":").trim().replace(/['"]/g, "");
10395
+ } else if (trimmed.startsWith("mode:")) {
10396
+ policy.mode = trimmed.split(":").slice(1).join(":").trim().replace(/['"]/g, "");
10397
+ } else if (trimmed === "rules:") {
10398
+ inRules = true;
10399
+ } else if (inRules && trimmed.startsWith("- pattern:")) {
10400
+ if (currentRule?.pattern) {
10401
+ policy.rules.push(currentRule);
10402
+ }
10403
+ currentRule = {
10404
+ pattern: trimmed.replace("- pattern:", "").trim().replace(/['"]/g, ""),
10405
+ allow: true,
10406
+ enabled: true
10407
+ };
10408
+ } else if (inRules && currentRule) {
10409
+ if (trimmed.startsWith("allow:")) {
10410
+ currentRule.allow = trimmed.includes("true");
10411
+ } else if (trimmed.startsWith("description:")) {
10412
+ currentRule.description = trimmed.split(":").slice(1).join(":").trim().replace(/['"]/g, "");
10413
+ } else if (trimmed.startsWith("require_approval:")) {
10414
+ currentRule.require_approval = trimmed.includes("true");
10415
+ } else if (trimmed.startsWith("max_args:")) {
10416
+ currentRule.max_args = parseInt(trimmed.split(":")[1].trim());
10417
+ } else if (trimmed.startsWith("priority:")) {
10418
+ currentRule.priority = parseInt(trimmed.split(":")[1].trim());
10419
+ }
10420
+ }
10421
+ }
10422
+ if (currentRule?.pattern) {
10423
+ policy.rules.push(currentRule);
10424
+ }
10425
+ return policy;
10426
+ }
10427
+ function generatePolicyFile(preset, format = "yaml") {
10428
+ const policy = PRESETS[preset];
10429
+ if (!policy) {
10430
+ throw new Error(`Unknown preset: ${preset}. Available: ${Object.keys(PRESETS).join(", ")}`);
10431
+ }
10432
+ if (format === "json") {
10433
+ return JSON.stringify(policy, null, 2);
10434
+ }
10435
+ let yaml = `# SOVR Whitelist Policy
10436
+ `;
10437
+ yaml += `# Preset: ${preset}
10438
+ `;
10439
+ yaml += `# Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
10440
+
10441
+ `;
10442
+ yaml += `version: "${policy.version}"
10443
+ `;
10444
+ yaml += `mode: ${policy.mode}
10445
+
10446
+ `;
10447
+ yaml += `rules:
10448
+ `;
10449
+ for (const rule of policy.rules) {
10450
+ yaml += ` - pattern: "${rule.pattern}"
10451
+ `;
10452
+ yaml += ` allow: ${rule.allow}
10453
+ `;
10454
+ if (rule.description) yaml += ` description: "${rule.description}"
10455
+ `;
10456
+ if (rule.require_approval) yaml += ` require_approval: true
10457
+ `;
10458
+ if (rule.max_args !== void 0) yaml += ` max_args: ${rule.max_args}
10459
+ `;
10460
+ yaml += `
10461
+ `;
10462
+ }
10463
+ if (policy.settings) {
10464
+ yaml += `settings:
10465
+ `;
10466
+ for (const [key, value] of Object.entries(policy.settings)) {
10467
+ yaml += ` ${key}: ${value}
10468
+ `;
10469
+ }
10470
+ }
10471
+ return yaml;
10472
+ }
10473
+ var import_node_fs5, import_node_path5, PRESET_READONLY, PRESET_DEVELOPER, PRESET_PRODUCTION, PRESETS, WhitelistEngine;
10474
+ var init_whitelistEngine = __esm({
10475
+ "src/whitelistEngine.ts"() {
10476
+ "use strict";
10477
+ import_node_fs5 = require("fs");
10478
+ import_node_path5 = require("path");
10479
+ PRESET_READONLY = {
10480
+ version: "1.0",
10481
+ mode: "whitelist",
10482
+ rules: [
10483
+ { pattern: "ls *", allow: true, description: "List directory" },
10484
+ { pattern: "cat *", allow: true, description: "Read file" },
10485
+ { pattern: "head *", allow: true, description: "Read file head" },
10486
+ { pattern: "tail *", allow: true, description: "Read file tail" },
10487
+ { pattern: "find *", allow: true, description: "Find files", max_args: 10 },
10488
+ { pattern: "grep *", allow: true, description: "Search in files" },
10489
+ { pattern: "wc *", allow: true, description: "Word count" },
10490
+ { pattern: "file *", allow: true, description: "File type" },
10491
+ { pattern: "pwd", allow: true, description: "Print working directory" },
10492
+ { pattern: "whoami", allow: true, description: "Current user" },
10493
+ { pattern: "date", allow: true, description: "Current date" },
10494
+ { pattern: "echo *", allow: true, description: "Echo text" }
10495
+ ],
10496
+ settings: {
10497
+ max_command_length: 500,
10498
+ allow_env_expansion: false,
10499
+ allow_subshell: false,
10500
+ allow_pipes: true,
10501
+ allow_chains: false,
10502
+ allow_redirects: false
10503
+ }
10504
+ };
10505
+ PRESET_DEVELOPER = {
10506
+ version: "1.0",
10507
+ mode: "whitelist",
10508
+ rules: [
10509
+ // Read operations
10510
+ ...PRESET_READONLY.rules,
10511
+ // Git (safe operations)
10512
+ { pattern: "git status", allow: true, description: "Git status" },
10513
+ { pattern: "git diff *", allow: true, description: "Git diff" },
10514
+ { pattern: "git log *", allow: true, description: "Git log" },
10515
+ { pattern: "git branch *", allow: true, description: "Git branch" },
10516
+ { pattern: "git add *", allow: true, description: "Git add" },
10517
+ { pattern: "git commit *", allow: true, description: "Git commit" },
10518
+ { pattern: "git push *", allow: true, require_approval: true, description: "Git push (requires approval)" },
10519
+ { pattern: "git pull *", allow: true, description: "Git pull" },
10520
+ { pattern: "git checkout *", allow: true, description: "Git checkout" },
10521
+ { pattern: "git stash *", allow: true, description: "Git stash" },
10522
+ // Node.js / npm
10523
+ { pattern: "node *", allow: true, description: "Run Node.js" },
10524
+ { pattern: "npm run *", allow: true, description: "Run npm script" },
10525
+ { pattern: "npm test", allow: true, description: "Run tests" },
10526
+ { pattern: "npm install *", allow: true, description: "Install packages" },
10527
+ { pattern: "npx *", allow: true, description: "Run npx" },
10528
+ { pattern: "pnpm *", allow: true, description: "Run pnpm" },
10529
+ { pattern: "yarn *", allow: true, description: "Run yarn" },
10530
+ // Python
10531
+ { pattern: "python3 *", allow: true, description: "Run Python", max_args: 20 },
10532
+ { pattern: "pip install *", allow: true, description: "Install Python packages" },
10533
+ { pattern: "pip3 install *", allow: true, description: "Install Python packages" },
10534
+ // Build tools
10535
+ { pattern: "make *", allow: true, description: "Run make" },
10536
+ { pattern: "cargo *", allow: true, description: "Run cargo" },
10537
+ // File operations (limited)
10538
+ { pattern: "mkdir *", allow: true, description: "Create directory" },
10539
+ { pattern: "cp *", allow: true, description: "Copy files" },
10540
+ { pattern: "mv *", allow: true, description: "Move files" },
10541
+ { pattern: "touch *", allow: true, description: "Create empty file" },
10542
+ // Dangerous operations — require approval
10543
+ { pattern: "rm *", allow: true, require_approval: true, description: "Delete files (requires approval)" },
10544
+ { pattern: "npm publish *", allow: true, require_approval: true, description: "Publish package (requires approval)" },
10545
+ { pattern: "docker *", allow: true, require_approval: true, description: "Docker operations (requires approval)" }
10546
+ ],
10547
+ settings: {
10548
+ max_command_length: 2e3,
10549
+ allow_env_expansion: true,
10550
+ allow_subshell: false,
10551
+ allow_pipes: true,
10552
+ allow_chains: true,
10553
+ allow_redirects: true
10554
+ }
10555
+ };
10556
+ PRESET_PRODUCTION = {
10557
+ version: "1.0",
10558
+ mode: "whitelist",
10559
+ rules: [
10560
+ { pattern: "echo *", allow: true, description: "Echo" },
10561
+ { pattern: "date", allow: true, description: "Date" },
10562
+ { pattern: "uptime", allow: true, description: "Uptime" },
10563
+ { pattern: "df *", allow: true, description: "Disk usage" },
10564
+ { pattern: "free *", allow: true, description: "Memory usage" },
10565
+ { pattern: "ps *", allow: true, description: "Process list" },
10566
+ { pattern: "curl *", allow: true, require_approval: true, description: "HTTP request (requires approval)" }
10567
+ ],
10568
+ default_action: "deny",
10569
+ settings: {
10570
+ max_command_length: 500,
10571
+ allow_env_expansion: false,
10572
+ allow_subshell: false,
10573
+ allow_pipes: false,
10574
+ allow_chains: false,
10575
+ allow_redirects: false
10576
+ }
10577
+ };
10578
+ PRESETS = {
10579
+ readonly: PRESET_READONLY,
10580
+ developer: PRESET_DEVELOPER,
10581
+ production: PRESET_PRODUCTION
10582
+ };
10583
+ WhitelistEngine = class {
10584
+ policy;
10585
+ constructor(policy) {
10586
+ this.policy = policy;
10587
+ this.policy.rules.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
10588
+ }
10589
+ /**
10590
+ * Evaluate a command against the whitelist policy.
10591
+ */
10592
+ evaluate(command) {
10593
+ const violations = [];
10594
+ const structuralCheck = this.checkStructural(command);
10595
+ violations.push(...structuralCheck.violations);
10596
+ if (structuralCheck.blocked) {
10597
+ return {
10598
+ allowed: false,
10599
+ require_approval: false,
10600
+ reason: `Structural violation: ${violations.join(", ")}`,
10601
+ risk: "critical",
10602
+ violations
10603
+ };
10604
+ }
10605
+ const enabledRules = this.policy.rules.filter((r) => r.enabled !== false);
10606
+ for (const rule of enabledRules) {
10607
+ if (matchPattern(command, rule.pattern)) {
10608
+ if (rule.max_args !== void 0) {
10609
+ const argCount = command.split(/\s+/).length - 1;
10610
+ if (argCount > rule.max_args) {
10611
+ violations.push(`Too many arguments: ${argCount} > ${rule.max_args}`);
10612
+ return {
10613
+ allowed: false,
10614
+ require_approval: false,
10615
+ matched_rule: rule,
10616
+ reason: `Argument limit exceeded for pattern "${rule.pattern}"`,
10617
+ risk: "high",
10618
+ violations
10619
+ };
10620
+ }
10621
+ }
10622
+ if (rule.allow) {
10623
+ return {
10624
+ allowed: true,
10625
+ require_approval: rule.require_approval ?? false,
10626
+ matched_rule: rule,
10627
+ reason: rule.description || `Matched whitelist pattern: ${rule.pattern}`,
10628
+ risk: rule.require_approval ? "medium" : "low",
10629
+ violations
10630
+ };
10631
+ } else {
10632
+ return {
10633
+ allowed: false,
10634
+ require_approval: false,
10635
+ matched_rule: rule,
10636
+ reason: rule.description || `Matched blacklist pattern: ${rule.pattern}`,
10637
+ risk: "high",
10638
+ violations
10639
+ };
10640
+ }
10641
+ }
10642
+ }
10643
+ switch (this.policy.mode) {
10644
+ case "whitelist":
10645
+ return {
10646
+ allowed: false,
10647
+ require_approval: false,
10648
+ reason: `No whitelist rule matches command. In whitelist mode, unlisted commands are DENIED.`,
10649
+ risk: "high",
10650
+ violations
10651
+ };
10652
+ case "blacklist":
10653
+ return {
10654
+ allowed: true,
10655
+ require_approval: false,
10656
+ reason: "No blacklist rule matches. Command allowed by default.",
10657
+ risk: "medium",
10658
+ violations
10659
+ };
10660
+ case "hybrid": {
10661
+ const defaultAction = this.policy.default_action || "deny";
10662
+ return {
10663
+ allowed: defaultAction === "allow",
10664
+ require_approval: defaultAction === "escalate",
10665
+ reason: `No rule matches. Hybrid mode default: ${defaultAction}`,
10666
+ risk: defaultAction === "allow" ? "medium" : "high",
10667
+ violations
10668
+ };
10669
+ }
10670
+ }
10671
+ }
10672
+ /**
10673
+ * Check structural constraints (pipes, chains, subshells, etc.)
10674
+ */
10675
+ checkStructural(command) {
10676
+ const violations = [];
10677
+ const settings = this.policy.settings || {};
10678
+ if (settings.max_command_length && command.length > settings.max_command_length) {
10679
+ violations.push(`Command too long: ${command.length} > ${settings.max_command_length}`);
10680
+ }
10681
+ if (settings.allow_subshell === false) {
10682
+ if (/\$\(/.test(command) || /`[^`]+`/.test(command)) {
10683
+ violations.push("Subshell execution not allowed");
10684
+ }
10685
+ }
10686
+ if (settings.allow_pipes === false) {
10687
+ if (/\|(?!\|)/.test(command)) {
10688
+ violations.push("Pipe chains not allowed");
10689
+ }
10690
+ }
10691
+ if (settings.allow_chains === false) {
10692
+ if (/[;&]|&&|\|\|/.test(command)) {
10693
+ violations.push("Command chaining not allowed");
10694
+ }
10695
+ }
10696
+ if (settings.allow_redirects === false) {
10697
+ if (/[<>]|>>/.test(command)) {
10698
+ violations.push("Output redirection not allowed");
10699
+ }
10700
+ }
10701
+ if (settings.allow_env_expansion === false) {
10702
+ if (/\$[A-Za-z_]/.test(command) || /\$\{/.test(command)) {
10703
+ violations.push("Environment variable expansion not allowed");
10704
+ }
10705
+ }
10706
+ return {
10707
+ blocked: violations.length > 0,
10708
+ violations
10709
+ };
10710
+ }
10711
+ /**
10712
+ * Get the current policy.
10713
+ */
10714
+ getPolicy() {
10715
+ return { ...this.policy };
10716
+ }
10717
+ /**
10718
+ * Add a rule dynamically.
10719
+ */
10720
+ addRule(rule) {
10721
+ this.policy.rules.push(rule);
10722
+ this.policy.rules.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
10723
+ }
10724
+ /**
10725
+ * Remove a rule by pattern.
10726
+ */
10727
+ removeRule(pattern) {
10728
+ const idx = this.policy.rules.findIndex((r) => r.pattern === pattern);
10729
+ if (idx >= 0) {
10730
+ this.policy.rules.splice(idx, 1);
10731
+ return true;
10732
+ }
10733
+ return false;
10734
+ }
10735
+ /**
10736
+ * List all rules.
10737
+ */
10738
+ listRules() {
10739
+ return [...this.policy.rules];
10740
+ }
10741
+ };
10742
+ }
10743
+ });
10744
+
9778
10745
  // src/index.ts
9779
10746
  var index_exports = {};
9780
10747
  __export(index_exports, {
@@ -9857,11 +10824,15 @@ async function checkVersionGate(packageName, currentVersion, apiKey) {
9857
10824
  }
9858
10825
  async function cli(args) {
9859
10826
  const { PolicyEngine: PolicyEngine2, DEFAULT_RULES: DEFAULT_RULES2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
10827
+ const { transformToolList: transformToolList2, shouldIntercept: shouldIntercept2, parseMode: parseMode2 } = await Promise.resolve().then(() => (init_toolReplacement(), toolReplacement_exports));
10828
+ const { normalize: normalizeCommand, summarize: summarizeNormalization } = await Promise.resolve().then(() => (init_commandNormalizer(), commandNormalizer_exports));
9860
10829
  let upstreamCmd = "";
9861
10830
  let upstreamArgs = [];
9862
10831
  let rulesFile = null;
9863
10832
  let verbose = false;
9864
10833
  let startupTimeoutMs = 3e4;
10834
+ let mode = "enforce";
10835
+ let whitelistPreset = null;
9865
10836
  for (let i = 0; i < args.length; i++) {
9866
10837
  switch (args[i]) {
9867
10838
  case "--upstream":
@@ -9883,14 +10854,39 @@ async function cli(args) {
9883
10854
  case "-t":
9884
10855
  startupTimeoutMs = parseInt(args[++i] ?? "30000", 10);
9885
10856
  break;
10857
+ case "--mode":
10858
+ case "-m":
10859
+ mode = args[++i] ?? "enforce";
10860
+ break;
10861
+ case "--whitelist":
10862
+ case "-w":
10863
+ whitelistPreset = args[++i] ?? null;
10864
+ break;
10865
+ default: {
10866
+ const modeMatch = args[i]?.match(/^--mode=(.+)$/);
10867
+ if (modeMatch) {
10868
+ mode = modeMatch[1];
10869
+ break;
10870
+ }
10871
+ const wlMatch = args[i]?.match(/^--whitelist=(.+)$/);
10872
+ if (wlMatch) {
10873
+ whitelistPreset = wlMatch[1];
10874
+ break;
10875
+ }
10876
+ }
9886
10877
  }
9887
10878
  }
9888
10879
  if (!upstreamCmd) {
9889
10880
  process.stderr.write(
9890
- 'Usage: sovr-mcp-proxy --upstream "command args..." [--rules policy.json] [--timeout 30000] [--verbose]\n'
10881
+ 'Usage: sovr-mcp-proxy --upstream "command args..." [--mode=exclusive|enforce|advisory|monitor] [--whitelist=preset|path] [--verbose]\n'
9891
10882
  );
9892
10883
  process.exit(1);
9893
10884
  }
10885
+ if (!["exclusive", "enforce", "advisory", "monitor"].includes(mode)) {
10886
+ process.stderr.write(`[SOVR] Invalid mode: ${mode}. Must be: exclusive|enforce|advisory|monitor
10887
+ `);
10888
+ process.exit(1);
10889
+ }
9894
10890
  let rules = DEFAULT_RULES2;
9895
10891
  if (rulesFile) {
9896
10892
  const fs = await import("fs");
@@ -9898,6 +10894,26 @@ async function cli(args) {
9898
10894
  const parsed = JSON.parse(content);
9899
10895
  rules = parsed.rules ?? parsed;
9900
10896
  }
10897
+ const { WhitelistEngine: WhitelistEngine2, PRESETS: WL_PRESETS } = await Promise.resolve().then(() => (init_whitelistEngine(), whitelistEngine_exports));
10898
+ let whitelist = null;
10899
+ if (whitelistPreset) {
10900
+ const presetNames = Object.keys(WL_PRESETS);
10901
+ if (presetNames.includes(whitelistPreset)) {
10902
+ whitelist = new WhitelistEngine2(WL_PRESETS[whitelistPreset]);
10903
+ } else {
10904
+ try {
10905
+ const fs = await import("fs");
10906
+ const content = fs.readFileSync(whitelistPreset, "utf-8");
10907
+ const parsed = JSON.parse(content);
10908
+ whitelist = new WhitelistEngine2(parsed);
10909
+ } catch (err) {
10910
+ process.stderr.write(`[SOVR] Failed to load whitelist from ${whitelistPreset}: ${err.message}
10911
+ `);
10912
+ process.exit(1);
10913
+ }
10914
+ }
10915
+ }
10916
+ const validatedMode = parseMode2(mode);
9901
10917
  const engine = new PolicyEngine2({
9902
10918
  rules,
9903
10919
  audit_log: true,
@@ -9910,6 +10926,24 @@ async function cli(args) {
9910
10926
  }
9911
10927
  }
9912
10928
  });
10929
+ process.stderr.write(`
10930
+ `);
10931
+ process.stderr.write(` \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
10932
+ `);
10933
+ process.stderr.write(` \u2551 SOVR MCP Proxy v${PROXY_VERSION} \u2551
10934
+ `);
10935
+ process.stderr.write(` \u2551 Mode: ${mode.toUpperCase().padEnd(40)}\u2551
10936
+ `);
10937
+ if (whitelist) {
10938
+ process.stderr.write(` \u2551 Whitelist: ${(whitelistPreset ?? "custom").padEnd(35)}\u2551
10939
+ `);
10940
+ }
10941
+ process.stderr.write(` \u2551 Upstream: ${upstreamCmd.substring(0, 36).padEnd(36)}\u2551
10942
+ `);
10943
+ process.stderr.write(` \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
10944
+ `);
10945
+ process.stderr.write(`
10946
+ `);
9913
10947
  const proxy = new McpProxy({
9914
10948
  engine,
9915
10949
  upstream: { command: upstreamCmd, args: upstreamArgs },
@@ -9926,8 +10960,37 @@ async function cli(args) {
9926
10960
  `[ESCALATED] ${info.toolName}: ${info.decision.reason}
9927
10961
  `
9928
10962
  );
10963
+ },
10964
+ onIntercept: (info) => {
10965
+ if (info.arguments?.command && typeof info.arguments.command === "string") {
10966
+ const normalized = normalizeCommand(info.arguments.command);
10967
+ if (verbose) {
10968
+ process.stderr.write(`[SOVR] Normalized: ${summarizeNormalization(normalized)}
10969
+ `);
10970
+ if (normalized.suspicious) {
10971
+ process.stderr.write(`[SOVR] \u26A0 Suspicious: ${normalized.suspicion_reasons.join(", ")}
10972
+ `);
10973
+ }
10974
+ }
10975
+ }
10976
+ if (whitelist && info.arguments?.command && typeof info.arguments.command === "string") {
10977
+ const wlResult = whitelist.evaluate(info.arguments.command);
10978
+ if (!wlResult.allowed && (validatedMode === "enforce" || validatedMode === "exclusive")) {
10979
+ if (verbose) {
10980
+ process.stderr.write(`[SOVR] Whitelist DENIED: ${wlResult.reason}
10981
+ `);
10982
+ }
10983
+ }
10984
+ }
9929
10985
  }
9930
10986
  });
10987
+ if (validatedMode === "exclusive") {
10988
+ proxy.on("intercept", () => {
10989
+ if (verbose) {
10990
+ process.stderr.write("[SOVR] Exclusive mode: all tool calls routed through SOVR\n");
10991
+ }
10992
+ });
10993
+ }
9931
10994
  await proxy.start();
9932
10995
  }
9933
10996
  var import_node_child_process2, import_node_events, PROXY_VERSION, SOVR_MIN_VERSION_URL, SOVR_DASHBOARD_URL2, McpProxy, index_default;
@@ -9937,7 +11000,7 @@ var init_index = __esm({
9937
11000
  import_node_child_process2 = require("child_process");
9938
11001
  import_node_events = require("events");
9939
11002
  init_usageTracker();
9940
- PROXY_VERSION = "5.2.0";
11003
+ PROXY_VERSION = "7.0.0";
9941
11004
  SOVR_MIN_VERSION_URL = "https://api.sovr.inc/api/sovr/v1/version/check";
9942
11005
  SOVR_DASHBOARD_URL2 = "https://sovr.inc/dashboard/api-keys";
9943
11006
  McpProxy = class extends import_node_events.EventEmitter {
@@ -10684,6 +11747,305 @@ var init_index = __esm({
10684
11747
  }
10685
11748
  });
10686
11749
 
11750
+ // src/hooksAdapter.ts
11751
+ var hooksAdapter_exports = {};
11752
+ __export(hooksAdapter_exports, {
11753
+ detectClaudeCode: () => detectClaudeCode,
11754
+ generateAuditHookScript: () => generateAuditHookScript,
11755
+ generateHookScript: () => generateHookScript,
11756
+ generateHooksConfig: () => generateHooksConfig,
11757
+ installHooks: () => installHooks,
11758
+ uninstallHooks: () => uninstallHooks
11759
+ });
11760
+ function generateHookScript(config) {
11761
+ const failExit = config.failMode === "closed" ? 2 : 0;
11762
+ const endpoint = config.endpoint || "http://localhost:45557";
11763
+ const apiKeyRef = config.apiKey ? `"${config.apiKey}"` : '"${SOVR_API_KEY:-}"';
11764
+ return `#!/bin/bash
11765
+ # SOVR PreToolUse Hook for Claude Code
11766
+ # Generated by: npx sovr-mcp-proxy init --claude-code
11767
+ #
11768
+ # This hook intercepts ALL tool calls and evaluates them against
11769
+ # SOVR security policies BEFORE Claude Code executes them.
11770
+ #
11771
+ # Exit codes:
11772
+ # 0 = ALLOW (proceed with tool call)
11773
+ # 2 = BLOCK (reject tool call)
11774
+ #
11775
+ # Environment:
11776
+ # SOVR_API_KEY \u2014 API key for SOVR authentication
11777
+ # SOVR_ENDPOINT \u2014 SOVR daemon endpoint (default: ${endpoint})
11778
+ # SOVR_FAIL_MODE \u2014 'open' (default) or 'closed'
11779
+ # SOVR_HOOK_DEBUG \u2014 Set to '1' for debug logging
11780
+
11781
+ set -euo pipefail
11782
+
11783
+ # Configuration
11784
+ SOVR_ENDPOINT="\${SOVR_ENDPOINT:-${endpoint}}"
11785
+ SOVR_API_KEY=${apiKeyRef}
11786
+ SOVR_FAIL_MODE="\${SOVR_FAIL_MODE:-${config.failMode || "open"}}"
11787
+ SOVR_HOOK_DEBUG="\${SOVR_HOOK_DEBUG:-0}"
11788
+ FAIL_EXIT=${failExit}
11789
+
11790
+ # Debug logging
11791
+ debug() {
11792
+ if [ "$SOVR_HOOK_DEBUG" = "1" ]; then
11793
+ echo "[SOVR-HOOK $(date -u +%H:%M:%S)] $*" >&2
11794
+ fi
11795
+ }
11796
+
11797
+ # Read hook input from stdin
11798
+ INPUT=$(cat)
11799
+ debug "Input: $INPUT"
11800
+
11801
+ # Extract tool name and input from JSON
11802
+ TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "unknown")
11803
+ TOOL_INPUT=$(echo "$INPUT" | grep -o '"tool_input":{[^}]*}' | head -1 2>/dev/null || echo "{}")
11804
+
11805
+ debug "Tool: $TOOL_NAME"
11806
+
11807
+ # Skip non-dangerous tools (read-only operations)
11808
+ case "$TOOL_NAME" in
11809
+ Read|View|LS|Glob|Grep|TodoRead|WebFetch|WebSearch)
11810
+ debug "SKIP: Read-only tool $TOOL_NAME"
11811
+ exit 0
11812
+ ;;
11813
+ esac
11814
+
11815
+ # Extract command for Bash tools
11816
+ COMMAND=""
11817
+ case "$TOOL_NAME" in
11818
+ Bash|bash|shell|execute_command)
11819
+ COMMAND=$(echo "$TOOL_INPUT" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
11820
+ ;;
11821
+ Write|Edit|MultiEdit)
11822
+ COMMAND=$(echo "$TOOL_INPUT" | grep -o '"file_path":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
11823
+ ;;
11824
+ esac
11825
+
11826
+ debug "Command: $COMMAND"
11827
+
11828
+ # Call SOVR daemon for evaluation
11829
+ RESPONSE=$(curl -s -X POST "$SOVR_ENDPOINT/api/hook/evaluate" \\
11830
+ -H "Content-Type: application/json" \\
11831
+ -H "X-SOVR-API-Key: $SOVR_API_KEY" \\
11832
+ -d "{
11833
+ \\"tool_name\\": \\"$TOOL_NAME\\",
11834
+ \\"command\\": $(echo "$COMMAND" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || echo '""'),
11835
+ \\"channel\\": \\"hooks\\",
11836
+ \\"context\\": {
11837
+ \\"platform\\": \\"claude-code\\",
11838
+ \\"hook_event\\": \\"PreToolUse\\"
11839
+ }
11840
+ }" \\
11841
+ --connect-timeout 3 --max-time 5 2>/dev/null) || {
11842
+ debug "SOVR daemon unreachable, fail-$SOVR_FAIL_MODE"
11843
+ exit $FAIL_EXIT
11844
+ }
11845
+
11846
+ debug "Response: $RESPONSE"
11847
+
11848
+ # Parse verdict
11849
+ VERDICT=$(echo "$RESPONSE" | grep -o '"verdict":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
11850
+ REASON=$(echo "$RESPONSE" | grep -o '"reason":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "Policy evaluation failed")
11851
+
11852
+ case "$VERDICT" in
11853
+ allow)
11854
+ debug "ALLOWED: $TOOL_NAME"
11855
+ exit 0
11856
+ ;;
11857
+ deny)
11858
+ debug "BLOCKED: $TOOL_NAME \u2014 $REASON"
11859
+ # Output block reason as JSON for Claude Code to display
11860
+ echo "{\\"error\\": \\"SOVR Policy Violation: $REASON\\"}"
11861
+ exit 2
11862
+ ;;
11863
+ escalate)
11864
+ debug "ESCALATED: $TOOL_NAME \u2014 requires approval"
11865
+ echo "{\\"error\\": \\"SOVR: This action requires human approval. Reason: $REASON\\"}"
11866
+ exit 2
11867
+ ;;
11868
+ *)
11869
+ debug "UNKNOWN verdict: $VERDICT, fail-$SOVR_FAIL_MODE"
11870
+ exit $FAIL_EXIT
11871
+ ;;
11872
+ esac
11873
+ `;
11874
+ }
11875
+ function generateAuditHookScript(config) {
11876
+ const endpoint = config.endpoint || "http://localhost:45557";
11877
+ const apiKeyRef = config.apiKey ? `"${config.apiKey}"` : '"${SOVR_API_KEY:-}"';
11878
+ return `#!/bin/bash
11879
+ # SOVR PostToolUse Audit Hook for Claude Code
11880
+ # Records tool execution results for audit trail
11881
+ set -euo pipefail
11882
+
11883
+ SOVR_ENDPOINT="\${SOVR_ENDPOINT:-${endpoint}}"
11884
+ SOVR_API_KEY=${apiKeyRef}
11885
+
11886
+ INPUT=$(cat)
11887
+ TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "unknown")
11888
+
11889
+ # Fire-and-forget audit log
11890
+ curl -s -X POST "$SOVR_ENDPOINT/api/hook/audit" \\
11891
+ -H "Content-Type: application/json" \\
11892
+ -H "X-SOVR-API-Key: $SOVR_API_KEY" \\
11893
+ -d "{
11894
+ \\"tool_name\\": \\"$TOOL_NAME\\",
11895
+ \\"event\\": \\"PostToolUse\\",
11896
+ \\"timestamp\\": $(date +%s)000
11897
+ }" \\
11898
+ --connect-timeout 2 --max-time 3 2>/dev/null &
11899
+
11900
+ # Always allow (audit is non-blocking)
11901
+ exit 0
11902
+ `;
11903
+ }
11904
+ function generateHooksConfig(config) {
11905
+ const hookScriptPath = getHookScriptPath();
11906
+ const timeout = config.hookTimeout || DEFAULT_HOOK_TIMEOUT;
11907
+ const hooks = {
11908
+ PreToolUse: []
11909
+ };
11910
+ const patterns = config.toolPatterns || DEFAULT_TOOL_PATTERNS;
11911
+ for (const pattern of patterns) {
11912
+ hooks.PreToolUse.push({
11913
+ matcher: { tool_name: pattern },
11914
+ hooks: [{ command: hookScriptPath, timeout }]
11915
+ });
11916
+ }
11917
+ if (config.addAuditHook) {
11918
+ const auditScriptPath = getAuditHookScriptPath();
11919
+ hooks.PostToolUse = patterns.map((pattern) => ({
11920
+ matcher: { tool_name: pattern },
11921
+ hooks: [{ command: auditScriptPath, timeout: 5 }]
11922
+ }));
11923
+ }
11924
+ return hooks;
11925
+ }
11926
+ function installHooks(config) {
11927
+ const settingsPath = config.settingsPath || getDefaultSettingsPath();
11928
+ const hookScriptPath = getHookScriptPath();
11929
+ const auditScriptPath = config.addAuditHook ? getAuditHookScriptPath() : void 0;
11930
+ const settingsDir = (0, import_node_path6.dirname)(settingsPath);
11931
+ const hooksDir = (0, import_node_path6.dirname)(hookScriptPath);
11932
+ (0, import_node_fs6.mkdirSync)(settingsDir, { recursive: true });
11933
+ (0, import_node_fs6.mkdirSync)(hooksDir, { recursive: true });
11934
+ (0, import_node_fs6.writeFileSync)(hookScriptPath, generateHookScript(config), "utf-8");
11935
+ (0, import_node_fs6.chmodSync)(hookScriptPath, 493);
11936
+ if (auditScriptPath) {
11937
+ (0, import_node_fs6.writeFileSync)(auditScriptPath, generateAuditHookScript(config), "utf-8");
11938
+ (0, import_node_fs6.chmodSync)(auditScriptPath, 493);
11939
+ }
11940
+ let existingSettings = {};
11941
+ let backup;
11942
+ if ((0, import_node_fs6.existsSync)(settingsPath)) {
11943
+ try {
11944
+ const raw = (0, import_node_fs6.readFileSync)(settingsPath, "utf-8");
11945
+ existingSettings = JSON.parse(raw);
11946
+ backup = `${settingsPath}.sovr-backup.${Date.now()}`;
11947
+ (0, import_node_fs6.writeFileSync)(backup, raw, "utf-8");
11948
+ } catch {
11949
+ }
11950
+ }
11951
+ const sovrHooks = generateHooksConfig(config);
11952
+ const mergedHooks = {};
11953
+ if (existingSettings.hooks) {
11954
+ for (const [event, defs] of Object.entries(existingSettings.hooks)) {
11955
+ mergedHooks[event] = defs.filter(
11956
+ (d) => !d.hooks.some((h) => h.command.includes("sovr"))
11957
+ );
11958
+ }
11959
+ }
11960
+ for (const [event, defs] of Object.entries(sovrHooks)) {
11961
+ if (!mergedHooks[event]) mergedHooks[event] = [];
11962
+ mergedHooks[event].push(...defs);
11963
+ }
11964
+ const updatedSettings = {
11965
+ ...existingSettings,
11966
+ hooks: mergedHooks
11967
+ };
11968
+ (0, import_node_fs6.writeFileSync)(settingsPath, JSON.stringify(updatedSettings, null, 2), "utf-8");
11969
+ return {
11970
+ settingsPath,
11971
+ hookScriptPath,
11972
+ auditScriptPath,
11973
+ installed: true,
11974
+ backup
11975
+ };
11976
+ }
11977
+ function uninstallHooks(config) {
11978
+ const settingsPath = config?.settingsPath || getDefaultSettingsPath();
11979
+ if (!(0, import_node_fs6.existsSync)(settingsPath)) {
11980
+ return { removed: false, settingsPath };
11981
+ }
11982
+ try {
11983
+ const raw = (0, import_node_fs6.readFileSync)(settingsPath, "utf-8");
11984
+ const settings = JSON.parse(raw);
11985
+ if (settings.hooks) {
11986
+ for (const [event, defs] of Object.entries(settings.hooks)) {
11987
+ settings.hooks[event] = defs.filter(
11988
+ (d) => !d.hooks.some((h) => h.command.includes("sovr"))
11989
+ );
11990
+ }
11991
+ }
11992
+ (0, import_node_fs6.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
11993
+ return { removed: true, settingsPath };
11994
+ } catch {
11995
+ return { removed: false, settingsPath };
11996
+ }
11997
+ }
11998
+ function getDefaultSettingsPath() {
11999
+ return (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".claude", "settings.json");
12000
+ }
12001
+ function getHookScriptPath() {
12002
+ return (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".sovr", "hooks", "sovr-pretool-hook.sh");
12003
+ }
12004
+ function getAuditHookScriptPath() {
12005
+ return (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".sovr", "hooks", "sovr-posttool-audit.sh");
12006
+ }
12007
+ function detectClaudeCode() {
12008
+ const defaultPath = getDefaultSettingsPath();
12009
+ if ((0, import_node_fs6.existsSync)((0, import_node_path6.dirname)(defaultPath))) {
12010
+ return {
12011
+ found: true,
12012
+ settingsPath: defaultPath
12013
+ };
12014
+ }
12015
+ const altPaths = [
12016
+ (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".config", "claude", "settings.json"),
12017
+ (0, import_node_path6.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "Claude", "settings.json")
12018
+ ];
12019
+ for (const p of altPaths) {
12020
+ if ((0, import_node_fs6.existsSync)((0, import_node_path6.dirname)(p))) {
12021
+ return { found: true, settingsPath: p };
12022
+ }
12023
+ }
12024
+ return { found: false };
12025
+ }
12026
+ var import_node_fs6, import_node_path6, import_node_os5, DEFAULT_TOOL_PATTERNS, DEFAULT_HOOK_TIMEOUT;
12027
+ var init_hooksAdapter = __esm({
12028
+ "src/hooksAdapter.ts"() {
12029
+ "use strict";
12030
+ import_node_fs6 = require("fs");
12031
+ import_node_path6 = require("path");
12032
+ import_node_os5 = require("os");
12033
+ DEFAULT_TOOL_PATTERNS = [
12034
+ "Bash",
12035
+ "bash",
12036
+ "shell",
12037
+ "execute_command",
12038
+ "Write",
12039
+ // File write operations
12040
+ "Edit",
12041
+ // File edit operations
12042
+ "MultiEdit"
12043
+ // Multi-file edit operations
12044
+ ];
12045
+ DEFAULT_HOOK_TIMEOUT = 10;
12046
+ }
12047
+ });
12048
+
10687
12049
  // src/cli.ts
10688
12050
  var import_meta = {};
10689
12051
  async function main(args) {
@@ -10694,6 +12056,10 @@ async function main(args) {
10694
12056
  await initCli2(args.slice(1));
10695
12057
  return;
10696
12058
  }
12059
+ case "hooks": {
12060
+ await hooksCli(args.slice(1));
12061
+ return;
12062
+ }
10697
12063
  case "keys": {
10698
12064
  const { keysCli: keysCli2 } = await Promise.resolve().then(() => (init_apiKeyManager(), apiKeyManager_exports));
10699
12065
  await keysCli2(args.slice(1));
@@ -10718,11 +12084,11 @@ async function main(args) {
10718
12084
  case "--version":
10719
12085
  case "-V": {
10720
12086
  try {
10721
- const { readFileSync: readFileSync5 } = await import("fs");
10722
- const { join: join5, dirname: dirname3 } = await import("path");
12087
+ const { readFileSync: readFileSync7 } = await import("fs");
12088
+ const { join: join7, dirname: dirname4 } = await import("path");
10723
12089
  const { fileURLToPath } = await import("url");
10724
- const __dirname = dirname3(fileURLToPath(import_meta.url));
10725
- const pkg = JSON.parse(readFileSync5(join5(__dirname, "..", "package.json"), "utf-8"));
12090
+ const __dirname = dirname4(fileURLToPath(import_meta.url));
12091
+ const pkg = JSON.parse(readFileSync7(join7(__dirname, "..", "package.json"), "utf-8"));
10726
12092
  process.stderr.write(`sovr-mcp-proxy v${pkg.version}
10727
12093
  `);
10728
12094
  } catch {
@@ -10737,15 +12103,151 @@ async function main(args) {
10737
12103
  }
10738
12104
  }
10739
12105
  }
12106
+ async function hooksCli(args) {
12107
+ const { installHooks: installHooks2, uninstallHooks: uninstallHooks2, detectClaudeCode: detectClaudeCode2, generateHooksConfig: generateHooksConfig2 } = await Promise.resolve().then(() => (init_hooksAdapter(), hooksAdapter_exports));
12108
+ const subcommand = args[0] || "install";
12109
+ let failMode = "open";
12110
+ let sovrEndpoint = "http://localhost:3271";
12111
+ let projectDir = process.cwd();
12112
+ for (let i = 1; i < args.length; i++) {
12113
+ if (args[i] === "--fail-closed") failMode = "closed";
12114
+ if (args[i] === "--fail-open") failMode = "open";
12115
+ if (args[i] === "--endpoint" && args[i + 1]) sovrEndpoint = args[++i];
12116
+ if (args[i] === "--dir" && args[i + 1]) projectDir = args[++i];
12117
+ }
12118
+ const config = {
12119
+ endpoint: sovrEndpoint,
12120
+ failMode,
12121
+ toolPatterns: ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit"],
12122
+ addAuditHook: true
12123
+ };
12124
+ switch (subcommand) {
12125
+ case "install": {
12126
+ const hooksConfig = generateHooksConfig2(config);
12127
+ const { generateHookScript: generateHookScript2, generateAuditHookScript: generateAuditHookScript2 } = await Promise.resolve().then(() => (init_hooksAdapter(), hooksAdapter_exports));
12128
+ const preToolScript = generateHookScript2(config);
12129
+ const postToolScript = config.addAuditHook ? generateAuditHookScript2(config) : null;
12130
+ const fs = await import("fs");
12131
+ const path = await import("path");
12132
+ const settingsDir = path.join(projectDir, ".claude");
12133
+ const settingsFile = path.join(settingsDir, "settings.json");
12134
+ if (!fs.existsSync(settingsDir)) {
12135
+ fs.mkdirSync(settingsDir, { recursive: true });
12136
+ }
12137
+ let existingSettings = {};
12138
+ if (fs.existsSync(settingsFile)) {
12139
+ try {
12140
+ existingSettings = JSON.parse(fs.readFileSync(settingsFile, "utf-8"));
12141
+ } catch {
12142
+ }
12143
+ }
12144
+ const newSettings = {
12145
+ ...existingSettings,
12146
+ hooks: hooksConfig
12147
+ };
12148
+ fs.writeFileSync(settingsFile, JSON.stringify(newSettings, null, 2));
12149
+ const scriptsDir = path.join(settingsDir, "hooks");
12150
+ if (!fs.existsSync(scriptsDir)) {
12151
+ fs.mkdirSync(scriptsDir, { recursive: true });
12152
+ }
12153
+ fs.writeFileSync(
12154
+ path.join(scriptsDir, "sovr-pre-tool.sh"),
12155
+ preToolScript,
12156
+ { mode: 493 }
12157
+ );
12158
+ if (postToolScript) {
12159
+ fs.writeFileSync(
12160
+ path.join(scriptsDir, "sovr-post-tool.sh"),
12161
+ postToolScript,
12162
+ { mode: 493 }
12163
+ );
12164
+ }
12165
+ process.stderr.write(`
12166
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
12167
+ \u2551 SOVR Claude Code Hooks Installed \u2551
12168
+ \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
12169
+ \u2551 \u2551
12170
+ \u2551 Settings: ${settingsFile.padEnd(47)}\u2551
12171
+ \u2551 Scripts: ${scriptsDir.padEnd(47)}\u2551
12172
+ \u2551 Fail mode: ${failMode.padEnd(47)}\u2551
12173
+ \u2551 Endpoint: ${sovrEndpoint.padEnd(47)}\u2551
12174
+ \u2551 \u2551
12175
+ \u2551 Every Bash/Write/Edit call will now pass through SOVR. \u2551
12176
+ \u2551 Use 'sovr-mcp-proxy hooks status' to verify. \u2551
12177
+ \u2551 \u2551
12178
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
12179
+ `);
12180
+ break;
12181
+ }
12182
+ case "uninstall": {
12183
+ const fs = await import("fs");
12184
+ const path = await import("path");
12185
+ const settingsFile = path.join(projectDir, ".claude", "settings.json");
12186
+ if (fs.existsSync(settingsFile)) {
12187
+ try {
12188
+ const settings = JSON.parse(fs.readFileSync(settingsFile, "utf-8"));
12189
+ delete settings.hooks;
12190
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
12191
+ process.stderr.write("[SOVR] Hooks removed from .claude/settings.json\n");
12192
+ } catch (err) {
12193
+ process.stderr.write(`[SOVR] Error removing hooks: ${err.message}
12194
+ `);
12195
+ }
12196
+ }
12197
+ const scriptsDir = path.join(projectDir, ".claude", "hooks");
12198
+ for (const f of ["sovr-pre-tool.sh", "sovr-post-tool.sh"]) {
12199
+ const fp = path.join(scriptsDir, f);
12200
+ if (fs.existsSync(fp)) fs.unlinkSync(fp);
12201
+ }
12202
+ process.stderr.write("[SOVR] Hooks uninstalled successfully.\n");
12203
+ break;
12204
+ }
12205
+ case "status": {
12206
+ const fs = await import("fs");
12207
+ const path = await import("path");
12208
+ const settingsFile = path.join(projectDir, ".claude", "settings.json");
12209
+ if (!fs.existsSync(settingsFile)) {
12210
+ process.stderr.write("[SOVR] No .claude/settings.json found. Hooks not installed.\n");
12211
+ return;
12212
+ }
12213
+ try {
12214
+ const settings = JSON.parse(fs.readFileSync(settingsFile, "utf-8"));
12215
+ if (settings.hooks) {
12216
+ const preHooks = settings.hooks.PreToolUse || [];
12217
+ const postHooks = settings.hooks.PostToolUse || [];
12218
+ process.stderr.write(`[SOVR] Hooks Status:
12219
+ `);
12220
+ process.stderr.write(` PreToolUse hooks: ${preHooks.length}
12221
+ `);
12222
+ process.stderr.write(` PostToolUse hooks: ${postHooks.length}
12223
+ `);
12224
+ process.stderr.write(` Settings file: ${settingsFile}
12225
+ `);
12226
+ } else {
12227
+ process.stderr.write("[SOVR] No hooks configured in settings.json\n");
12228
+ }
12229
+ } catch (err) {
12230
+ process.stderr.write(`[SOVR] Error reading settings: ${err.message}
12231
+ `);
12232
+ }
12233
+ break;
12234
+ }
12235
+ default:
12236
+ process.stderr.write(`Unknown hooks subcommand: ${subcommand}
12237
+ Usage: sovr-mcp-proxy hooks [install|uninstall|status]
12238
+ `);
12239
+ }
12240
+ }
10740
12241
  function printHelp() {
10741
12242
  process.stderr.write(`
10742
- sovr-mcp-proxy \u2014 The Responsibility Layer for AI Agents
12243
+ sovr-mcp-proxy v7.0.0 \u2014 The Responsibility Layer for AI Agents
10743
12244
 
10744
12245
  USAGE:
10745
12246
  sovr-mcp-proxy [COMMAND] [OPTIONS]
10746
12247
 
10747
12248
  COMMANDS:
10748
12249
  init One-command setup for AI coding platforms
12250
+ hooks Install/manage Claude Code Hooks (PreToolUse)
10749
12251
  keys Manage SOVR API keys
10750
12252
  usage View usage statistics
10751
12253
  daemon Manage persistent background daemon
@@ -10753,10 +12255,25 @@ COMMANDS:
10753
12255
 
10754
12256
  PROXY OPTIONS (default mode):
10755
12257
  --upstream, -u Downstream MCP server command to wrap
12258
+ --mode, -m Security mode: exclusive|enforce|advisory|monitor
12259
+ exclusive \u2014 Replace all native tools (strongest)
12260
+ enforce \u2014 Intercept + block violations (default)
12261
+ advisory \u2014 Warn but don't block
12262
+ monitor \u2014 Silent audit only
12263
+ --whitelist, -w Whitelist preset or path: readonly|developer|production|<file>
10756
12264
  --rules, -r Path to custom policy rules file
10757
12265
  --timeout, -t Startup timeout in ms (default: 30000)
10758
12266
  --verbose, -v Verbose output
10759
12267
 
12268
+ HOOKS OPTIONS:
12269
+ install Install Claude Code PreToolUse hooks (default)
12270
+ uninstall Remove SOVR hooks from .claude/settings.json
12271
+ status Show current hooks configuration
12272
+ --fail-closed Block on SOVR unreachable (default: fail-open)
12273
+ --fail-open Allow on SOVR unreachable
12274
+ --endpoint URL SOVR daemon endpoint (default: http://localhost:3271)
12275
+ --dir PATH Project directory (default: cwd)
12276
+
10760
12277
  INIT OPTIONS:
10761
12278
  --claude-code Configure Claude Code
10762
12279
  --claude-desktop Configure Claude Desktop
@@ -10785,31 +12302,43 @@ DAEMON OPTIONS:
10785
12302
  --foreground Run in foreground (for debugging)
10786
12303
  --verbose Verbose logging
10787
12304
 
10788
- EXAMPLES:
10789
- # One-command setup (auto-detect platforms)
10790
- npx sovr-mcp-proxy init
12305
+ SECURITY MODES:
12306
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
12307
+ \u2502 exclusive \u2502 SOVR replaces native Bash/Shell tools. \u2502
12308
+ \u2502 \u2502 LLM can ONLY execute through sovr_exec. \u2502
12309
+ \u2502 \u2502 Bypass is architecturally impossible. \u2502
12310
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
12311
+ \u2502 enforce \u2502 Native tools preserved but all calls intercepted.\u2502
12312
+ \u2502 \u2502 Violations are blocked with error response. \u2502
12313
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
12314
+ \u2502 advisory \u2502 Violations trigger warnings in stderr. \u2502
12315
+ \u2502 \u2502 Calls are still forwarded to upstream. \u2502
12316
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
12317
+ \u2502 monitor \u2502 Silent audit logging only. \u2502
12318
+ \u2502 \u2502 No warnings, no blocking. \u2502
12319
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
10791
12320
 
10792
- # Setup for Cursor with API key
10793
- npx sovr-mcp-proxy init --cursor --api-key sovr_sk_abc123
12321
+ WHITELIST PRESETS:
12322
+ readonly \u2014 git status/log/diff, ls, cat, find, grep (read-only ops)
12323
+ developer \u2014 readonly + git add/commit/push, npm/pnpm, tsc, eslint
12324
+ production \u2014 developer + docker, kubectl, terraform (with restrictions)
10794
12325
 
10795
- # Start proxy wrapping another MCP server
10796
- npx sovr-mcp-proxy --upstream "npx -y @modelcontextprotocol/server-filesystem /tmp"
12326
+ EXAMPLES:
12327
+ # Maximum security: exclusive mode + readonly whitelist
12328
+ npx sovr-mcp-proxy --upstream "npx @mcp/server-fs /tmp" --mode=exclusive --whitelist=readonly
10797
12329
 
10798
- # Check usage
10799
- npx sovr-mcp-proxy usage
12330
+ # Developer workflow: enforce mode + developer whitelist
12331
+ npx sovr-mcp-proxy --upstream "npx @mcp/server-fs ." --mode=enforce --whitelist=developer
10800
12332
 
10801
- # Manage keys
10802
- npx sovr-mcp-proxy keys add sovr_sk_abc123
12333
+ # Install Claude Code hooks (no upstream needed)
12334
+ npx sovr-mcp-proxy hooks install --fail-closed
10803
12335
 
10804
- # Start daemon (eliminates cold-start overhead)
10805
- npx sovr-mcp-proxy daemon start
12336
+ # One-command setup (auto-detect platforms)
12337
+ npx sovr-mcp-proxy init
10806
12338
 
10807
- # Start daemon with fail-closed mode
12339
+ # Start daemon with fail-closed
10808
12340
  npx sovr-mcp-proxy daemon start --fail-closed
10809
12341
 
10810
- # Check daemon status
10811
- npx sovr-mcp-proxy daemon status
10812
-
10813
12342
  ENVIRONMENT:
10814
12343
  SOVR_API_KEY API key for authentication
10815
12344
  SOVR_ENDPOINT Custom SOVR endpoint URL