multicorn-shield 0.1.16 → 0.2.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/dist/multicorn-proxy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { readFile, writeFile, mkdir, unlink } from 'fs/promises';
|
|
3
4
|
import { join } from 'path';
|
|
4
5
|
import { homedir } from 'os';
|
|
5
6
|
import { createInterface } from 'readline';
|
|
@@ -7,12 +8,44 @@ import { spawn } from 'child_process';
|
|
|
7
8
|
import { createHash } from 'crypto';
|
|
8
9
|
import 'stream';
|
|
9
10
|
|
|
11
|
+
var style = {
|
|
12
|
+
violet: (s) => `\x1B[38;2;124;58;237m${s}\x1B[0m`,
|
|
13
|
+
violetLight: (s) => `\x1B[38;2;167;139;250m${s}\x1B[0m`,
|
|
14
|
+
green: (s) => `\x1B[38;2;34;197;94m${s}\x1B[0m`,
|
|
15
|
+
yellow: (s) => `\x1B[38;2;245;158;11m${s}\x1B[0m`,
|
|
16
|
+
red: (s) => `\x1B[38;2;239;68;68m${s}\x1B[0m`,
|
|
17
|
+
cyan: (s) => `\x1B[38;2;6;182;212m${s}\x1B[0m`,
|
|
18
|
+
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
19
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`
|
|
20
|
+
};
|
|
21
|
+
var BANNER = [
|
|
22
|
+
" \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588 \u2588\u2588\u2584 ",
|
|
23
|
+
" \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
|
|
24
|
+
" \u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588",
|
|
25
|
+
" \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
|
|
26
|
+
" \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2580 "
|
|
27
|
+
].map((line) => style.violet(line)).join("\n");
|
|
28
|
+
function withSpinner(message) {
|
|
29
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
30
|
+
let i = 0;
|
|
31
|
+
const interval = setInterval(() => {
|
|
32
|
+
const frame = frames[i % frames.length];
|
|
33
|
+
process.stderr.write(`\r${style.violet(frame ?? "\u280B")} ${message}`);
|
|
34
|
+
i++;
|
|
35
|
+
}, 80);
|
|
36
|
+
return {
|
|
37
|
+
stop(success, result) {
|
|
38
|
+
clearInterval(interval);
|
|
39
|
+
const icon = success ? style.green("\u2713") : style.red("\u2717");
|
|
40
|
+
process.stderr.write(`\r\x1B[2K${icon} ${result}
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
10
45
|
var CONFIG_DIR = join(homedir(), ".multicorn");
|
|
11
46
|
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
12
47
|
var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
|
13
|
-
var
|
|
14
|
-
var OPENCLAW_PARSE_WARNING = "Multicorn Shield: Could not update ~/.openclaw/openclaw.json - please set MULTICORN_API_KEY manually.\n";
|
|
15
|
-
var OPENCLAW_UPDATED_MESSAGE = "OpenClaw config updated at ~/.openclaw/openclaw.json\n";
|
|
48
|
+
var OPENCLAW_MIN_VERSION = "2026.2.26";
|
|
16
49
|
async function loadConfig() {
|
|
17
50
|
try {
|
|
18
51
|
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
@@ -33,14 +66,13 @@ async function saveConfig(config) {
|
|
|
33
66
|
function isErrnoException(e) {
|
|
34
67
|
return typeof e === "object" && e !== null && "code" in e;
|
|
35
68
|
}
|
|
36
|
-
async function updateOpenClawConfigIfPresent(apiKey, baseUrl) {
|
|
69
|
+
async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
|
|
37
70
|
let raw;
|
|
38
71
|
try {
|
|
39
72
|
raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
40
73
|
} catch (e) {
|
|
41
74
|
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
42
|
-
|
|
43
|
-
return;
|
|
75
|
+
return "not-found";
|
|
44
76
|
}
|
|
45
77
|
throw e;
|
|
46
78
|
}
|
|
@@ -48,8 +80,7 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl) {
|
|
|
48
80
|
try {
|
|
49
81
|
obj = JSON.parse(raw);
|
|
50
82
|
} catch {
|
|
51
|
-
|
|
52
|
-
return;
|
|
83
|
+
return "parse-error";
|
|
53
84
|
}
|
|
54
85
|
let hooks = obj["hooks"];
|
|
55
86
|
if (hooks === void 0 || typeof hooks !== "object") {
|
|
@@ -78,10 +109,66 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl) {
|
|
|
78
109
|
}
|
|
79
110
|
env["MULTICORN_API_KEY"] = apiKey;
|
|
80
111
|
env["MULTICORN_BASE_URL"] = baseUrl;
|
|
112
|
+
if (agentName !== void 0) {
|
|
113
|
+
env["MULTICORN_AGENT_NAME"] = agentName;
|
|
114
|
+
const agentsList = obj["agents"];
|
|
115
|
+
const list = agentsList?.["list"];
|
|
116
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
117
|
+
const first = list[0];
|
|
118
|
+
if (first["id"] !== agentName) {
|
|
119
|
+
first["id"] = agentName;
|
|
120
|
+
first["name"] = agentName;
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
if (agentsList !== void 0 && typeof agentsList === "object") {
|
|
124
|
+
agentsList["list"] = [{ id: agentName, name: agentName }];
|
|
125
|
+
} else {
|
|
126
|
+
obj["agents"] = { list: [{ id: agentName, name: agentName }] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
81
130
|
await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
|
|
82
131
|
encoding: "utf8"
|
|
83
132
|
});
|
|
84
|
-
|
|
133
|
+
return "updated";
|
|
134
|
+
}
|
|
135
|
+
async function detectOpenClaw() {
|
|
136
|
+
let raw;
|
|
137
|
+
try {
|
|
138
|
+
raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
139
|
+
} catch (e) {
|
|
140
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
141
|
+
return { status: "not-found", version: null };
|
|
142
|
+
}
|
|
143
|
+
throw e;
|
|
144
|
+
}
|
|
145
|
+
let obj;
|
|
146
|
+
try {
|
|
147
|
+
obj = JSON.parse(raw);
|
|
148
|
+
} catch {
|
|
149
|
+
return { status: "parse-error", version: null };
|
|
150
|
+
}
|
|
151
|
+
const meta = obj["meta"];
|
|
152
|
+
if (typeof meta === "object" && meta !== null) {
|
|
153
|
+
const v = meta["lastTouchedVersion"];
|
|
154
|
+
if (typeof v === "string" && v.length > 0) {
|
|
155
|
+
return { status: "detected", version: v };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { status: "detected", version: null };
|
|
159
|
+
}
|
|
160
|
+
function isVersionAtLeast(version, minimum) {
|
|
161
|
+
const vParts = version.split(".").map(Number);
|
|
162
|
+
const mParts = minimum.split(".").map(Number);
|
|
163
|
+
const len = Math.max(vParts.length, mParts.length);
|
|
164
|
+
for (let i = 0; i < len; i++) {
|
|
165
|
+
const v = vParts[i] ?? 0;
|
|
166
|
+
const m = mParts[i] ?? 0;
|
|
167
|
+
if (Number.isNaN(v) || Number.isNaN(m)) return false;
|
|
168
|
+
if (v > m) return true;
|
|
169
|
+
if (v < m) return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
85
172
|
}
|
|
86
173
|
async function validateApiKey(apiKey, baseUrl) {
|
|
87
174
|
try {
|
|
@@ -107,7 +194,116 @@ async function validateApiKey(apiKey, baseUrl) {
|
|
|
107
194
|
};
|
|
108
195
|
}
|
|
109
196
|
}
|
|
197
|
+
function normalizeAgentName(raw) {
|
|
198
|
+
return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
199
|
+
}
|
|
200
|
+
async function isOpenClawConnected() {
|
|
201
|
+
try {
|
|
202
|
+
const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
203
|
+
const obj = JSON.parse(raw);
|
|
204
|
+
const hooks = obj["hooks"];
|
|
205
|
+
const internal = hooks?.["internal"];
|
|
206
|
+
const entries = internal?.["entries"];
|
|
207
|
+
const shield = entries?.["multicorn-shield"];
|
|
208
|
+
const env = shield?.["env"];
|
|
209
|
+
const key = env?.["MULTICORN_API_KEY"];
|
|
210
|
+
return typeof key === "string" && key.length > 0;
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function isClaudeCodeConnected() {
|
|
216
|
+
try {
|
|
217
|
+
return existsSync(join(homedir(), ".claude", "plugins", "cache", "multicorn-shield"));
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function getClaudeDesktopConfigPath() {
|
|
223
|
+
switch (process.platform) {
|
|
224
|
+
case "win32":
|
|
225
|
+
return join(
|
|
226
|
+
process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"),
|
|
227
|
+
"Claude",
|
|
228
|
+
"claude_desktop_config.json"
|
|
229
|
+
);
|
|
230
|
+
case "linux":
|
|
231
|
+
return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
232
|
+
default:
|
|
233
|
+
return join(
|
|
234
|
+
homedir(),
|
|
235
|
+
"Library",
|
|
236
|
+
"Application Support",
|
|
237
|
+
"Claude",
|
|
238
|
+
"claude_desktop_config.json"
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function updateClaudeDesktopConfig(agentName, mcpServerCommand, overwrite = false) {
|
|
243
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(agentName)) {
|
|
244
|
+
throw new Error("Agent name must contain only letters, numbers, hyphens, and underscores");
|
|
245
|
+
}
|
|
246
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
247
|
+
let obj = {};
|
|
248
|
+
let fileExists = false;
|
|
249
|
+
try {
|
|
250
|
+
const raw = await readFile(configPath, "utf8");
|
|
251
|
+
fileExists = true;
|
|
252
|
+
try {
|
|
253
|
+
obj = JSON.parse(raw);
|
|
254
|
+
} catch {
|
|
255
|
+
return "parse-error";
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
259
|
+
fileExists = false;
|
|
260
|
+
} else {
|
|
261
|
+
throw e;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
let mcpServers = obj["mcpServers"];
|
|
265
|
+
if (mcpServers === void 0 || typeof mcpServers !== "object") {
|
|
266
|
+
mcpServers = {};
|
|
267
|
+
obj["mcpServers"] = mcpServers;
|
|
268
|
+
}
|
|
269
|
+
if (mcpServers[agentName] !== void 0 && !overwrite) {
|
|
270
|
+
return "skipped";
|
|
271
|
+
}
|
|
272
|
+
const commandParts = mcpServerCommand.trim().split(/\s+/);
|
|
273
|
+
mcpServers[agentName] = {
|
|
274
|
+
command: "npx",
|
|
275
|
+
args: ["multicorn-proxy", "--wrap", ...commandParts, "--agent-name", agentName]
|
|
276
|
+
};
|
|
277
|
+
const configDir = join(configPath, "..");
|
|
278
|
+
if (!fileExists) {
|
|
279
|
+
await mkdir(configDir, { recursive: true });
|
|
280
|
+
}
|
|
281
|
+
await writeFile(configPath, JSON.stringify(obj, null, 2) + "\n", { encoding: "utf8" });
|
|
282
|
+
return fileExists ? "updated" : "created";
|
|
283
|
+
}
|
|
284
|
+
async function isClaudeDesktopConnected() {
|
|
285
|
+
try {
|
|
286
|
+
const raw = await readFile(getClaudeDesktopConfigPath(), "utf8");
|
|
287
|
+
const obj = JSON.parse(raw);
|
|
288
|
+
const mcpServers = obj["mcpServers"];
|
|
289
|
+
if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
|
|
290
|
+
for (const entry of Object.values(mcpServers)) {
|
|
291
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
292
|
+
const args = entry["args"];
|
|
293
|
+
if (Array.isArray(args) && args.includes("multicorn-proxy")) return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
110
300
|
async function runInit(baseUrl = "https://api.multicorn.ai") {
|
|
301
|
+
if (!process.stdin.isTTY) {
|
|
302
|
+
process.stderr.write(
|
|
303
|
+
style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
|
|
304
|
+
);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
111
307
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
112
308
|
function ask(question) {
|
|
113
309
|
return new Promise((resolve) => {
|
|
@@ -116,39 +312,332 @@ async function runInit(baseUrl = "https://api.multicorn.ai") {
|
|
|
116
312
|
});
|
|
117
313
|
});
|
|
118
314
|
}
|
|
119
|
-
process.stderr.write("
|
|
120
|
-
process.stderr.write("
|
|
121
|
-
|
|
122
|
-
|
|
315
|
+
process.stderr.write("\n" + BANNER + "\n");
|
|
316
|
+
process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
|
|
317
|
+
process.stderr.write(style.bold(style.violet("Multicorn Shield proxy setup")) + "\n\n");
|
|
318
|
+
process.stderr.write(
|
|
319
|
+
style.dim("Get your API key at https://app.multicorn.ai/settings/api-keys") + "\n\n"
|
|
320
|
+
);
|
|
321
|
+
let apiKey = "";
|
|
322
|
+
const existing = await loadConfig().catch(() => null);
|
|
323
|
+
if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
|
|
324
|
+
const masked = "mcs_..." + existing.apiKey.slice(-4);
|
|
325
|
+
process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
|
|
326
|
+
const answer = await ask("Use this key? (Y/n) ");
|
|
327
|
+
if (answer.trim().toLowerCase() !== "n") {
|
|
328
|
+
apiKey = existing.apiKey;
|
|
329
|
+
if (baseUrl === "https://api.multicorn.ai") {
|
|
330
|
+
baseUrl = existing.baseUrl;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
while (apiKey.length === 0) {
|
|
123
335
|
const input = await ask("API key (starts with mcs_): ");
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
process.stderr.write("API key is required
|
|
336
|
+
const key = input.trim();
|
|
337
|
+
if (key.length === 0) {
|
|
338
|
+
process.stderr.write(style.red("API key is required.") + "\n");
|
|
127
339
|
continue;
|
|
128
340
|
}
|
|
129
|
-
|
|
130
|
-
|
|
341
|
+
const spinner = withSpinner("Validating key...");
|
|
342
|
+
let result;
|
|
343
|
+
try {
|
|
344
|
+
result = await validateApiKey(key, baseUrl);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
spinner.stop(false, "Validation failed");
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
131
349
|
if (!result.valid) {
|
|
132
|
-
|
|
133
|
-
`);
|
|
350
|
+
spinner.stop(false, result.error ?? "Validation failed. Try again.");
|
|
134
351
|
continue;
|
|
135
352
|
}
|
|
136
|
-
|
|
353
|
+
spinner.stop(true, "Key validated");
|
|
354
|
+
apiKey = key;
|
|
137
355
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
} catch {
|
|
356
|
+
const configuredPlatforms = /* @__PURE__ */ new Set();
|
|
357
|
+
let lastConfig = { apiKey, baseUrl };
|
|
358
|
+
let configuring = true;
|
|
359
|
+
while (configuring) {
|
|
143
360
|
process.stderr.write(
|
|
144
|
-
"
|
|
361
|
+
"\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n"
|
|
145
362
|
);
|
|
363
|
+
const platformLabels = ["OpenClaw", "Claude Code", "Claude Desktop", "Other MCP Agent"];
|
|
364
|
+
const openClawConnected = await isOpenClawConnected();
|
|
365
|
+
const claudeCodeConnected = isClaudeCodeConnected();
|
|
366
|
+
const claudeDesktopConnected = await isClaudeDesktopConnected();
|
|
367
|
+
for (let i = 0; i < platformLabels.length; i++) {
|
|
368
|
+
const sessionMarker = configuredPlatforms.has(i + 1) ? " " + style.green("\u2713") : "";
|
|
369
|
+
let connectedMarker = "";
|
|
370
|
+
if (!configuredPlatforms.has(i + 1)) {
|
|
371
|
+
if (i === 0 && openClawConnected) {
|
|
372
|
+
connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
|
|
373
|
+
} else if (i === 1 && claudeCodeConnected) {
|
|
374
|
+
connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
|
|
375
|
+
} else if (i === 2 && claudeDesktopConnected) {
|
|
376
|
+
connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
process.stderr.write(
|
|
380
|
+
` ${style.violet(String(i + 1))}. ${platformLabels[i] ?? ""}${sessionMarker}${connectedMarker}
|
|
381
|
+
`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
let selection = 0;
|
|
385
|
+
while (selection === 0) {
|
|
386
|
+
const input = await ask("Select (1-4): ");
|
|
387
|
+
const num = parseInt(input.trim(), 10);
|
|
388
|
+
if (num >= 1 && num <= 4) {
|
|
389
|
+
selection = num;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
let agentName = "";
|
|
393
|
+
while (agentName.length === 0) {
|
|
394
|
+
const input = await ask("\nWhat would you like to call this agent? ");
|
|
395
|
+
if (input.trim().length === 0) continue;
|
|
396
|
+
const transformed = normalizeAgentName(input);
|
|
397
|
+
if (transformed.length === 0) {
|
|
398
|
+
process.stderr.write(
|
|
399
|
+
style.red("Agent name must contain letters or numbers. Please try again.") + "\n"
|
|
400
|
+
);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (transformed !== input.trim()) {
|
|
404
|
+
process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
|
|
405
|
+
}
|
|
406
|
+
agentName = transformed;
|
|
407
|
+
}
|
|
408
|
+
if (selection === 1) {
|
|
409
|
+
let detection;
|
|
410
|
+
try {
|
|
411
|
+
detection = await detectOpenClaw();
|
|
412
|
+
} catch (error) {
|
|
413
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
414
|
+
process.stderr.write(style.red("\u2717") + ` Failed to read OpenClaw config: ${detail}
|
|
415
|
+
`);
|
|
416
|
+
rl.close();
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
if (detection.status === "not-found") {
|
|
420
|
+
process.stderr.write(
|
|
421
|
+
style.red("\u2717") + " OpenClaw is not installed. Install OpenClaw first, then run npx multicorn-proxy init again.\n"
|
|
422
|
+
);
|
|
423
|
+
rl.close();
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
if (detection.status === "parse-error") {
|
|
427
|
+
process.stderr.write(
|
|
428
|
+
style.red("\u2717") + " Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually.\n"
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
if (detection.status === "detected") {
|
|
432
|
+
if (detection.version !== null) {
|
|
433
|
+
process.stderr.write(
|
|
434
|
+
style.green("\u2713") + ` OpenClaw detected ${style.dim(`(${detection.version})`)}
|
|
435
|
+
`
|
|
436
|
+
);
|
|
437
|
+
if (isVersionAtLeast(detection.version, OPENCLAW_MIN_VERSION)) {
|
|
438
|
+
process.stderr.write(
|
|
439
|
+
style.green("\u2713") + " " + style.green("Version compatible") + "\n"
|
|
440
|
+
);
|
|
441
|
+
} else {
|
|
442
|
+
process.stderr.write(
|
|
443
|
+
style.yellow("\u26A0") + ` Shield has been tested with OpenClaw ${style.cyan(OPENCLAW_MIN_VERSION)} and above. Your version (${detection.version}) may work but is untested. We recommend upgrading to at least ${style.cyan(OPENCLAW_MIN_VERSION)}.
|
|
444
|
+
`
|
|
445
|
+
);
|
|
446
|
+
const answer = await ask("Continue anyway? (y/N) ");
|
|
447
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
448
|
+
rl.close();
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
process.stderr.write(
|
|
454
|
+
style.yellow("\u26A0") + " Could not detect OpenClaw version. Continuing anyway.\n"
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const spinner = withSpinner("Updating OpenClaw config...");
|
|
458
|
+
try {
|
|
459
|
+
const result = await updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName);
|
|
460
|
+
if (result === "not-found") {
|
|
461
|
+
spinner.stop(false, "OpenClaw config disappeared unexpectedly.");
|
|
462
|
+
rl.close();
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
if (result === "parse-error") {
|
|
466
|
+
spinner.stop(
|
|
467
|
+
false,
|
|
468
|
+
"Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually."
|
|
469
|
+
);
|
|
470
|
+
} else {
|
|
471
|
+
spinner.stop(
|
|
472
|
+
true,
|
|
473
|
+
"OpenClaw config updated at " + style.cyan("~/.openclaw/openclaw.json")
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
478
|
+
spinner.stop(false, `Failed to update OpenClaw config: ${detail}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} else if (selection === 2) {
|
|
482
|
+
process.stderr.write("\nTo connect Claude Code to Shield:\n\n");
|
|
483
|
+
process.stderr.write(
|
|
484
|
+
" " + style.bold("Step 1") + " - Add the Multicorn marketplace:\n " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n\n"
|
|
485
|
+
);
|
|
486
|
+
process.stderr.write(
|
|
487
|
+
" " + style.bold("Step 2") + " - Install the plugin:\n " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n\n"
|
|
488
|
+
);
|
|
489
|
+
process.stderr.write(
|
|
490
|
+
" " + style.bold("Step 3") + " - Start Claude Code:\n " + style.cyan("claude") + "\n\n"
|
|
491
|
+
);
|
|
492
|
+
process.stderr.write(
|
|
493
|
+
style.dim("Run /plugin inside Claude Code to confirm multicorn-shield is installed.") + "\n"
|
|
494
|
+
);
|
|
495
|
+
process.stderr.write(
|
|
496
|
+
style.dim("Requires Claude Code to be installed. Get it at https://code.claude.com") + "\n"
|
|
497
|
+
);
|
|
498
|
+
} else if (selection === 3) {
|
|
499
|
+
const mcpCommand = await ask(
|
|
500
|
+
"\nWhat MCP server should Shield govern for this agent?\nThis is the command you'd normally use to start your MCP server.\nExample: npx -y @modelcontextprotocol/server-filesystem /tmp\nLeave blank to skip and configure later: "
|
|
501
|
+
);
|
|
502
|
+
if (mcpCommand.trim().length === 0) {
|
|
503
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
504
|
+
process.stderr.write("\n" + style.dim("Add this to your Claude Desktop config at:") + "\n");
|
|
505
|
+
process.stderr.write(" " + style.cyan(configPath) + "\n\n");
|
|
506
|
+
const snippet = JSON.stringify(
|
|
507
|
+
{
|
|
508
|
+
mcpServers: {
|
|
509
|
+
[agentName]: {
|
|
510
|
+
command: "npx",
|
|
511
|
+
args: [
|
|
512
|
+
"multicorn-proxy",
|
|
513
|
+
"--wrap",
|
|
514
|
+
"<your-mcp-server-command>",
|
|
515
|
+
"--agent-name",
|
|
516
|
+
agentName
|
|
517
|
+
]
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
null,
|
|
522
|
+
2
|
|
523
|
+
);
|
|
524
|
+
process.stderr.write(style.cyan(snippet) + "\n\n");
|
|
525
|
+
} else {
|
|
526
|
+
let shouldWrite = true;
|
|
527
|
+
const spinner = withSpinner("Updating Claude Desktop config...");
|
|
528
|
+
try {
|
|
529
|
+
let result = await updateClaudeDesktopConfig(agentName, mcpCommand.trim());
|
|
530
|
+
if (result === "skipped") {
|
|
531
|
+
spinner.stop(false, `Agent "${agentName}" already exists in Claude Desktop config.`);
|
|
532
|
+
const overwrite = await ask("Overwrite the existing entry? (y/N) ");
|
|
533
|
+
if (overwrite.trim().toLowerCase() === "y") {
|
|
534
|
+
const retrySpinner = withSpinner("Updating Claude Desktop config...");
|
|
535
|
+
result = await updateClaudeDesktopConfig(agentName, mcpCommand.trim(), true);
|
|
536
|
+
retrySpinner.stop(
|
|
537
|
+
true,
|
|
538
|
+
"Claude Desktop config updated at " + style.cyan(getClaudeDesktopConfigPath())
|
|
539
|
+
);
|
|
540
|
+
} else {
|
|
541
|
+
shouldWrite = false;
|
|
542
|
+
process.stderr.write(style.dim("Skipped. Existing config left unchanged.") + "\n");
|
|
543
|
+
}
|
|
544
|
+
} else if (result === "parse-error") {
|
|
545
|
+
spinner.stop(false, "Claude Desktop config file contains invalid JSON.");
|
|
546
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
547
|
+
process.stderr.write(
|
|
548
|
+
style.yellow("\u26A0") + " Fix the JSON in " + style.cyan(configPath) + " or add this entry manually:\n\n"
|
|
549
|
+
);
|
|
550
|
+
const snippet = JSON.stringify(
|
|
551
|
+
{
|
|
552
|
+
mcpServers: {
|
|
553
|
+
[agentName]: {
|
|
554
|
+
command: "npx",
|
|
555
|
+
args: [
|
|
556
|
+
"multicorn-proxy",
|
|
557
|
+
"--wrap",
|
|
558
|
+
...mcpCommand.trim().split(/\s+/),
|
|
559
|
+
"--agent-name",
|
|
560
|
+
agentName
|
|
561
|
+
]
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
null,
|
|
566
|
+
2
|
|
567
|
+
);
|
|
568
|
+
process.stderr.write(style.cyan(snippet) + "\n\n");
|
|
569
|
+
} else {
|
|
570
|
+
const verb = result === "created" ? "Created" : "Updated";
|
|
571
|
+
spinner.stop(
|
|
572
|
+
true,
|
|
573
|
+
verb + " Claude Desktop config at " + style.cyan(getClaudeDesktopConfigPath())
|
|
574
|
+
);
|
|
575
|
+
process.stderr.write(style.dim("Restart Claude Desktop to pick up changes.") + "\n");
|
|
576
|
+
}
|
|
577
|
+
} catch (error) {
|
|
578
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
579
|
+
spinner.stop(false, `Failed to update Claude Desktop config: ${detail}`);
|
|
580
|
+
shouldWrite = false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
process.stderr.write("\n" + style.dim("Start the Shield proxy with:") + "\n");
|
|
585
|
+
process.stderr.write(
|
|
586
|
+
" " + style.cyan(
|
|
587
|
+
`npx multicorn-proxy --wrap <your-mcp-server-command> --agent-name ${agentName}`
|
|
588
|
+
) + "\n\n"
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
configuredPlatforms.add(selection);
|
|
592
|
+
lastConfig = { apiKey, baseUrl, agentName };
|
|
593
|
+
try {
|
|
594
|
+
await saveConfig(lastConfig);
|
|
595
|
+
process.stderr.write(style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
|
|
596
|
+
`);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
599
|
+
process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
|
|
600
|
+
}
|
|
601
|
+
if (configuredPlatforms.size >= 4) {
|
|
602
|
+
configuring = false;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const another = await ask("\nWould you like to configure another agent? (y/N) ");
|
|
606
|
+
if (another.trim().toLowerCase() !== "y") {
|
|
607
|
+
configuring = false;
|
|
608
|
+
}
|
|
146
609
|
}
|
|
147
|
-
|
|
148
|
-
|
|
610
|
+
rl.close();
|
|
611
|
+
process.stderr.write("\n" + style.bold(style.violet("Setup complete")) + "\n\n");
|
|
612
|
+
const allPlatforms = ["OpenClaw", "Claude Code", "Claude Desktop", "Other MCP Agent"];
|
|
613
|
+
for (const idx of configuredPlatforms) {
|
|
614
|
+
process.stderr.write(` ${style.green("\u2713")} ${allPlatforms[idx - 1] ?? ""}
|
|
149
615
|
`);
|
|
150
|
-
|
|
151
|
-
|
|
616
|
+
}
|
|
617
|
+
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
618
|
+
const blocks = [];
|
|
619
|
+
if (configuredPlatforms.has(1)) {
|
|
620
|
+
blocks.push(
|
|
621
|
+
"\n" + style.bold("To complete your OpenClaw setup:") + "\n \u2192 Restart your gateway: " + style.cyan("openclaw gateway restart") + "\n \u2192 Start a session: " + style.cyan("openclaw tui") + "\n"
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (configuredPlatforms.has(2)) {
|
|
625
|
+
blocks.push(
|
|
626
|
+
"\n" + style.bold("To complete your Claude Code setup:") + "\n \u2192 Add marketplace: " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n \u2192 Install plugin: " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n"
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (configuredPlatforms.has(3)) {
|
|
630
|
+
blocks.push(
|
|
631
|
+
"\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (configuredPlatforms.has(4)) {
|
|
635
|
+
blocks.push(
|
|
636
|
+
"\n" + style.bold("To complete your Other MCP Agent setup:") + "\n \u2192 Start your agent with: " + style.cyan("npx multicorn-proxy --wrap <your-server> --agent-name <name>") + "\n"
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
process.stderr.write(blocks.join("") + "\n");
|
|
640
|
+
return lastConfig;
|
|
152
641
|
}
|
|
153
642
|
function isProxyConfig(value) {
|
|
154
643
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1324,7 +1813,7 @@ Use https:// or http://localhost for local development.
|
|
|
1324
1813
|
);
|
|
1325
1814
|
process.exit(1);
|
|
1326
1815
|
}
|
|
1327
|
-
const agentName = cli.agentName.length > 0 ? cli.agentName : deriveAgentName(cli.wrapCommand);
|
|
1816
|
+
const agentName = cli.agentName.length > 0 ? cli.agentName : config.agentName !== void 0 && config.agentName.length > 0 ? config.agentName : deriveAgentName(cli.wrapCommand);
|
|
1328
1817
|
const finalBaseUrl = cli.baseUrl !== "https://api.multicorn.ai" ? cli.baseUrl : config.baseUrl;
|
|
1329
1818
|
const finalDashboardUrl = cli.dashboardUrl !== "" ? cli.dashboardUrl : deriveDashboardUrl(finalBaseUrl);
|
|
1330
1819
|
const proxy = createProxyServer({
|
|
@@ -329,13 +329,6 @@ var POLL_INTERVAL_MS2 = 3e3;
|
|
|
329
329
|
var POLL_TIMEOUT_MS2 = 5 * 60 * 1e3;
|
|
330
330
|
function deriveDashboardUrl(baseUrl) {
|
|
331
331
|
try {
|
|
332
|
-
const envBase = process.env["MULTICORN_BASE_URL"];
|
|
333
|
-
if (typeof envBase === "string" && envBase.trim().length > 0) {
|
|
334
|
-
const trimmed = envBase.trim();
|
|
335
|
-
if (trimmed.includes("localhost") || trimmed.includes("127.0.0.1")) {
|
|
336
|
-
baseUrl = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
332
|
if (!/^https?:\/\//i.test(baseUrl) && (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1"))) {
|
|
340
333
|
baseUrl = `http://${baseUrl}`;
|
|
341
334
|
}
|
|
@@ -391,13 +391,6 @@ var POLL_INTERVAL_MS2 = 3e3;
|
|
|
391
391
|
var POLL_TIMEOUT_MS2 = 5 * 60 * 1e3;
|
|
392
392
|
function deriveDashboardUrl(baseUrl) {
|
|
393
393
|
try {
|
|
394
|
-
const envBase = process.env["MULTICORN_BASE_URL"];
|
|
395
|
-
if (typeof envBase === "string" && envBase.trim().length > 0) {
|
|
396
|
-
const trimmed = envBase.trim();
|
|
397
|
-
if (trimmed.includes("localhost") || trimmed.includes("127.0.0.1")) {
|
|
398
|
-
baseUrl = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
394
|
if (!/^https?:\/\//i.test(baseUrl) && (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1"))) {
|
|
402
395
|
baseUrl = `http://${baseUrl}`;
|
|
403
396
|
}
|
|
@@ -503,7 +496,7 @@ function readConfig() {
|
|
|
503
496
|
const pc = pluginConfig ?? {};
|
|
504
497
|
const resolvedApiKey = asString(cachedMulticornConfig?.apiKey) ?? asString(process.env["MULTICORN_API_KEY"]) ?? "";
|
|
505
498
|
const resolvedBaseUrl = asString(cachedMulticornConfig?.baseUrl) ?? asString(process.env["MULTICORN_BASE_URL"]) ?? "https://api.multicorn.ai";
|
|
506
|
-
const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? null;
|
|
499
|
+
const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? asString(cachedMulticornConfig?.agentName) ?? null;
|
|
507
500
|
const failMode = "closed";
|
|
508
501
|
return { apiKey: resolvedApiKey, baseUrl: resolvedBaseUrl, agentName, failMode };
|
|
509
502
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multicorn-shield",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,21 +31,6 @@
|
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": ">=20"
|
|
33
33
|
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "tsup",
|
|
36
|
-
"dev": "tsup --watch",
|
|
37
|
-
"lint": "eslint . && prettier --check .",
|
|
38
|
-
"lint:fix": "eslint --fix . && prettier --write .",
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"test:watch": "vitest",
|
|
41
|
-
"test:coverage": "vitest run --coverage",
|
|
42
|
-
"typecheck": "tsc --noEmit",
|
|
43
|
-
"docs": "typedoc",
|
|
44
|
-
"clean": "rm -rf dist coverage docs/api",
|
|
45
|
-
"size": "size-limit",
|
|
46
|
-
"prepublishOnly": "pnpm run clean && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
|
|
47
|
-
"prepare": "husky"
|
|
48
|
-
},
|
|
49
34
|
"lint-staged": {
|
|
50
35
|
"*.ts": [
|
|
51
36
|
"eslint --fix",
|
|
@@ -113,10 +98,20 @@
|
|
|
113
98
|
"url": "https://github.com/Multicorn-AI/multicorn-shield/issues"
|
|
114
99
|
},
|
|
115
100
|
"homepage": "https://multicorn.ai",
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
"scripts": {
|
|
102
|
+
"build": "tsup",
|
|
103
|
+
"dev": "tsup --watch",
|
|
104
|
+
"lint": "eslint . && prettier --check .",
|
|
105
|
+
"lint:fix": "eslint --fix . && prettier --write .",
|
|
106
|
+
"test": "vitest run",
|
|
107
|
+
"test:watch": "vitest",
|
|
108
|
+
"test:coverage": "vitest run --coverage",
|
|
109
|
+
"typecheck": "tsc --noEmit",
|
|
110
|
+
"docs": "typedoc",
|
|
111
|
+
"clean": "rm -rf dist coverage docs/api",
|
|
112
|
+
"size": "size-limit",
|
|
113
|
+
"release:patch": "npm version patch && pnpm publish",
|
|
114
|
+
"release:minor": "npm version minor && pnpm publish",
|
|
115
|
+
"release:major": "npm version major && pnpm publish"
|
|
121
116
|
}
|
|
122
|
-
}
|
|
117
|
+
}
|