multicorn-shield 0.8.0 → 0.10.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,54 @@
1
+ # Multicorn Shield for Windsurf (Cascade Hooks)
2
+
3
+ Native Shield integration for [Windsurf](https://windsurf.com) using [Cascade Hooks](https://docs.windsurf.com/windsurf/cascade/hooks). Every governed pre-hook asks the Shield API whether the action may run; post-hooks log completed actions to your audit trail.
4
+
5
+ ## Install
6
+
7
+ 1. Install the CLI package (or use `npx`).
8
+
9
+ ```bash
10
+ npm install -g multicorn-shield
11
+ ```
12
+
13
+ 2. Run the wizard and pick **Windsurf**, then **Native plugin (recommended)**.
14
+
15
+ ```bash
16
+ npx multicorn-proxy init
17
+ ```
18
+
19
+ 3. Restart Windsurf (quit fully, then reopen) so hooks load.
20
+
21
+ The wizard copies `pre-action.cjs` and `post-action.cjs` to `~/.multicorn/windsurf-hooks/` and merges entries into `~/.codeium/windsurf/hooks.json`.
22
+
23
+ ## How it works
24
+
25
+ - **Config** is read from `~/.multicorn/config.json` (same file as other Shield integrations). The agent row must use `platform: "windsurf"`.
26
+ - **Permission check**: `POST /api/v1/actions` with `status: "pending"` and `X-Multicorn-Key`. Exit code `0` allows the action; `2` blocks and prints guidance on stderr (see Windsurf hook docs). (Exit code `2` tells Windsurf to cancel the action and show the message to the user.)
27
+ - **Audit log**: post-hooks send `POST /api/v1/actions` with `status: "approved"` after the action completes.
28
+
29
+ ### Event to Shield mapping
30
+
31
+ | Windsurf `agent_action_name` | Shield `service` | Shield `actionType` |
32
+ | ----------------------------- | --------------------- | ------------------- |
33
+ | `pre_read_code` / `post_*` | `filesystem` | `read` |
34
+ | `pre_write_code` / `post_*` | `filesystem` | `write` |
35
+ | `pre_run_command` / `post_*` | `terminal` | `execute` |
36
+ | `pre_mcp_tool_use` / `post_*` | `mcp:<server>.<tool>` | `execute` |
37
+
38
+ Stdin includes `trajectory_id`, `execution_id`, and `tool_info`; those are forwarded in `metadata` for auditing.
39
+
40
+ ## Trust model
41
+
42
+ Hooks run shell commands with **your user permissions**. They can read the JSON on stdin and call the network. Review the scripts under `~/.multicorn/windsurf-hooks/` before you rely on them in sensitive environments.
43
+
44
+ ## Hosted proxy alternative
45
+
46
+ If you only need MCP traffic governed, use **Hosted proxy** in `npx multicorn-proxy init` and paste the proxy URL into `~/.codeium/windsurf/mcp_config.json` instead.
47
+
48
+ ## Windows
49
+
50
+ Hooks include a `powershell` field for Windsurf on Windows. Full Windows support may be incomplete compared to macOS and Linux; if something breaks, open an issue with your Windsurf and Node versions.
51
+
52
+ ## References
53
+
54
+ - [Cascade Hooks](https://docs.windsurf.com/windsurf/cascade/hooks)
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Windsurf Cascade post-hook: logs completed actions to the Shield audit trail.
3
+ * Routes by agent_action_name. Never blocks; always exit 0.
4
+ */
5
+
6
+ "use strict";
7
+
8
+ const fs = require("node:fs");
9
+ const http = require("node:http");
10
+ const https = require("node:https");
11
+ const os = require("node:os");
12
+ const path = require("node:path");
13
+
14
+ const AUTH_HEADER = "X-Multicorn-Key";
15
+ const LOG_PREFIX = "[multicorn-shield] Windsurf post-hook:";
16
+ const HTTP_REQUEST_TIMEOUT_MS =
17
+ process.env.MULTICORN_SHIELD_WINDSURF_PRE_HOOK_TEST_FAST_POLL === "1" ? 100 : 10000;
18
+
19
+ /** @type {Readonly<Record<string, { service: string; actionType: string }>>} */
20
+ const POST_EVENT_MAP = {
21
+ post_read_code: { service: "filesystem", actionType: "read" },
22
+ post_write_code: { service: "filesystem", actionType: "write" },
23
+ post_run_command: { service: "terminal", actionType: "execute" },
24
+ };
25
+
26
+ /**
27
+ * @returns {Promise<string>}
28
+ */
29
+ function readStdin() {
30
+ return new Promise((resolve, reject) => {
31
+ const chunks = [];
32
+ process.stdin.setEncoding("utf8");
33
+ process.stdin.on("data", (c) => chunks.push(c));
34
+ process.stdin.on("end", () => resolve(chunks.join("")));
35
+ process.stdin.on("error", reject);
36
+ });
37
+ }
38
+
39
+ // Duplicated in pre-action.cjs. CJS hooks cannot import shared TypeScript modules.
40
+ /**
41
+ * @param {Record<string, unknown>} obj
42
+ * @returns {string}
43
+ */
44
+ function resolveWindsurfAgentName(obj) {
45
+ const agents = obj.agents;
46
+ if (Array.isArray(agents)) {
47
+ for (const entry of agents) {
48
+ if (
49
+ entry &&
50
+ typeof entry === "object" &&
51
+ /** @type {{ platform?: string; name?: string }} */ (entry).platform === "windsurf" &&
52
+ typeof (/** @type {{ platform?: string; name?: string }} */ (entry).name) === "string"
53
+ ) {
54
+ return /** @type {{ name: string }} */ (entry).name;
55
+ }
56
+ }
57
+ }
58
+ return typeof obj.agentName === "string" ? obj.agentName : "";
59
+ }
60
+
61
+ /**
62
+ * @returns {{ apiKey: string; baseUrl: string; agentName: string } | null}
63
+ */
64
+ function loadConfig() {
65
+ try {
66
+ const configPath = path.join(os.homedir(), ".multicorn", "config.json");
67
+ const raw = fs.readFileSync(configPath, "utf8");
68
+ const obj = JSON.parse(raw);
69
+ const apiKey = typeof obj.apiKey === "string" ? obj.apiKey : "";
70
+ const baseUrl =
71
+ typeof obj.baseUrl === "string" && obj.baseUrl.length > 0
72
+ ? obj.baseUrl.replace(/\/+$/, "")
73
+ : "https://api.multicorn.ai";
74
+ const agentName = resolveWindsurfAgentName(obj);
75
+ return { apiKey, baseUrl, agentName };
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @param {unknown} toolInfo
83
+ * @returns {{ service: string; actionType: string }}
84
+ */
85
+ function mapMcpPost(toolInfo) {
86
+ if (toolInfo === null || typeof toolInfo !== "object") {
87
+ return { service: "mcp", actionType: "execute" };
88
+ }
89
+ const t = /** @type {Record<string, unknown>} */ (toolInfo);
90
+ const server = String(t.mcp_server_name ?? "unknown").trim() || "unknown";
91
+ const tool = String(t.mcp_tool_name ?? "unknown").trim() || "unknown";
92
+ const safeServer = server.replace(/[^a-zA-Z0-9._-]+/g, "_");
93
+ const safeTool = tool.replace(/[^a-zA-Z0-9._-]+/g, "_");
94
+ return { service: `mcp:${safeServer}.${safeTool}`, actionType: "execute" };
95
+ }
96
+
97
+ /**
98
+ * @param {string} agentActionName
99
+ * @param {unknown} toolInfo
100
+ * @returns {{ service: string; actionType: string } | null}
101
+ */
102
+ function mapPostEvent(agentActionName, toolInfo) {
103
+ const name = String(agentActionName || "").trim();
104
+ if (name === "post_mcp_tool_use") {
105
+ return mapMcpPost(toolInfo);
106
+ }
107
+ const mapped = POST_EVENT_MAP[name];
108
+ if (mapped !== undefined) {
109
+ return mapped;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ /**
115
+ * @param {string} baseUrl
116
+ * @param {string} apiKey
117
+ * @param {Record<string, unknown>} bodyObj
118
+ * @returns {Promise<void>}
119
+ */
120
+ function postJson(baseUrl, apiKey, bodyObj) {
121
+ return new Promise((resolve, reject) => {
122
+ let u;
123
+ try {
124
+ const root = String(baseUrl).replace(/\/+$/, "");
125
+ u = new URL(`${root}/api/v1/actions`);
126
+ } catch (e) {
127
+ reject(e);
128
+ return;
129
+ }
130
+ const payload = JSON.stringify(bodyObj);
131
+ const isHttps = u.protocol === "https:";
132
+ const lib = isHttps ? https : http;
133
+ const port = u.port || (isHttps ? 443 : 80);
134
+ const options = {
135
+ hostname: u.hostname,
136
+ port,
137
+ path: u.pathname + u.search,
138
+ method: "POST",
139
+ headers: {
140
+ Connection: "close",
141
+ "Content-Type": "application/json",
142
+ "Content-Length": Buffer.byteLength(payload, "utf8"),
143
+ [AUTH_HEADER]: apiKey,
144
+ },
145
+ };
146
+ const req = lib.request(options, (res) => {
147
+ res.resume();
148
+ res.on("end", () => {
149
+ const code = res.statusCode ?? 0;
150
+ if (code >= 200 && code < 300) {
151
+ resolve();
152
+ } else {
153
+ reject(new Error(`HTTP ${String(code)}`));
154
+ }
155
+ });
156
+ });
157
+ req.setTimeout(HTTP_REQUEST_TIMEOUT_MS, () => {
158
+ req.destroy(new Error("request timeout"));
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 agentActionName =
188
+ typeof hookPayload.agent_action_name === "string" ? hookPayload.agent_action_name : "";
189
+ const toolInfo = hookPayload.tool_info;
190
+
191
+ const mapped = mapPostEvent(agentActionName, toolInfo);
192
+ if (mapped === null) {
193
+ process.exit(0);
194
+ }
195
+ const { service, actionType } = mapped;
196
+
197
+ let toolInfoSerialized;
198
+ try {
199
+ toolInfoSerialized =
200
+ typeof toolInfo === "string"
201
+ ? toolInfo
202
+ : JSON.stringify(toolInfo === undefined ? null : toolInfo);
203
+ } catch {
204
+ process.exit(0);
205
+ }
206
+
207
+ /** @type {Record<string, unknown>} */
208
+ const metadata = {
209
+ agent_action_name: agentActionName,
210
+ trajectory_id: typeof hookPayload.trajectory_id === "string" ? hookPayload.trajectory_id : "",
211
+ execution_id: typeof hookPayload.execution_id === "string" ? hookPayload.execution_id : "",
212
+ model_name: typeof hookPayload.model_name === "string" ? hookPayload.model_name : "",
213
+ tool_info: toolInfoSerialized,
214
+ source: "windsurf",
215
+ };
216
+
217
+ /** @type {Record<string, unknown>} */
218
+ const payload = {
219
+ agent: config.agentName,
220
+ service,
221
+ actionType,
222
+ status: "approved",
223
+ metadata,
224
+ platform: "windsurf",
225
+ };
226
+
227
+ try {
228
+ await postJson(config.baseUrl, config.apiKey, payload);
229
+ } catch (e) {
230
+ const msg = e instanceof Error ? e.message : String(e);
231
+ process.stderr.write(
232
+ `${LOG_PREFIX} Warning: failed to log action to Shield audit trail. Check your network connection and that your API key in ~/.multicorn/config.json is valid.\n Detail: ${msg}\n`,
233
+ );
234
+ }
235
+
236
+ process.exit(0);
237
+ }
238
+
239
+ main().catch((e) => {
240
+ const msg = e instanceof Error ? e.message : String(e);
241
+ process.stderr.write(
242
+ `${LOG_PREFIX} Warning: failed to log action to Shield audit trail. Check your network connection and that your API key in ~/.multicorn/config.json is valid.\n Detail: ${msg}\n`,
243
+ );
244
+ process.exit(0);
245
+ });