protect-mcp 0.4.6 → 0.5.1
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/README.md +152 -161
- package/dist/chunk-IAJJA5IW.mjs +827 -0
- package/dist/{chunk-VF3OCG4D.mjs → chunk-IUFFDQYZ.mjs} +15 -583
- package/dist/chunk-UEHLYOJY.mjs +321 -0
- package/dist/chunk-V52W3XIN.mjs +582 -0
- package/dist/{chunk-VIA2B65K.mjs → chunk-YKM6W6T7.mjs} +4 -2
- package/dist/cli.js +1452 -100
- package/dist/cli.mjs +205 -10
- package/dist/hook-patterns.d.mts +41 -0
- package/dist/hook-patterns.d.ts +41 -0
- package/dist/hook-patterns.js +348 -0
- package/dist/hook-patterns.mjs +13 -0
- package/dist/hook-server.d.mts +38 -0
- package/dist/hook-server.d.ts +38 -0
- package/dist/hook-server.js +1211 -0
- package/dist/hook-server.mjs +8 -0
- package/dist/{http-transport-XCHIKTYG.mjs → http-transport-GXIXLVJQ.mjs} +2 -1
- package/dist/index.d.mts +194 -1
- package/dist/index.d.ts +194 -1
- package/dist/index.js +1181 -22
- package/dist/index.mjs +35 -19
- package/package.json +7 -4
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReceiptBuffer,
|
|
3
|
+
checkRateLimit,
|
|
4
|
+
evaluateCedar,
|
|
5
|
+
getSignerInfo,
|
|
6
|
+
getToolPolicy,
|
|
7
|
+
initSigning,
|
|
8
|
+
isCedarAvailable,
|
|
9
|
+
isSigningEnabled,
|
|
10
|
+
loadCedarPolicies,
|
|
11
|
+
loadPolicy,
|
|
12
|
+
parseRateLimit,
|
|
13
|
+
signDecision
|
|
14
|
+
} from "./chunk-V52W3XIN.mjs";
|
|
15
|
+
|
|
16
|
+
// src/hook-server.ts
|
|
17
|
+
import { createServer } from "http";
|
|
18
|
+
import { createHash, randomUUID, randomBytes } from "crypto";
|
|
19
|
+
import { appendFileSync, readFileSync, existsSync, readdirSync } from "fs";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
var DEFAULT_PORT = 9377;
|
|
22
|
+
var LOG_FILE = ".protect-mcp-log.jsonl";
|
|
23
|
+
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
24
|
+
var PAYLOAD_HASH_THRESHOLD = 1024;
|
|
25
|
+
function detectSwarmContext() {
|
|
26
|
+
const teamName = process.env.CLAUDE_CODE_TEAM_NAME;
|
|
27
|
+
const agentId = process.env.CLAUDE_CODE_AGENT_ID;
|
|
28
|
+
const agentName = process.env.CLAUDE_CODE_AGENT_NAME;
|
|
29
|
+
if (!teamName && !agentId) {
|
|
30
|
+
return { agent_type: "standalone" };
|
|
31
|
+
}
|
|
32
|
+
const isLeader = !agentId || agentId === "team-lead";
|
|
33
|
+
return {
|
|
34
|
+
team_name: teamName,
|
|
35
|
+
agent_id: agentId,
|
|
36
|
+
agent_name: agentName,
|
|
37
|
+
is_leader: isLeader,
|
|
38
|
+
agent_type: isLeader ? "coordinator" : "worker"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function computePayloadDigest(input) {
|
|
42
|
+
const content = typeof input === "string" ? input : JSON.stringify(input || {});
|
|
43
|
+
const size = Buffer.byteLength(content, "utf-8");
|
|
44
|
+
if (size <= PAYLOAD_HASH_THRESHOLD) {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
input_hash: createHash("sha256").update(content).digest("hex"),
|
|
49
|
+
input_size: size,
|
|
50
|
+
truncated: true,
|
|
51
|
+
preview: content.slice(0, 256)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function computeOutputDigest(output) {
|
|
55
|
+
const content = typeof output === "string" ? output : JSON.stringify(output || {});
|
|
56
|
+
const size = Buffer.byteLength(content, "utf-8");
|
|
57
|
+
if (size <= PAYLOAD_HASH_THRESHOLD) {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
output_hash: createHash("sha256").update(content).digest("hex"),
|
|
62
|
+
output_size: size
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function detectSandboxState() {
|
|
66
|
+
if (process.env.SANDBOX_ENABLED === "1" || process.env.CLAUDE_CODE_SANDBOX === "1") {
|
|
67
|
+
return "enabled";
|
|
68
|
+
}
|
|
69
|
+
if (process.platform === "darwin" && process.env.APP_SANDBOX_CONTAINER_ID) {
|
|
70
|
+
return "enabled";
|
|
71
|
+
}
|
|
72
|
+
if (process.platform === "linux") {
|
|
73
|
+
try {
|
|
74
|
+
const procStatus = readFileSync("/proc/self/status", "utf-8");
|
|
75
|
+
if (procStatus.includes("Seccomp: 2")) return "enabled";
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return "unavailable";
|
|
80
|
+
}
|
|
81
|
+
async function handlePreToolUse(input, state) {
|
|
82
|
+
const hookStart = Date.now();
|
|
83
|
+
const toolName = input.toolName || "unknown";
|
|
84
|
+
const requestId = input.toolUseId || randomUUID().slice(0, 12);
|
|
85
|
+
state.inflightTools.set(requestId, {
|
|
86
|
+
tool: toolName,
|
|
87
|
+
startedAt: hookStart,
|
|
88
|
+
requestId
|
|
89
|
+
});
|
|
90
|
+
const payloadDigest = computePayloadDigest(input.toolInput);
|
|
91
|
+
const swarm = {
|
|
92
|
+
...state.swarmContext,
|
|
93
|
+
...input.agentId && { agent_id: input.agentId },
|
|
94
|
+
...input.agentName && { agent_name: input.agentName },
|
|
95
|
+
...input.teamName && { team_name: input.teamName },
|
|
96
|
+
...input.agentType && { agent_type: input.agentType }
|
|
97
|
+
};
|
|
98
|
+
if (state.cedarPolicies) {
|
|
99
|
+
try {
|
|
100
|
+
const cedarDecision = await evaluateCedar(state.cedarPolicies, {
|
|
101
|
+
tool: toolName,
|
|
102
|
+
tier: "unknown",
|
|
103
|
+
// Hook mode doesn't have admission tier yet
|
|
104
|
+
agentId: swarm.agent_id,
|
|
105
|
+
context: {
|
|
106
|
+
hook_event: "PreToolUse",
|
|
107
|
+
...input.toolInput || {}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (!cedarDecision.allowed) {
|
|
111
|
+
const reason = cedarDecision.reason || "cedar_deny";
|
|
112
|
+
const hookLatency2 = Date.now() - hookStart;
|
|
113
|
+
const denyKey2 = `${toolName}:${input.sessionId || "default"}`;
|
|
114
|
+
const denyCount = (state.denyCounter.get(denyKey2) || 0) + 1;
|
|
115
|
+
state.denyCounter.set(denyKey2, denyCount);
|
|
116
|
+
const suggestion = `permit(principal, action == Action::"MCP::Tool::call", resource == Tool::"${toolName}");`;
|
|
117
|
+
state.permissionSuggestions.set(toolName, suggestion);
|
|
118
|
+
emitDecisionLog(state, {
|
|
119
|
+
tool: toolName,
|
|
120
|
+
decision: "deny",
|
|
121
|
+
reason_code: reason,
|
|
122
|
+
request_id: requestId,
|
|
123
|
+
hook_event: "PreToolUse",
|
|
124
|
+
swarm: swarm.team_name ? swarm : void 0,
|
|
125
|
+
timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
|
|
126
|
+
payload_digest: payloadDigest,
|
|
127
|
+
deny_iteration: denyCount,
|
|
128
|
+
sandbox_state: detectSandboxState(),
|
|
129
|
+
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
130
|
+
});
|
|
131
|
+
if (denyCount === 1) {
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
`[PROTECT_MCP] No Cedar permit for "${toolName}" \u2014 suggest:
|
|
134
|
+
${suggestion}
|
|
135
|
+
`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
hookSpecificOutput: {
|
|
140
|
+
hookEventName: "PreToolUse",
|
|
141
|
+
permissionDecision: "deny",
|
|
142
|
+
permissionDecisionReason: `[ScopeBlind] Denied by Cedar policy. ${reason}. Forbidden: "${toolName}" is not permitted. Try a read-only alternative.` + (denyCount > 1 ? ` (attempt ${denyCount})` : "")
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (state.verbose) {
|
|
148
|
+
process.stderr.write(`[PROTECT_MCP] Cedar eval error: ${err instanceof Error ? err.message : err}
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (state.jsonPolicy?.policy) {
|
|
154
|
+
const toolPolicy = getToolPolicy(toolName, state.jsonPolicy.policy);
|
|
155
|
+
if (toolPolicy.block) {
|
|
156
|
+
const hookLatency2 = Date.now() - hookStart;
|
|
157
|
+
emitDecisionLog(state, {
|
|
158
|
+
tool: toolName,
|
|
159
|
+
decision: "deny",
|
|
160
|
+
reason_code: "policy_block",
|
|
161
|
+
request_id: requestId,
|
|
162
|
+
hook_event: "PreToolUse",
|
|
163
|
+
swarm: swarm.team_name ? swarm : void 0,
|
|
164
|
+
timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
|
|
165
|
+
payload_digest: payloadDigest,
|
|
166
|
+
sandbox_state: detectSandboxState()
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
hookSpecificOutput: {
|
|
170
|
+
hookEventName: "PreToolUse",
|
|
171
|
+
permissionDecision: "deny",
|
|
172
|
+
permissionDecisionReason: `[ScopeBlind] "${toolName}" is blocked by policy.`
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (toolPolicy.require_approval) {
|
|
177
|
+
const hookLatency2 = Date.now() - hookStart;
|
|
178
|
+
emitDecisionLog(state, {
|
|
179
|
+
tool: toolName,
|
|
180
|
+
decision: "require_approval",
|
|
181
|
+
reason_code: "requires_human_approval",
|
|
182
|
+
request_id: requestId,
|
|
183
|
+
hook_event: "PreToolUse",
|
|
184
|
+
swarm: swarm.team_name ? swarm : void 0,
|
|
185
|
+
timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
|
|
186
|
+
sandbox_state: detectSandboxState()
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
hookSpecificOutput: {
|
|
190
|
+
hookEventName: "PreToolUse",
|
|
191
|
+
permissionDecision: "ask",
|
|
192
|
+
permissionDecisionReason: `[ScopeBlind] "${toolName}" requires human approval. Policy: ${state.policyDigest}`
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (toolPolicy.rate_limit) {
|
|
197
|
+
try {
|
|
198
|
+
const limit = parseRateLimit(toolPolicy.rate_limit);
|
|
199
|
+
const key = `tool:${toolName}:hook`;
|
|
200
|
+
const { allowed, remaining } = checkRateLimit(key, limit, state.rateLimitStore);
|
|
201
|
+
if (!allowed) {
|
|
202
|
+
const hookLatency2 = Date.now() - hookStart;
|
|
203
|
+
emitDecisionLog(state, {
|
|
204
|
+
tool: toolName,
|
|
205
|
+
decision: "deny",
|
|
206
|
+
reason_code: "rate_limit_exceeded",
|
|
207
|
+
request_id: requestId,
|
|
208
|
+
hook_event: "PreToolUse",
|
|
209
|
+
swarm: swarm.team_name ? swarm : void 0,
|
|
210
|
+
timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
|
|
211
|
+
sandbox_state: detectSandboxState()
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
hookSpecificOutput: {
|
|
215
|
+
hookEventName: "PreToolUse",
|
|
216
|
+
permissionDecision: "deny",
|
|
217
|
+
permissionDecisionReason: `[ScopeBlind] "${toolName}" rate limit exceeded (${toolPolicy.rate_limit}).`
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const hookLatency = Date.now() - hookStart;
|
|
226
|
+
const denyKey = `${toolName}:${input.sessionId || "default"}`;
|
|
227
|
+
state.denyCounter.delete(denyKey);
|
|
228
|
+
emitDecisionLog(state, {
|
|
229
|
+
tool: toolName,
|
|
230
|
+
decision: "allow",
|
|
231
|
+
reason_code: state.cedarPolicies ? "cedar_allow" : state.jsonPolicy ? "policy_allow" : "observe_mode",
|
|
232
|
+
request_id: requestId,
|
|
233
|
+
hook_event: "PreToolUse",
|
|
234
|
+
swarm: swarm.team_name ? swarm : void 0,
|
|
235
|
+
timing: { hook_latency_ms: hookLatency, started_at: hookStart },
|
|
236
|
+
payload_digest: payloadDigest,
|
|
237
|
+
sandbox_state: detectSandboxState(),
|
|
238
|
+
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
239
|
+
});
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
async function handlePostToolUse(input, state) {
|
|
243
|
+
const toolName = input.toolName || "unknown";
|
|
244
|
+
const requestId = input.toolUseId || randomUUID().slice(0, 12);
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
const inflight = state.inflightTools.get(requestId);
|
|
247
|
+
const timing = {
|
|
248
|
+
completed_at: now
|
|
249
|
+
};
|
|
250
|
+
if (inflight) {
|
|
251
|
+
timing.tool_duration_ms = now - inflight.startedAt;
|
|
252
|
+
timing.started_at = inflight.startedAt;
|
|
253
|
+
state.inflightTools.delete(requestId);
|
|
254
|
+
}
|
|
255
|
+
const outputDigest = computeOutputDigest(input.toolResult);
|
|
256
|
+
const receiptId = randomUUID().slice(0, 8);
|
|
257
|
+
const policyName = state.cedarPolicies ? `cedar:${state.policyDigest}` : state.policyDigest;
|
|
258
|
+
const additionalContext = `[ScopeBlind] Tool call receipted. Policy: ${policyName}. Decision: allow. Receipt: #${receiptId}.` + (timing.tool_duration_ms !== void 0 ? ` Duration: ${timing.tool_duration_ms}ms.` : "") + (timing.hook_latency_ms !== void 0 ? ` Overhead: ${timing.hook_latency_ms}ms.` : "");
|
|
259
|
+
emitDecisionLog(state, {
|
|
260
|
+
tool: toolName,
|
|
261
|
+
decision: "allow",
|
|
262
|
+
reason_code: "post_execution_receipt",
|
|
263
|
+
request_id: requestId,
|
|
264
|
+
hook_event: "PostToolUse",
|
|
265
|
+
swarm: state.swarmContext.team_name ? state.swarmContext : void 0,
|
|
266
|
+
timing,
|
|
267
|
+
payload_digest: outputDigest ? {
|
|
268
|
+
truncated: true,
|
|
269
|
+
output_hash: outputDigest.output_hash,
|
|
270
|
+
output_size: outputDigest.output_size
|
|
271
|
+
} : void 0,
|
|
272
|
+
sandbox_state: detectSandboxState()
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
hookSpecificOutput: {
|
|
276
|
+
hookEventName: "PostToolUse",
|
|
277
|
+
additionalContext
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function handleSubagentStart(input, state) {
|
|
282
|
+
const agentId = input.agentId || "unknown";
|
|
283
|
+
const agentType = input.agentType || "worker";
|
|
284
|
+
emitDecisionLog(state, {
|
|
285
|
+
tool: `subagent:${agentId}`,
|
|
286
|
+
decision: "allow",
|
|
287
|
+
reason_code: "subagent_started",
|
|
288
|
+
request_id: randomUUID().slice(0, 12),
|
|
289
|
+
hook_event: "SubagentStart",
|
|
290
|
+
swarm: {
|
|
291
|
+
...state.swarmContext,
|
|
292
|
+
agent_id: agentId,
|
|
293
|
+
agent_name: input.agentName,
|
|
294
|
+
agent_type: agentType
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
if (state.verbose) {
|
|
298
|
+
process.stderr.write(`[PROTECT_MCP] Subagent started: ${agentId} (${agentType})
|
|
299
|
+
`);
|
|
300
|
+
}
|
|
301
|
+
return {};
|
|
302
|
+
}
|
|
303
|
+
function handleSubagentStop(input, state) {
|
|
304
|
+
const agentId = input.agentId || "unknown";
|
|
305
|
+
emitDecisionLog(state, {
|
|
306
|
+
tool: `subagent:${agentId}`,
|
|
307
|
+
decision: "allow",
|
|
308
|
+
reason_code: "subagent_stopped",
|
|
309
|
+
request_id: randomUUID().slice(0, 12),
|
|
310
|
+
hook_event: "SubagentStop",
|
|
311
|
+
swarm: {
|
|
312
|
+
...state.swarmContext,
|
|
313
|
+
agent_id: agentId,
|
|
314
|
+
agent_name: input.agentName
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return {};
|
|
318
|
+
}
|
|
319
|
+
function handleTaskCreated(input, state) {
|
|
320
|
+
emitDecisionLog(state, {
|
|
321
|
+
tool: `task:${input.taskId || "unknown"}`,
|
|
322
|
+
decision: "allow",
|
|
323
|
+
reason_code: "task_created",
|
|
324
|
+
request_id: randomUUID().slice(0, 12),
|
|
325
|
+
hook_event: "TaskCreated",
|
|
326
|
+
swarm: {
|
|
327
|
+
...state.swarmContext,
|
|
328
|
+
agent_name: input.teammateName
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
function handleTaskCompleted(input, state) {
|
|
334
|
+
emitDecisionLog(state, {
|
|
335
|
+
tool: `task:${input.taskId || "unknown"}`,
|
|
336
|
+
decision: "allow",
|
|
337
|
+
reason_code: "task_completed",
|
|
338
|
+
request_id: randomUUID().slice(0, 12),
|
|
339
|
+
hook_event: "TaskCompleted",
|
|
340
|
+
swarm: state.swarmContext
|
|
341
|
+
});
|
|
342
|
+
return {};
|
|
343
|
+
}
|
|
344
|
+
function handleSessionStart(input, state) {
|
|
345
|
+
emitDecisionLog(state, {
|
|
346
|
+
tool: "session",
|
|
347
|
+
decision: "allow",
|
|
348
|
+
reason_code: "session_started",
|
|
349
|
+
request_id: input.sessionId || randomUUID().slice(0, 12),
|
|
350
|
+
hook_event: "SessionStart",
|
|
351
|
+
swarm: state.swarmContext,
|
|
352
|
+
sandbox_state: detectSandboxState()
|
|
353
|
+
});
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
function handleSessionEnd(input, state) {
|
|
357
|
+
const suggestions = [...state.permissionSuggestions.entries()];
|
|
358
|
+
if (suggestions.length > 0) {
|
|
359
|
+
process.stderr.write(`
|
|
360
|
+
[PROTECT_MCP] Session summary \u2014 ${suggestions.length} policy suggestion(s):
|
|
361
|
+
`);
|
|
362
|
+
for (const [tool, suggestion] of suggestions) {
|
|
363
|
+
process.stderr.write(` ${tool}: ${suggestion}
|
|
364
|
+
`);
|
|
365
|
+
}
|
|
366
|
+
process.stderr.write("\n");
|
|
367
|
+
}
|
|
368
|
+
emitDecisionLog(state, {
|
|
369
|
+
tool: "session",
|
|
370
|
+
decision: "allow",
|
|
371
|
+
reason_code: "session_ended",
|
|
372
|
+
request_id: input.sessionId || randomUUID().slice(0, 12),
|
|
373
|
+
hook_event: "SessionEnd",
|
|
374
|
+
swarm: state.swarmContext
|
|
375
|
+
});
|
|
376
|
+
return {};
|
|
377
|
+
}
|
|
378
|
+
function handleTeammateIdle(input, state) {
|
|
379
|
+
emitDecisionLog(state, {
|
|
380
|
+
tool: `teammate:${input.agentId || "unknown"}`,
|
|
381
|
+
decision: "allow",
|
|
382
|
+
reason_code: "teammate_idle",
|
|
383
|
+
request_id: randomUUID().slice(0, 12),
|
|
384
|
+
hook_event: "TeammateIdle",
|
|
385
|
+
swarm: {
|
|
386
|
+
...state.swarmContext,
|
|
387
|
+
agent_id: input.agentId,
|
|
388
|
+
agent_name: input.agentName
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
return {};
|
|
392
|
+
}
|
|
393
|
+
function handleConfigChange(input, state) {
|
|
394
|
+
const configPath = input.filePath || input.configPath || "unknown";
|
|
395
|
+
const source = input.configSource || "unknown";
|
|
396
|
+
const isSelfModification = configPath.includes("settings.json") || configPath.includes(".claude/");
|
|
397
|
+
if (isSelfModification) {
|
|
398
|
+
state.configAlerts.push({
|
|
399
|
+
timestamp: Date.now(),
|
|
400
|
+
path: configPath,
|
|
401
|
+
source
|
|
402
|
+
});
|
|
403
|
+
process.stderr.write(
|
|
404
|
+
`[PROTECT_MCP] \u26A0\uFE0F TAMPER ALERT: Config file modified: ${configPath} (source: ${source})
|
|
405
|
+
`
|
|
406
|
+
);
|
|
407
|
+
emitDecisionLog(state, {
|
|
408
|
+
tool: "config",
|
|
409
|
+
decision: "deny",
|
|
410
|
+
reason_code: "config_tamper_detected",
|
|
411
|
+
request_id: randomUUID().slice(0, 12),
|
|
412
|
+
hook_event: "ConfigChange",
|
|
413
|
+
swarm: state.swarmContext
|
|
414
|
+
});
|
|
415
|
+
} else {
|
|
416
|
+
emitDecisionLog(state, {
|
|
417
|
+
tool: "config",
|
|
418
|
+
decision: "allow",
|
|
419
|
+
reason_code: "config_changed",
|
|
420
|
+
request_id: randomUUID().slice(0, 12),
|
|
421
|
+
hook_event: "ConfigChange"
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return {};
|
|
425
|
+
}
|
|
426
|
+
function handleStop(input, state) {
|
|
427
|
+
const suggestions = [...state.permissionSuggestions.entries()];
|
|
428
|
+
if (suggestions.length > 0) {
|
|
429
|
+
process.stderr.write(`
|
|
430
|
+
[PROTECT_MCP] Final policy suggestions:
|
|
431
|
+
`);
|
|
432
|
+
for (const [tool, suggestion] of suggestions) {
|
|
433
|
+
process.stderr.write(` ${suggestion}
|
|
434
|
+
`);
|
|
435
|
+
}
|
|
436
|
+
process.stderr.write("\n");
|
|
437
|
+
}
|
|
438
|
+
emitDecisionLog(state, {
|
|
439
|
+
tool: "session",
|
|
440
|
+
decision: "allow",
|
|
441
|
+
reason_code: "agent_stopped",
|
|
442
|
+
request_id: randomUUID().slice(0, 12),
|
|
443
|
+
hook_event: "Stop",
|
|
444
|
+
swarm: state.swarmContext
|
|
445
|
+
});
|
|
446
|
+
return {};
|
|
447
|
+
}
|
|
448
|
+
function emitDecisionLog(state, entry) {
|
|
449
|
+
const mode = state.enforce ? "enforce" : "shadow";
|
|
450
|
+
const otelTraceId = randomBytes(16).toString("hex");
|
|
451
|
+
const otelSpanId = randomBytes(8).toString("hex");
|
|
452
|
+
const log = {
|
|
453
|
+
v: 2,
|
|
454
|
+
tool: entry.tool || "unknown",
|
|
455
|
+
decision: entry.decision || "allow",
|
|
456
|
+
reason_code: entry.reason_code || "default_allow",
|
|
457
|
+
policy_digest: state.policyDigest,
|
|
458
|
+
policy_engine: state.cedarPolicies ? "cedar" : "built-in",
|
|
459
|
+
request_id: entry.request_id || randomUUID().slice(0, 12),
|
|
460
|
+
timestamp: Date.now(),
|
|
461
|
+
mode,
|
|
462
|
+
otel_trace_id: otelTraceId,
|
|
463
|
+
otel_span_id: otelSpanId,
|
|
464
|
+
...entry.tier && { tier: entry.tier },
|
|
465
|
+
...entry.hook_event && { hook_event: entry.hook_event },
|
|
466
|
+
...entry.swarm && { swarm: entry.swarm },
|
|
467
|
+
...entry.timing && { timing: entry.timing },
|
|
468
|
+
...entry.payload_digest && { payload_digest: entry.payload_digest },
|
|
469
|
+
...entry.deny_iteration && { deny_iteration: entry.deny_iteration },
|
|
470
|
+
...entry.sandbox_state && { sandbox_state: entry.sandbox_state },
|
|
471
|
+
...entry.plan_receipt_id && { plan_receipt_id: entry.plan_receipt_id }
|
|
472
|
+
};
|
|
473
|
+
process.stderr.write(`[PROTECT_MCP] ${JSON.stringify(log)}
|
|
474
|
+
`);
|
|
475
|
+
try {
|
|
476
|
+
appendFileSync(state.logFilePath, JSON.stringify(log) + "\n");
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
if (isSigningEnabled()) {
|
|
480
|
+
const signed = signDecision(log);
|
|
481
|
+
if (signed.signed) {
|
|
482
|
+
try {
|
|
483
|
+
appendFileSync(state.receiptFilePath, signed.signed + "\n");
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
state.receiptBuffer.add(log.request_id, signed.signed);
|
|
487
|
+
} else if (signed.warning) {
|
|
488
|
+
process.stderr.write(`[PROTECT_MCP] Warning: ${signed.warning}
|
|
489
|
+
`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function routeHookEvent(input, state) {
|
|
494
|
+
switch (input.hookEventName) {
|
|
495
|
+
case "PreToolUse":
|
|
496
|
+
return handlePreToolUse(input, state);
|
|
497
|
+
case "PostToolUse":
|
|
498
|
+
return handlePostToolUse(input, state);
|
|
499
|
+
case "SubagentStart":
|
|
500
|
+
return handleSubagentStart(input, state);
|
|
501
|
+
case "SubagentStop":
|
|
502
|
+
return handleSubagentStop(input, state);
|
|
503
|
+
case "TaskCreated":
|
|
504
|
+
return handleTaskCreated(input, state);
|
|
505
|
+
case "TaskCompleted":
|
|
506
|
+
return handleTaskCompleted(input, state);
|
|
507
|
+
case "SessionStart":
|
|
508
|
+
return handleSessionStart(input, state);
|
|
509
|
+
case "SessionEnd":
|
|
510
|
+
return handleSessionEnd(input, state);
|
|
511
|
+
case "TeammateIdle":
|
|
512
|
+
return handleTeammateIdle(input, state);
|
|
513
|
+
case "ConfigChange":
|
|
514
|
+
return handleConfigChange(input, state);
|
|
515
|
+
case "Stop":
|
|
516
|
+
return handleStop(input, state);
|
|
517
|
+
default:
|
|
518
|
+
if (state.verbose) {
|
|
519
|
+
process.stderr.write(`[PROTECT_MCP] Unknown hook event: ${input.hookEventName}
|
|
520
|
+
`);
|
|
521
|
+
}
|
|
522
|
+
return {};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function startHookServer(options = {}) {
|
|
526
|
+
const port = options.port || DEFAULT_PORT;
|
|
527
|
+
const verbose = options.verbose || false;
|
|
528
|
+
const enforce = options.enforce || false;
|
|
529
|
+
let cedarPolicies = null;
|
|
530
|
+
let jsonPolicy = null;
|
|
531
|
+
let policyDigest = "none";
|
|
532
|
+
const cedarDir = options.cedarDir || findCedarDir();
|
|
533
|
+
if (cedarDir) {
|
|
534
|
+
try {
|
|
535
|
+
cedarPolicies = loadCedarPolicies(cedarDir);
|
|
536
|
+
policyDigest = cedarPolicies.digest;
|
|
537
|
+
process.stderr.write(
|
|
538
|
+
`[PROTECT_MCP] Cedar policies loaded: ${cedarPolicies.fileCount} files from ${cedarDir} (digest: ${policyDigest})
|
|
539
|
+
`
|
|
540
|
+
);
|
|
541
|
+
const cedarAvailable = await isCedarAvailable();
|
|
542
|
+
if (!cedarAvailable) {
|
|
543
|
+
process.stderr.write(
|
|
544
|
+
"[PROTECT_MCP] Warning: @cedar-policy/cedar-wasm not installed. Cedar policies loaded but evaluation fallback is allow-all.\n"
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
} catch (err) {
|
|
548
|
+
process.stderr.write(`[PROTECT_MCP] Cedar load error: ${err instanceof Error ? err.message : err}
|
|
549
|
+
`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (options.policyPath) {
|
|
553
|
+
try {
|
|
554
|
+
jsonPolicy = loadPolicy(options.policyPath);
|
|
555
|
+
if (!cedarPolicies) policyDigest = jsonPolicy.digest;
|
|
556
|
+
process.stderr.write(`[PROTECT_MCP] JSON policy loaded from ${options.policyPath}
|
|
557
|
+
`);
|
|
558
|
+
if (jsonPolicy.signing) {
|
|
559
|
+
const warnings = await initSigning(jsonPolicy.signing);
|
|
560
|
+
for (const w of warnings) {
|
|
561
|
+
process.stderr.write(`[PROTECT_MCP] Warning: ${w}
|
|
562
|
+
`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} catch (err) {
|
|
566
|
+
process.stderr.write(`[PROTECT_MCP] Policy load error: ${err instanceof Error ? err.message : err}
|
|
567
|
+
`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (!jsonPolicy?.signing) {
|
|
571
|
+
const keyPath = join(process.cwd(), "keys", "gateway.json");
|
|
572
|
+
if (existsSync(keyPath)) {
|
|
573
|
+
const warnings = await initSigning({ key_path: keyPath, issuer: "protect-mcp", enabled: true });
|
|
574
|
+
for (const w of warnings) {
|
|
575
|
+
process.stderr.write(`[PROTECT_MCP] Warning: ${w}
|
|
576
|
+
`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const state = {
|
|
581
|
+
cedarPolicies,
|
|
582
|
+
jsonPolicy,
|
|
583
|
+
rateLimitStore: /* @__PURE__ */ new Map(),
|
|
584
|
+
receiptBuffer: new ReceiptBuffer(),
|
|
585
|
+
inflightTools: /* @__PURE__ */ new Map(),
|
|
586
|
+
denyCounter: /* @__PURE__ */ new Map(),
|
|
587
|
+
swarmContext: detectSwarmContext(),
|
|
588
|
+
activePlanReceiptId: null,
|
|
589
|
+
startTime: Date.now(),
|
|
590
|
+
port,
|
|
591
|
+
verbose,
|
|
592
|
+
enforce,
|
|
593
|
+
policyDigest,
|
|
594
|
+
logFilePath: join(process.cwd(), LOG_FILE),
|
|
595
|
+
receiptFilePath: join(process.cwd(), RECEIPTS_FILE),
|
|
596
|
+
permissionSuggestions: /* @__PURE__ */ new Map(),
|
|
597
|
+
configAlerts: []
|
|
598
|
+
};
|
|
599
|
+
const server = createServer(async (req, res) => {
|
|
600
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
601
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
602
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
603
|
+
res.setHeader("Content-Type", "application/json");
|
|
604
|
+
if (req.method === "OPTIONS") {
|
|
605
|
+
res.writeHead(204);
|
|
606
|
+
res.end();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
610
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
611
|
+
const signerInfo = getSignerInfo();
|
|
612
|
+
res.writeHead(200);
|
|
613
|
+
res.end(JSON.stringify({
|
|
614
|
+
status: "ok",
|
|
615
|
+
server: "protect-mcp-hooks",
|
|
616
|
+
version: "0.5.0",
|
|
617
|
+
uptime_ms: Date.now() - state.startTime,
|
|
618
|
+
mode: enforce ? "enforce" : "shadow",
|
|
619
|
+
policy_digest: policyDigest,
|
|
620
|
+
policy_engine: cedarPolicies ? "cedar" : jsonPolicy ? "built-in" : "none",
|
|
621
|
+
signing: isSigningEnabled(),
|
|
622
|
+
swarm: state.swarmContext,
|
|
623
|
+
signer: signerInfo ? { kid: signerInfo.kid, issuer: signerInfo.issuer } : null,
|
|
624
|
+
cedar_files: cedarPolicies?.fileCount || 0
|
|
625
|
+
}));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (url.pathname === "/receipts" && req.method === "GET") {
|
|
629
|
+
const limit = parseInt(url.searchParams.get("limit") || "20", 10);
|
|
630
|
+
const receipts = state.receiptBuffer.getAll().slice(0, Math.min(limit, 100));
|
|
631
|
+
res.writeHead(200);
|
|
632
|
+
res.end(JSON.stringify({ count: receipts.length, total: state.receiptBuffer.count(), receipts }));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (url.pathname === "/receipts/latest" && req.method === "GET") {
|
|
636
|
+
const latest = state.receiptBuffer.getLatest();
|
|
637
|
+
if (!latest) {
|
|
638
|
+
res.writeHead(404);
|
|
639
|
+
res.end(JSON.stringify({ error: "no_receipts" }));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
res.writeHead(200);
|
|
643
|
+
res.end(JSON.stringify(latest));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (url.pathname === "/suggestions" && req.method === "GET") {
|
|
647
|
+
const suggestions = [...state.permissionSuggestions.entries()].map(([tool, rule]) => ({ tool, cedar_rule: rule }));
|
|
648
|
+
res.writeHead(200);
|
|
649
|
+
res.end(JSON.stringify({ count: suggestions.length, suggestions }));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (url.pathname === "/alerts" && req.method === "GET") {
|
|
653
|
+
res.writeHead(200);
|
|
654
|
+
res.end(JSON.stringify({ count: state.configAlerts.length, alerts: state.configAlerts }));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (url.pathname === "/hook" && req.method === "POST") {
|
|
658
|
+
let body = "";
|
|
659
|
+
req.on("data", (chunk) => {
|
|
660
|
+
body += chunk;
|
|
661
|
+
});
|
|
662
|
+
req.on("end", async () => {
|
|
663
|
+
try {
|
|
664
|
+
const raw = JSON.parse(body);
|
|
665
|
+
const input = normalizeHookInput(raw);
|
|
666
|
+
if (!input.hookEventName) {
|
|
667
|
+
res.writeHead(400);
|
|
668
|
+
res.end(JSON.stringify({ error: "missing_hook_event_name", hint: "Expected hook_event_name or hookEventName in POST body" }));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const response = await routeHookEvent(input, state);
|
|
672
|
+
res.writeHead(200);
|
|
673
|
+
res.end(JSON.stringify(response));
|
|
674
|
+
} catch (err) {
|
|
675
|
+
if (verbose) {
|
|
676
|
+
process.stderr.write(`[PROTECT_MCP] Hook error: ${err instanceof Error ? err.message : err}
|
|
677
|
+
`);
|
|
678
|
+
}
|
|
679
|
+
res.writeHead(400);
|
|
680
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
res.writeHead(404);
|
|
686
|
+
res.end(JSON.stringify({
|
|
687
|
+
error: "not_found",
|
|
688
|
+
endpoints: [
|
|
689
|
+
"POST /hook \u2014 Claude Code hook endpoint",
|
|
690
|
+
"GET /health \u2014 Health check",
|
|
691
|
+
"GET /receipts \u2014 Recent receipts",
|
|
692
|
+
"GET /receipts/latest \u2014 Most recent receipt",
|
|
693
|
+
"GET /suggestions \u2014 Policy suggestions",
|
|
694
|
+
"GET /alerts \u2014 Config tamper alerts"
|
|
695
|
+
]
|
|
696
|
+
}));
|
|
697
|
+
});
|
|
698
|
+
server.listen(port, "127.0.0.1", () => {
|
|
699
|
+
process.stderr.write(`
|
|
700
|
+
`);
|
|
701
|
+
process.stderr.write(`[PROTECT_MCP] \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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
|
|
702
|
+
`);
|
|
703
|
+
process.stderr.write(`[PROTECT_MCP] \u2502 protect-mcp Hook Server v0.5.0 \u2502
|
|
704
|
+
`);
|
|
705
|
+
process.stderr.write(`[PROTECT_MCP] \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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
|
|
706
|
+
`);
|
|
707
|
+
process.stderr.write(`[PROTECT_MCP] \u2502 Listening: http://127.0.0.1:${String(port).padEnd(5)} \u2502
|
|
708
|
+
`);
|
|
709
|
+
process.stderr.write(`[PROTECT_MCP] \u2502 Mode: ${(enforce ? "enforce" : "shadow").padEnd(36)}\u2502
|
|
710
|
+
`);
|
|
711
|
+
process.stderr.write(`[PROTECT_MCP] \u2502 Policy: ${(cedarPolicies ? `Cedar (${cedarPolicies.fileCount} files)` : jsonPolicy ? "JSON" : "none").padEnd(36)}\u2502
|
|
712
|
+
`);
|
|
713
|
+
process.stderr.write(`[PROTECT_MCP] \u2502 Signing: ${(isSigningEnabled() ? "Ed25519 \u2713" : "disabled").padEnd(36)}\u2502
|
|
714
|
+
`);
|
|
715
|
+
process.stderr.write(`[PROTECT_MCP] \u2502 Swarm: ${(state.swarmContext.team_name ? `${state.swarmContext.team_name} (${state.swarmContext.agent_type})` : "standalone").padEnd(36)}\u2502
|
|
716
|
+
`);
|
|
717
|
+
process.stderr.write(`[PROTECT_MCP] \u2502 Sandbox: ${detectSandboxState().padEnd(36)}\u2502
|
|
718
|
+
`);
|
|
719
|
+
process.stderr.write(`[PROTECT_MCP] \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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
|
|
720
|
+
`);
|
|
721
|
+
process.stderr.write(`
|
|
722
|
+
`);
|
|
723
|
+
process.stderr.write(`[PROTECT_MCP] Endpoints:
|
|
724
|
+
`);
|
|
725
|
+
process.stderr.write(` POST /hook \u2014 Claude Code hook receiver
|
|
726
|
+
`);
|
|
727
|
+
process.stderr.write(` GET /health \u2014 Health check + signer info
|
|
728
|
+
`);
|
|
729
|
+
process.stderr.write(` GET /receipts \u2014 Recent signed receipts
|
|
730
|
+
`);
|
|
731
|
+
process.stderr.write(` GET /suggestions \u2014 Auto-generated Cedar policy suggestions
|
|
732
|
+
`);
|
|
733
|
+
process.stderr.write(` GET /alerts \u2014 Config tamper alerts
|
|
734
|
+
`);
|
|
735
|
+
process.stderr.write(`
|
|
736
|
+
`);
|
|
737
|
+
process.stderr.write(`[PROTECT_MCP] deny decisions are AUTHORITATIVE \u2014 they cannot be overridden.
|
|
738
|
+
`);
|
|
739
|
+
process.stderr.write(`
|
|
740
|
+
`);
|
|
741
|
+
});
|
|
742
|
+
const shutdown = () => {
|
|
743
|
+
process.stderr.write("\n[PROTECT_MCP] Shutting down hook server...\n");
|
|
744
|
+
const suggestions = [...state.permissionSuggestions.entries()];
|
|
745
|
+
if (suggestions.length > 0) {
|
|
746
|
+
process.stderr.write(`[PROTECT_MCP] ${suggestions.length} policy suggestion(s) accumulated:
|
|
747
|
+
`);
|
|
748
|
+
for (const [tool, suggestion] of suggestions) {
|
|
749
|
+
process.stderr.write(` ${suggestion}
|
|
750
|
+
`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
server.close();
|
|
754
|
+
process.exit(0);
|
|
755
|
+
};
|
|
756
|
+
process.on("SIGINT", shutdown);
|
|
757
|
+
process.on("SIGTERM", shutdown);
|
|
758
|
+
return server;
|
|
759
|
+
}
|
|
760
|
+
function findCedarDir() {
|
|
761
|
+
for (const candidate of ["cedar", "policies", "."]) {
|
|
762
|
+
try {
|
|
763
|
+
if (existsSync(candidate)) {
|
|
764
|
+
const files = readdirSync(candidate, { encoding: "utf-8" });
|
|
765
|
+
if (files.some((f) => f.endsWith(".cedar"))) {
|
|
766
|
+
return candidate;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return void 0;
|
|
773
|
+
}
|
|
774
|
+
var SNAKE_TO_CAMEL_MAP = {
|
|
775
|
+
hook_event_name: "hookEventName",
|
|
776
|
+
session_id: "sessionId",
|
|
777
|
+
transcript_path: "transcriptPath",
|
|
778
|
+
permission_mode: "permissionMode",
|
|
779
|
+
agent_id: "agentId",
|
|
780
|
+
agent_type: "agentType",
|
|
781
|
+
tool_name: "toolName",
|
|
782
|
+
tool_input: "toolInput",
|
|
783
|
+
tool_use_id: "toolUseId",
|
|
784
|
+
tool_response: "toolResult",
|
|
785
|
+
// Claude Code sends tool_response, we read toolResult
|
|
786
|
+
stop_hook_active: "stopHookActive",
|
|
787
|
+
agent_transcript_path: "agentTranscriptPath",
|
|
788
|
+
last_assistant_message: "lastAssistantMessage",
|
|
789
|
+
teammate_name: "teammateName",
|
|
790
|
+
team_name: "teamName",
|
|
791
|
+
task_id: "taskId",
|
|
792
|
+
task_subject: "taskSubject",
|
|
793
|
+
task_description: "taskDescription",
|
|
794
|
+
file_path: "filePath",
|
|
795
|
+
config_path: "configPath",
|
|
796
|
+
old_cwd: "oldCwd",
|
|
797
|
+
new_cwd: "newCwd",
|
|
798
|
+
notification_type: "notificationType",
|
|
799
|
+
is_interrupt: "isInterrupt",
|
|
800
|
+
error_details: "errorDetails",
|
|
801
|
+
compact_summary: "compactSummary",
|
|
802
|
+
custom_instructions: "customInstructions",
|
|
803
|
+
worktree_path: "worktreePath",
|
|
804
|
+
trigger_file_path: "triggerFilePath",
|
|
805
|
+
parent_file_path: "parentFilePath",
|
|
806
|
+
memory_type: "memoryType",
|
|
807
|
+
load_reason: "loadReason",
|
|
808
|
+
mcp_server_name: "mcpServerName",
|
|
809
|
+
elicitation_id: "elicitationId",
|
|
810
|
+
requested_schema: "requestedSchema",
|
|
811
|
+
permission_suggestions: "permissionSuggestions"
|
|
812
|
+
};
|
|
813
|
+
function normalizeHookInput(raw) {
|
|
814
|
+
const result = {};
|
|
815
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
816
|
+
const camelKey = SNAKE_TO_CAMEL_MAP[key] || key;
|
|
817
|
+
result[camelKey] = value;
|
|
818
|
+
}
|
|
819
|
+
if (raw.source !== void 0 && raw.hook_event_name === "ConfigChange" && !raw.config_source) {
|
|
820
|
+
result["configSource"] = raw.source;
|
|
821
|
+
}
|
|
822
|
+
return result;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export {
|
|
826
|
+
startHookServer
|
|
827
|
+
};
|