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
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// src/hooksAdapter.ts
|
|
2
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, chmodSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
var DEFAULT_TOOL_PATTERNS = [
|
|
6
|
+
"Bash",
|
|
7
|
+
"bash",
|
|
8
|
+
"shell",
|
|
9
|
+
"execute_command",
|
|
10
|
+
"Write",
|
|
11
|
+
// File write operations
|
|
12
|
+
"Edit",
|
|
13
|
+
// File edit operations
|
|
14
|
+
"MultiEdit"
|
|
15
|
+
// Multi-file edit operations
|
|
16
|
+
];
|
|
17
|
+
var DEFAULT_HOOK_TIMEOUT = 10;
|
|
18
|
+
function generateHookScript(config) {
|
|
19
|
+
const failExit = config.failMode === "closed" ? 2 : 0;
|
|
20
|
+
const endpoint = config.endpoint || "http://localhost:45557";
|
|
21
|
+
const apiKeyRef = config.apiKey ? `"${config.apiKey}"` : '"${SOVR_API_KEY:-}"';
|
|
22
|
+
return `#!/bin/bash
|
|
23
|
+
# SOVR PreToolUse Hook for Claude Code
|
|
24
|
+
# Generated by: npx sovr-mcp-proxy init --claude-code
|
|
25
|
+
#
|
|
26
|
+
# This hook intercepts ALL tool calls and evaluates them against
|
|
27
|
+
# SOVR security policies BEFORE Claude Code executes them.
|
|
28
|
+
#
|
|
29
|
+
# Exit codes:
|
|
30
|
+
# 0 = ALLOW (proceed with tool call)
|
|
31
|
+
# 2 = BLOCK (reject tool call)
|
|
32
|
+
#
|
|
33
|
+
# Environment:
|
|
34
|
+
# SOVR_API_KEY \u2014 API key for SOVR authentication
|
|
35
|
+
# SOVR_ENDPOINT \u2014 SOVR daemon endpoint (default: ${endpoint})
|
|
36
|
+
# SOVR_FAIL_MODE \u2014 'open' (default) or 'closed'
|
|
37
|
+
# SOVR_HOOK_DEBUG \u2014 Set to '1' for debug logging
|
|
38
|
+
|
|
39
|
+
set -euo pipefail
|
|
40
|
+
|
|
41
|
+
# Configuration
|
|
42
|
+
SOVR_ENDPOINT="\${SOVR_ENDPOINT:-${endpoint}}"
|
|
43
|
+
SOVR_API_KEY=${apiKeyRef}
|
|
44
|
+
SOVR_FAIL_MODE="\${SOVR_FAIL_MODE:-${config.failMode || "open"}}"
|
|
45
|
+
SOVR_HOOK_DEBUG="\${SOVR_HOOK_DEBUG:-0}"
|
|
46
|
+
FAIL_EXIT=${failExit}
|
|
47
|
+
|
|
48
|
+
# Debug logging
|
|
49
|
+
debug() {
|
|
50
|
+
if [ "$SOVR_HOOK_DEBUG" = "1" ]; then
|
|
51
|
+
echo "[SOVR-HOOK $(date -u +%H:%M:%S)] $*" >&2
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Read hook input from stdin
|
|
56
|
+
INPUT=$(cat)
|
|
57
|
+
debug "Input: $INPUT"
|
|
58
|
+
|
|
59
|
+
# Extract tool name and input from JSON
|
|
60
|
+
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "unknown")
|
|
61
|
+
TOOL_INPUT=$(echo "$INPUT" | grep -o '"tool_input":{[^}]*}' | head -1 2>/dev/null || echo "{}")
|
|
62
|
+
|
|
63
|
+
debug "Tool: $TOOL_NAME"
|
|
64
|
+
|
|
65
|
+
# Skip non-dangerous tools (read-only operations)
|
|
66
|
+
case "$TOOL_NAME" in
|
|
67
|
+
Read|View|LS|Glob|Grep|TodoRead|WebFetch|WebSearch)
|
|
68
|
+
debug "SKIP: Read-only tool $TOOL_NAME"
|
|
69
|
+
exit 0
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
|
|
73
|
+
# Extract command for Bash tools
|
|
74
|
+
COMMAND=""
|
|
75
|
+
case "$TOOL_NAME" in
|
|
76
|
+
Bash|bash|shell|execute_command)
|
|
77
|
+
COMMAND=$(echo "$TOOL_INPUT" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
|
|
78
|
+
;;
|
|
79
|
+
Write|Edit|MultiEdit)
|
|
80
|
+
COMMAND=$(echo "$TOOL_INPUT" | grep -o '"file_path":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
|
|
84
|
+
debug "Command: $COMMAND"
|
|
85
|
+
|
|
86
|
+
# Call SOVR daemon for evaluation
|
|
87
|
+
RESPONSE=$(curl -s -X POST "$SOVR_ENDPOINT/api/hook/evaluate" \\
|
|
88
|
+
-H "Content-Type: application/json" \\
|
|
89
|
+
-H "X-SOVR-API-Key: $SOVR_API_KEY" \\
|
|
90
|
+
-d "{
|
|
91
|
+
\\"tool_name\\": \\"$TOOL_NAME\\",
|
|
92
|
+
\\"command\\": $(echo "$COMMAND" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || echo '""'),
|
|
93
|
+
\\"channel\\": \\"hooks\\",
|
|
94
|
+
\\"context\\": {
|
|
95
|
+
\\"platform\\": \\"claude-code\\",
|
|
96
|
+
\\"hook_event\\": \\"PreToolUse\\"
|
|
97
|
+
}
|
|
98
|
+
}" \\
|
|
99
|
+
--connect-timeout 3 --max-time 5 2>/dev/null) || {
|
|
100
|
+
debug "SOVR daemon unreachable, fail-$SOVR_FAIL_MODE"
|
|
101
|
+
exit $FAIL_EXIT
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
debug "Response: $RESPONSE"
|
|
105
|
+
|
|
106
|
+
# Parse verdict
|
|
107
|
+
VERDICT=$(echo "$RESPONSE" | grep -o '"verdict":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
|
|
108
|
+
REASON=$(echo "$RESPONSE" | grep -o '"reason":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "Policy evaluation failed")
|
|
109
|
+
|
|
110
|
+
case "$VERDICT" in
|
|
111
|
+
allow)
|
|
112
|
+
debug "ALLOWED: $TOOL_NAME"
|
|
113
|
+
exit 0
|
|
114
|
+
;;
|
|
115
|
+
deny)
|
|
116
|
+
debug "BLOCKED: $TOOL_NAME \u2014 $REASON"
|
|
117
|
+
# Output block reason as JSON for Claude Code to display
|
|
118
|
+
echo "{\\"error\\": \\"SOVR Policy Violation: $REASON\\"}"
|
|
119
|
+
exit 2
|
|
120
|
+
;;
|
|
121
|
+
escalate)
|
|
122
|
+
debug "ESCALATED: $TOOL_NAME \u2014 requires approval"
|
|
123
|
+
echo "{\\"error\\": \\"SOVR: This action requires human approval. Reason: $REASON\\"}"
|
|
124
|
+
exit 2
|
|
125
|
+
;;
|
|
126
|
+
*)
|
|
127
|
+
debug "UNKNOWN verdict: $VERDICT, fail-$SOVR_FAIL_MODE"
|
|
128
|
+
exit $FAIL_EXIT
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
function generateAuditHookScript(config) {
|
|
134
|
+
const endpoint = config.endpoint || "http://localhost:45557";
|
|
135
|
+
const apiKeyRef = config.apiKey ? `"${config.apiKey}"` : '"${SOVR_API_KEY:-}"';
|
|
136
|
+
return `#!/bin/bash
|
|
137
|
+
# SOVR PostToolUse Audit Hook for Claude Code
|
|
138
|
+
# Records tool execution results for audit trail
|
|
139
|
+
set -euo pipefail
|
|
140
|
+
|
|
141
|
+
SOVR_ENDPOINT="\${SOVR_ENDPOINT:-${endpoint}}"
|
|
142
|
+
SOVR_API_KEY=${apiKeyRef}
|
|
143
|
+
|
|
144
|
+
INPUT=$(cat)
|
|
145
|
+
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "unknown")
|
|
146
|
+
|
|
147
|
+
# Fire-and-forget audit log
|
|
148
|
+
curl -s -X POST "$SOVR_ENDPOINT/api/hook/audit" \\
|
|
149
|
+
-H "Content-Type: application/json" \\
|
|
150
|
+
-H "X-SOVR-API-Key: $SOVR_API_KEY" \\
|
|
151
|
+
-d "{
|
|
152
|
+
\\"tool_name\\": \\"$TOOL_NAME\\",
|
|
153
|
+
\\"event\\": \\"PostToolUse\\",
|
|
154
|
+
\\"timestamp\\": $(date +%s)000
|
|
155
|
+
}" \\
|
|
156
|
+
--connect-timeout 2 --max-time 3 2>/dev/null &
|
|
157
|
+
|
|
158
|
+
# Always allow (audit is non-blocking)
|
|
159
|
+
exit 0
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
function generateHooksConfig(config) {
|
|
163
|
+
const hookScriptPath = getHookScriptPath();
|
|
164
|
+
const timeout = config.hookTimeout || DEFAULT_HOOK_TIMEOUT;
|
|
165
|
+
const hooks = {
|
|
166
|
+
PreToolUse: []
|
|
167
|
+
};
|
|
168
|
+
const patterns = config.toolPatterns || DEFAULT_TOOL_PATTERNS;
|
|
169
|
+
for (const pattern of patterns) {
|
|
170
|
+
hooks.PreToolUse.push({
|
|
171
|
+
matcher: { tool_name: pattern },
|
|
172
|
+
hooks: [{ command: hookScriptPath, timeout }]
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (config.addAuditHook) {
|
|
176
|
+
const auditScriptPath = getAuditHookScriptPath();
|
|
177
|
+
hooks.PostToolUse = patterns.map((pattern) => ({
|
|
178
|
+
matcher: { tool_name: pattern },
|
|
179
|
+
hooks: [{ command: auditScriptPath, timeout: 5 }]
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
return hooks;
|
|
183
|
+
}
|
|
184
|
+
function installHooks(config) {
|
|
185
|
+
const settingsPath = config.settingsPath || getDefaultSettingsPath();
|
|
186
|
+
const hookScriptPath = getHookScriptPath();
|
|
187
|
+
const auditScriptPath = config.addAuditHook ? getAuditHookScriptPath() : void 0;
|
|
188
|
+
const settingsDir = dirname(settingsPath);
|
|
189
|
+
const hooksDir = dirname(hookScriptPath);
|
|
190
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
191
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
192
|
+
writeFileSync(hookScriptPath, generateHookScript(config), "utf-8");
|
|
193
|
+
chmodSync(hookScriptPath, 493);
|
|
194
|
+
if (auditScriptPath) {
|
|
195
|
+
writeFileSync(auditScriptPath, generateAuditHookScript(config), "utf-8");
|
|
196
|
+
chmodSync(auditScriptPath, 493);
|
|
197
|
+
}
|
|
198
|
+
let existingSettings = {};
|
|
199
|
+
let backup;
|
|
200
|
+
if (existsSync(settingsPath)) {
|
|
201
|
+
try {
|
|
202
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
203
|
+
existingSettings = JSON.parse(raw);
|
|
204
|
+
backup = `${settingsPath}.sovr-backup.${Date.now()}`;
|
|
205
|
+
writeFileSync(backup, raw, "utf-8");
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const sovrHooks = generateHooksConfig(config);
|
|
210
|
+
const mergedHooks = {};
|
|
211
|
+
if (existingSettings.hooks) {
|
|
212
|
+
for (const [event, defs] of Object.entries(existingSettings.hooks)) {
|
|
213
|
+
mergedHooks[event] = defs.filter(
|
|
214
|
+
(d) => !d.hooks.some((h) => h.command.includes("sovr"))
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const [event, defs] of Object.entries(sovrHooks)) {
|
|
219
|
+
if (!mergedHooks[event]) mergedHooks[event] = [];
|
|
220
|
+
mergedHooks[event].push(...defs);
|
|
221
|
+
}
|
|
222
|
+
const updatedSettings = {
|
|
223
|
+
...existingSettings,
|
|
224
|
+
hooks: mergedHooks
|
|
225
|
+
};
|
|
226
|
+
writeFileSync(settingsPath, JSON.stringify(updatedSettings, null, 2), "utf-8");
|
|
227
|
+
return {
|
|
228
|
+
settingsPath,
|
|
229
|
+
hookScriptPath,
|
|
230
|
+
auditScriptPath,
|
|
231
|
+
installed: true,
|
|
232
|
+
backup
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function uninstallHooks(config) {
|
|
236
|
+
const settingsPath = config?.settingsPath || getDefaultSettingsPath();
|
|
237
|
+
if (!existsSync(settingsPath)) {
|
|
238
|
+
return { removed: false, settingsPath };
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
242
|
+
const settings = JSON.parse(raw);
|
|
243
|
+
if (settings.hooks) {
|
|
244
|
+
for (const [event, defs] of Object.entries(settings.hooks)) {
|
|
245
|
+
settings.hooks[event] = defs.filter(
|
|
246
|
+
(d) => !d.hooks.some((h) => h.command.includes("sovr"))
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
251
|
+
return { removed: true, settingsPath };
|
|
252
|
+
} catch {
|
|
253
|
+
return { removed: false, settingsPath };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function getDefaultSettingsPath() {
|
|
257
|
+
return join(homedir(), ".claude", "settings.json");
|
|
258
|
+
}
|
|
259
|
+
function getHookScriptPath() {
|
|
260
|
+
return join(homedir(), ".sovr", "hooks", "sovr-pretool-hook.sh");
|
|
261
|
+
}
|
|
262
|
+
function getAuditHookScriptPath() {
|
|
263
|
+
return join(homedir(), ".sovr", "hooks", "sovr-posttool-audit.sh");
|
|
264
|
+
}
|
|
265
|
+
function detectClaudeCode() {
|
|
266
|
+
const defaultPath = getDefaultSettingsPath();
|
|
267
|
+
if (existsSync(dirname(defaultPath))) {
|
|
268
|
+
return {
|
|
269
|
+
found: true,
|
|
270
|
+
settingsPath: defaultPath
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const altPaths = [
|
|
274
|
+
join(homedir(), ".config", "claude", "settings.json"),
|
|
275
|
+
join(homedir(), "Library", "Application Support", "Claude", "settings.json")
|
|
276
|
+
];
|
|
277
|
+
for (const p of altPaths) {
|
|
278
|
+
if (existsSync(dirname(p))) {
|
|
279
|
+
return { found: true, settingsPath: p };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { found: false };
|
|
283
|
+
}
|
|
284
|
+
export {
|
|
285
|
+
detectClaudeCode,
|
|
286
|
+
generateAuditHookScript,
|
|
287
|
+
generateHookScript,
|
|
288
|
+
generateHooksConfig,
|
|
289
|
+
installHooks,
|
|
290
|
+
uninstallHooks
|
|
291
|
+
};
|