sovr-mcp-proxy 6.0.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ };