multicorn-shield 1.1.0 → 1.3.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/CHANGELOG.md +29 -0
- package/dist/multicorn-proxy.js +2723 -2430
- package/dist/multicorn-shield.js +2677 -2383
- package/dist/openclaw-hook/handler.js +3 -3
- package/dist/openclaw-plugin/multicorn-shield.js +3 -3
- package/dist/shield-extension.js +32 -37
- package/package.json +2 -1
- package/plugins/cline/hooks/scripts/shared.cjs +49 -12
- package/plugins/gemini-cli/hooks/scripts/shared.cjs +49 -12
- package/plugins/multicorn-shield/.claude-plugin/plugin.json +8 -0
- package/plugins/multicorn-shield/hooks/hooks.json +26 -0
- package/plugins/multicorn-shield/hooks/scripts/claude-code-tool-map.cjs +138 -0
- package/plugins/multicorn-shield/hooks/scripts/post-tool-use.cjs +253 -0
- package/plugins/multicorn-shield/hooks/scripts/pre-tool-use.cjs +642 -0
- package/plugins/multicorn-shield/skills/shield-governance/SKILL.md +24 -0
- package/plugins/windsurf/hooks/scripts/post-action.cjs +60 -12
- package/plugins/windsurf/hooks/scripts/pre-action.cjs +60 -12
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code PostToolUse hook: logs completed tool calls to Shield (audit trail).
|
|
3
|
+
* Never blocks; always exit 0.
|
|
4
|
+
*
|
|
5
|
+
* Tool mapping: see `./claude-code-tool-map.cjs` (built from `src/hooks/claude-code-tool-map.ts`).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const fs = require("node:fs");
|
|
11
|
+
const http = require("node:http");
|
|
12
|
+
const https = require("node:https");
|
|
13
|
+
const os = require("node:os");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
|
|
16
|
+
const { mapClaudeCodeToolToShield } = require("./claude-code-tool-map.cjs");
|
|
17
|
+
|
|
18
|
+
const AUTH_HEADER = "X-Multicorn-Key";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @returns {Promise<string>}
|
|
22
|
+
*/
|
|
23
|
+
function readStdin() {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
process.stdin.setEncoding("utf8");
|
|
27
|
+
process.stdin.on("data", (c) => chunks.push(c));
|
|
28
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
29
|
+
process.stdin.on("error", reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Agent resolution for Claude Code (duplicated in pre-tool-use.cjs).
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} cwdResolved
|
|
36
|
+
* @param {string} workspacePath
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
function cwdUnderWorkspacePath(cwdResolved, workspacePath) {
|
|
40
|
+
const w = path.resolve(workspacePath);
|
|
41
|
+
if (cwdResolved === w) return true;
|
|
42
|
+
const prefix = w.endsWith(path.sep) ? w : w + path.sep;
|
|
43
|
+
return cwdResolved.startsWith(prefix);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {Record<string, unknown>} obj
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function resolveClaudeCodeAgentName(obj) {
|
|
51
|
+
const cwdRaw =
|
|
52
|
+
process.env.PWD !== undefined && String(process.env.PWD).length > 0
|
|
53
|
+
? process.env.PWD
|
|
54
|
+
: process.cwd();
|
|
55
|
+
const agents = obj.agents;
|
|
56
|
+
const defaultAgentRaw = obj.defaultAgent;
|
|
57
|
+
const defaultAgentName =
|
|
58
|
+
typeof defaultAgentRaw === "string" && defaultAgentRaw.length > 0 ? defaultAgentRaw : "";
|
|
59
|
+
|
|
60
|
+
if (!Array.isArray(agents)) {
|
|
61
|
+
return typeof obj.agentName === "string" ? obj.agentName : "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const matches = [];
|
|
65
|
+
for (const entry of agents) {
|
|
66
|
+
if (
|
|
67
|
+
entry &&
|
|
68
|
+
typeof entry === "object" &&
|
|
69
|
+
/** @type {{ platform?: string; name?: string; workspacePath?: string }} */ (entry)
|
|
70
|
+
.platform === "claude-code" &&
|
|
71
|
+
typeof (/** @type {{ name?: string }} */ (entry).name) === "string"
|
|
72
|
+
) {
|
|
73
|
+
matches.push(/** @type {{ name: string; workspacePath?: string }} */ (entry));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (matches.length === 0) {
|
|
78
|
+
return typeof obj.agentName === "string" ? obj.agentName : "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const withWs = matches.filter(
|
|
82
|
+
(m) => typeof m.workspacePath === "string" && m.workspacePath.length > 0,
|
|
83
|
+
);
|
|
84
|
+
const resolvedCwd = path.resolve(cwdRaw);
|
|
85
|
+
let best = null;
|
|
86
|
+
let bestLen = -1;
|
|
87
|
+
for (const m of withWs) {
|
|
88
|
+
if (!cwdUnderWorkspacePath(resolvedCwd, /** @type {string} */ (m.workspacePath))) continue;
|
|
89
|
+
const len = path.resolve(/** @type {string} */ (m.workspacePath)).length;
|
|
90
|
+
if (len > bestLen) {
|
|
91
|
+
bestLen = len;
|
|
92
|
+
best = m;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (best !== null) {
|
|
96
|
+
return best.name;
|
|
97
|
+
}
|
|
98
|
+
if (defaultAgentName.length > 0) {
|
|
99
|
+
const d = matches.find((m) => m.name === defaultAgentName);
|
|
100
|
+
if (d !== undefined) return d.name;
|
|
101
|
+
}
|
|
102
|
+
return matches[0].name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @returns {{ apiKey: string; baseUrl: string; agentName: string } | null}
|
|
107
|
+
*/
|
|
108
|
+
function loadConfig() {
|
|
109
|
+
try {
|
|
110
|
+
const configPath = path.join(os.homedir(), ".multicorn", "config.json");
|
|
111
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
112
|
+
const obj = JSON.parse(raw);
|
|
113
|
+
const apiKey = typeof obj.apiKey === "string" ? obj.apiKey : "";
|
|
114
|
+
const baseUrl =
|
|
115
|
+
typeof obj.baseUrl === "string" && obj.baseUrl.length > 0
|
|
116
|
+
? obj.baseUrl.replace(/\/+$/, "")
|
|
117
|
+
: "https://api.multicorn.ai";
|
|
118
|
+
const agentName = resolveClaudeCodeAgentName(obj);
|
|
119
|
+
return { apiKey, baseUrl, agentName };
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} urlString
|
|
127
|
+
* @param {string} apiKey
|
|
128
|
+
* @param {Record<string, unknown>} bodyObj
|
|
129
|
+
* @returns {Promise<void>}
|
|
130
|
+
*/
|
|
131
|
+
function postJson(baseUrl, apiKey, bodyObj) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
let u;
|
|
134
|
+
try {
|
|
135
|
+
const root = String(baseUrl).replace(/\/+$/, "");
|
|
136
|
+
u = new URL(`${root}/api/v1/actions`);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
reject(e);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const payload = JSON.stringify(bodyObj);
|
|
142
|
+
const isHttps = u.protocol === "https:";
|
|
143
|
+
const lib = isHttps ? https : http;
|
|
144
|
+
const port = u.port || (isHttps ? 443 : 80);
|
|
145
|
+
const options = {
|
|
146
|
+
hostname: u.hostname,
|
|
147
|
+
port,
|
|
148
|
+
path: u.pathname + u.search,
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"Content-Length": Buffer.byteLength(payload, "utf8"),
|
|
153
|
+
[AUTH_HEADER]: apiKey,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const req = lib.request(options, (res) => {
|
|
157
|
+
res.resume();
|
|
158
|
+
res.on("end", () => resolve());
|
|
159
|
+
});
|
|
160
|
+
req.on("error", reject);
|
|
161
|
+
req.write(payload);
|
|
162
|
+
req.end();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function main() {
|
|
167
|
+
let raw;
|
|
168
|
+
try {
|
|
169
|
+
raw = await readStdin();
|
|
170
|
+
} catch {
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const config = loadConfig();
|
|
175
|
+
if (config === null || config.apiKey.length === 0 || config.agentName.length === 0) {
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @type {Record<string, unknown>} */
|
|
180
|
+
let hookPayload;
|
|
181
|
+
try {
|
|
182
|
+
hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
|
|
183
|
+
} catch {
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const toolNameRaw =
|
|
188
|
+
(typeof hookPayload.tool_name === "string" && hookPayload.tool_name) ||
|
|
189
|
+
(typeof hookPayload.toolName === "string" && hookPayload.toolName) ||
|
|
190
|
+
"";
|
|
191
|
+
const toolInput =
|
|
192
|
+
hookPayload.tool_input !== undefined ? hookPayload.tool_input : hookPayload.toolInput;
|
|
193
|
+
const toolResult =
|
|
194
|
+
hookPayload.tool_result !== undefined
|
|
195
|
+
? hookPayload.tool_result
|
|
196
|
+
: hookPayload.toolResult !== undefined
|
|
197
|
+
? hookPayload.toolResult
|
|
198
|
+
: undefined;
|
|
199
|
+
|
|
200
|
+
let toolInputSerialized;
|
|
201
|
+
let toolResultSerialized;
|
|
202
|
+
try {
|
|
203
|
+
toolInputSerialized =
|
|
204
|
+
typeof toolInput === "string"
|
|
205
|
+
? toolInput
|
|
206
|
+
: JSON.stringify(toolInput === undefined ? null : toolInput);
|
|
207
|
+
toolResultSerialized =
|
|
208
|
+
typeof toolResult === "string"
|
|
209
|
+
? toolResult
|
|
210
|
+
: JSON.stringify(toolResult === undefined ? null : toolResult);
|
|
211
|
+
} catch {
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { service, actionType } = mapClaudeCodeToolToShield(toolNameRaw, toolInput);
|
|
216
|
+
|
|
217
|
+
/** @type {Record<string, unknown>} */
|
|
218
|
+
const metadata = {
|
|
219
|
+
tool_name: toolNameRaw,
|
|
220
|
+
tool_input: toolInputSerialized,
|
|
221
|
+
tool_result: toolResultSerialized,
|
|
222
|
+
source: "claude-code",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/** @type {Record<string, unknown>} */
|
|
226
|
+
const payload = {
|
|
227
|
+
agent: config.agentName,
|
|
228
|
+
service,
|
|
229
|
+
actionType,
|
|
230
|
+
status: "approved",
|
|
231
|
+
metadata,
|
|
232
|
+
platform: "claude-code",
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await postJson(config.baseUrl, config.apiKey, payload);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
239
|
+
process.stderr.write(
|
|
240
|
+
`[multicorn-shield] PostToolUse: Warning: failed to log action to Shield audit trail.\n Detail: ${msg}\n`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
main().catch((e) => {
|
|
248
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
249
|
+
process.stderr.write(
|
|
250
|
+
`[multicorn-shield] PostToolUse: Warning: failed to log action to Shield audit trail.\n Detail: ${msg}\n`,
|
|
251
|
+
);
|
|
252
|
+
process.exit(0);
|
|
253
|
+
});
|