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/LICENSE +54 -19
- package/README.md +387 -164
- 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/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
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..." [--
|
|
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 = "
|
|
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:
|
|
10722
|
-
const { join:
|
|
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 =
|
|
10725
|
-
const pkg = JSON.parse(
|
|
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
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
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
|
-
|
|
10793
|
-
|
|
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
|
-
|
|
10796
|
-
|
|
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
|
-
#
|
|
10799
|
-
npx sovr-mcp-proxy
|
|
12330
|
+
# Developer workflow: enforce mode + developer whitelist
|
|
12331
|
+
npx sovr-mcp-proxy --upstream "npx @mcp/server-fs ." --mode=enforce --whitelist=developer
|
|
10800
12332
|
|
|
10801
|
-
#
|
|
10802
|
-
npx sovr-mcp-proxy
|
|
12333
|
+
# Install Claude Code hooks (no upstream needed)
|
|
12334
|
+
npx sovr-mcp-proxy hooks install --fail-closed
|
|
10803
12335
|
|
|
10804
|
-
#
|
|
10805
|
-
npx sovr-mcp-proxy
|
|
12336
|
+
# One-command setup (auto-detect platforms)
|
|
12337
|
+
npx sovr-mcp-proxy init
|
|
10806
12338
|
|
|
10807
|
-
# Start daemon with fail-closed
|
|
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
|