multicorn-shield 0.2.2 → 0.6.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/README.md +3 -1
- package/dist/multicorn-proxy.js +617 -311
- package/dist/openclaw-plugin/multicorn-shield.js +12 -1
- package/dist/shield-extension.js +792 -32
- package/package.json +22 -33
package/dist/multicorn-proxy.js
CHANGED
|
@@ -45,17 +45,106 @@ function withSpinner(message) {
|
|
|
45
45
|
var CONFIG_DIR = join(homedir(), ".multicorn");
|
|
46
46
|
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
47
47
|
var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
|
48
|
-
var
|
|
48
|
+
var ANSI_PATTERN = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
|
|
49
|
+
function stripAnsi(str) {
|
|
50
|
+
return str.replace(ANSI_PATTERN, "");
|
|
51
|
+
}
|
|
52
|
+
function normalizeAgentName(raw) {
|
|
53
|
+
return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
54
|
+
}
|
|
55
|
+
function isErrnoException(e) {
|
|
56
|
+
return typeof e === "object" && e !== null && "code" in e;
|
|
57
|
+
}
|
|
58
|
+
function isProxyConfig(value) {
|
|
59
|
+
if (typeof value !== "object" || value === null) return false;
|
|
60
|
+
const obj = value;
|
|
61
|
+
return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
|
|
62
|
+
}
|
|
63
|
+
function isAgentEntry(value) {
|
|
64
|
+
if (typeof value !== "object" || value === null) return false;
|
|
65
|
+
const o = value;
|
|
66
|
+
return typeof o["name"] === "string" && typeof o["platform"] === "string";
|
|
67
|
+
}
|
|
68
|
+
function getAgentByPlatform(config, platform) {
|
|
69
|
+
const list = config.agents;
|
|
70
|
+
if (list === void 0 || list.length === 0) return void 0;
|
|
71
|
+
return list.find((a) => a.platform === platform);
|
|
72
|
+
}
|
|
73
|
+
function getDefaultAgent(config) {
|
|
74
|
+
const list = config.agents;
|
|
75
|
+
if (list === void 0 || list.length === 0) return void 0;
|
|
76
|
+
const defName = config.defaultAgent;
|
|
77
|
+
if (typeof defName === "string" && defName.length > 0) {
|
|
78
|
+
const match = list.find((a) => a.name === defName);
|
|
79
|
+
if (match !== void 0) return match;
|
|
80
|
+
}
|
|
81
|
+
return list[0];
|
|
82
|
+
}
|
|
83
|
+
function collectAgentsFromConfig(cfg) {
|
|
84
|
+
if (cfg === null) return [];
|
|
85
|
+
if (cfg.agents !== void 0 && cfg.agents.length > 0) {
|
|
86
|
+
return cfg.agents.map((a) => ({ name: a.name, platform: a.platform }));
|
|
87
|
+
}
|
|
88
|
+
const raw = cfg;
|
|
89
|
+
const legacyName = raw["agentName"];
|
|
90
|
+
const legacyPlatform = raw["platform"];
|
|
91
|
+
if (typeof legacyName === "string" && legacyName.length > 0) {
|
|
92
|
+
const plat = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "unknown";
|
|
93
|
+
return [{ name: legacyName, platform: plat }];
|
|
94
|
+
}
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
49
97
|
async function loadConfig() {
|
|
50
98
|
try {
|
|
51
99
|
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
52
100
|
const parsed = JSON.parse(raw);
|
|
53
101
|
if (!isProxyConfig(parsed)) return null;
|
|
54
|
-
|
|
102
|
+
const obj = parsed;
|
|
103
|
+
const agentNameRaw = obj["agentName"];
|
|
104
|
+
const agentsRaw = obj["agents"];
|
|
105
|
+
const hasNonEmptyAgents = Array.isArray(agentsRaw) && agentsRaw.length > 0 && agentsRaw.every((e) => isAgentEntry(e));
|
|
106
|
+
const needsMigrate = typeof agentNameRaw === "string" && agentNameRaw.length > 0 && !hasNonEmptyAgents;
|
|
107
|
+
if (!needsMigrate) {
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
const platform = typeof obj["platform"] === "string" && obj["platform"].length > 0 ? obj["platform"] : "unknown";
|
|
111
|
+
const next = { ...obj };
|
|
112
|
+
delete next["agentName"];
|
|
113
|
+
delete next["platform"];
|
|
114
|
+
next["agents"] = [{ name: agentNameRaw, platform }];
|
|
115
|
+
next["defaultAgent"] = agentNameRaw;
|
|
116
|
+
const migrated = next;
|
|
117
|
+
await saveConfig(migrated);
|
|
118
|
+
return migrated;
|
|
55
119
|
} catch {
|
|
56
120
|
return null;
|
|
57
121
|
}
|
|
58
122
|
}
|
|
123
|
+
async function deleteAgentByName(name) {
|
|
124
|
+
const config = await loadConfig();
|
|
125
|
+
if (config === null) return false;
|
|
126
|
+
const agents = collectAgentsFromConfig(config);
|
|
127
|
+
const idx = agents.findIndex((a) => a.name === name);
|
|
128
|
+
if (idx === -1) return false;
|
|
129
|
+
const nextAgents = agents.filter((_, i) => i !== idx);
|
|
130
|
+
let defaultAgent = config.defaultAgent;
|
|
131
|
+
if (defaultAgent === name) {
|
|
132
|
+
defaultAgent = void 0;
|
|
133
|
+
}
|
|
134
|
+
const raw = { ...config };
|
|
135
|
+
if (nextAgents.length > 0) {
|
|
136
|
+
raw["agents"] = nextAgents;
|
|
137
|
+
} else {
|
|
138
|
+
delete raw["agents"];
|
|
139
|
+
}
|
|
140
|
+
if (defaultAgent !== void 0 && defaultAgent.length > 0) {
|
|
141
|
+
raw["defaultAgent"] = defaultAgent;
|
|
142
|
+
} else {
|
|
143
|
+
delete raw["defaultAgent"];
|
|
144
|
+
}
|
|
145
|
+
await saveConfig(raw);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
59
148
|
async function saveConfig(config) {
|
|
60
149
|
await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
61
150
|
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
|
|
@@ -63,8 +152,44 @@ async function saveConfig(config) {
|
|
|
63
152
|
mode: 384
|
|
64
153
|
});
|
|
65
154
|
}
|
|
66
|
-
|
|
67
|
-
|
|
155
|
+
var OPENCLAW_MIN_VERSION = "2026.2.26";
|
|
156
|
+
async function detectOpenClaw() {
|
|
157
|
+
let raw;
|
|
158
|
+
try {
|
|
159
|
+
raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
160
|
+
} catch (e) {
|
|
161
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
162
|
+
return { status: "not-found", version: null };
|
|
163
|
+
}
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
let obj;
|
|
167
|
+
try {
|
|
168
|
+
obj = JSON.parse(raw);
|
|
169
|
+
} catch {
|
|
170
|
+
return { status: "parse-error", version: null };
|
|
171
|
+
}
|
|
172
|
+
const meta = obj["meta"];
|
|
173
|
+
if (typeof meta === "object" && meta !== null) {
|
|
174
|
+
const v = meta["lastTouchedVersion"];
|
|
175
|
+
if (typeof v === "string" && v.length > 0) {
|
|
176
|
+
return { status: "detected", version: v };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { status: "detected", version: null };
|
|
180
|
+
}
|
|
181
|
+
function isVersionAtLeast(version, minimum) {
|
|
182
|
+
const vParts = version.split(".").map(Number);
|
|
183
|
+
const mParts = minimum.split(".").map(Number);
|
|
184
|
+
const len = Math.max(vParts.length, mParts.length);
|
|
185
|
+
for (let i = 0; i < len; i++) {
|
|
186
|
+
const v = vParts[i] ?? 0;
|
|
187
|
+
const m = mParts[i] ?? 0;
|
|
188
|
+
if (Number.isNaN(v) || Number.isNaN(m)) return false;
|
|
189
|
+
if (v > m) return true;
|
|
190
|
+
if (v < m) return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
68
193
|
}
|
|
69
194
|
async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
|
|
70
195
|
let raw;
|
|
@@ -132,44 +257,6 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
|
|
|
132
257
|
});
|
|
133
258
|
return "updated";
|
|
134
259
|
}
|
|
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;
|
|
172
|
-
}
|
|
173
260
|
async function validateApiKey(apiKey, baseUrl) {
|
|
174
261
|
try {
|
|
175
262
|
const response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
@@ -194,9 +281,6 @@ async function validateApiKey(apiKey, baseUrl) {
|
|
|
194
281
|
};
|
|
195
282
|
}
|
|
196
283
|
}
|
|
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
284
|
async function isOpenClawConnected() {
|
|
201
285
|
try {
|
|
202
286
|
const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
@@ -219,85 +303,211 @@ function isClaudeCodeConnected() {
|
|
|
219
303
|
return false;
|
|
220
304
|
}
|
|
221
305
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
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";
|
|
306
|
+
function getCursorConfigPath() {
|
|
307
|
+
return join(homedir(), ".cursor", "mcp.json");
|
|
283
308
|
}
|
|
284
|
-
async function
|
|
309
|
+
async function isCursorConnected() {
|
|
285
310
|
try {
|
|
286
|
-
const raw = await readFile(
|
|
311
|
+
const raw = await readFile(getCursorConfigPath(), "utf8");
|
|
287
312
|
const obj = JSON.parse(raw);
|
|
288
313
|
const mcpServers = obj["mcpServers"];
|
|
289
314
|
if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
|
|
290
315
|
for (const entry of Object.values(mcpServers)) {
|
|
291
316
|
if (typeof entry !== "object" || entry === null) continue;
|
|
292
|
-
const
|
|
317
|
+
const rec = entry;
|
|
318
|
+
const url = rec["url"];
|
|
319
|
+
if (typeof url === "string" && url.includes("multicorn")) return true;
|
|
320
|
+
const args = rec["args"];
|
|
293
321
|
if (Array.isArray(args) && args.includes("multicorn-proxy")) return true;
|
|
294
322
|
}
|
|
295
323
|
return false;
|
|
296
|
-
} catch {
|
|
324
|
+
} catch (err) {
|
|
325
|
+
process.stderr.write(
|
|
326
|
+
`Warning: could not check Cursor connection status: ${err instanceof Error ? err.message : String(err)}
|
|
327
|
+
`
|
|
328
|
+
);
|
|
297
329
|
return false;
|
|
298
330
|
}
|
|
299
331
|
}
|
|
300
|
-
|
|
332
|
+
var PLATFORM_LABELS = ["OpenClaw", "Claude Code", "Cursor"];
|
|
333
|
+
var PLATFORM_BY_SELECTION = {
|
|
334
|
+
1: "openclaw",
|
|
335
|
+
2: "claude-code",
|
|
336
|
+
3: "cursor"
|
|
337
|
+
};
|
|
338
|
+
var DEFAULT_AGENT_NAMES = {
|
|
339
|
+
openclaw: "my-openclaw-agent",
|
|
340
|
+
"claude-code": "my-claude-code-agent",
|
|
341
|
+
cursor: "my-cursor-agent"
|
|
342
|
+
};
|
|
343
|
+
async function promptPlatformSelection(ask) {
|
|
344
|
+
process.stderr.write(
|
|
345
|
+
"\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n"
|
|
346
|
+
);
|
|
347
|
+
const connectedFlags = [
|
|
348
|
+
await isOpenClawConnected(),
|
|
349
|
+
isClaudeCodeConnected(),
|
|
350
|
+
await isCursorConnected()
|
|
351
|
+
];
|
|
352
|
+
for (let i = 0; i < PLATFORM_LABELS.length; i++) {
|
|
353
|
+
const marker = connectedFlags[i] ? " " + style.green("\u2713") + style.dim(" connected") : "";
|
|
354
|
+
process.stderr.write(
|
|
355
|
+
` ${style.violet(String(i + 1))}. ${PLATFORM_LABELS[i] ?? ""}${marker}
|
|
356
|
+
`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
let selection = 0;
|
|
360
|
+
while (selection === 0) {
|
|
361
|
+
const input = await ask("Select (1-3): ");
|
|
362
|
+
const num = parseInt(input.trim(), 10);
|
|
363
|
+
if (num >= 1 && num <= 3) {
|
|
364
|
+
selection = num;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return selection;
|
|
368
|
+
}
|
|
369
|
+
async function promptAgentName(ask, platform) {
|
|
370
|
+
const defaultAgentName = DEFAULT_AGENT_NAMES[platform] ?? "my-agent";
|
|
371
|
+
let agentName = "";
|
|
372
|
+
while (agentName.length === 0) {
|
|
373
|
+
const input = await ask(
|
|
374
|
+
`
|
|
375
|
+
What would you like to call this agent? ${style.dim(`(${defaultAgentName})`)} `
|
|
376
|
+
);
|
|
377
|
+
const raw = input.trim().length > 0 ? input.trim() : defaultAgentName;
|
|
378
|
+
const transformed = normalizeAgentName(raw);
|
|
379
|
+
if (transformed.length === 0) {
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
style.red("Agent name must contain letters or numbers. Please try again.") + "\n"
|
|
382
|
+
);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (transformed !== raw) {
|
|
386
|
+
process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
|
|
387
|
+
}
|
|
388
|
+
agentName = transformed;
|
|
389
|
+
}
|
|
390
|
+
return agentName;
|
|
391
|
+
}
|
|
392
|
+
async function promptProxyConfig(ask, agentName) {
|
|
393
|
+
let targetUrl = "";
|
|
394
|
+
while (targetUrl.length === 0) {
|
|
395
|
+
process.stderr.write(
|
|
396
|
+
"\n" + style.bold("Target MCP server URL:") + "\n" + style.dim(
|
|
397
|
+
"The URL of the MCP server you want Shield to protect. Example: https://your-server.example.com/mcp"
|
|
398
|
+
) + "\n"
|
|
399
|
+
);
|
|
400
|
+
const input = await ask("URL: ");
|
|
401
|
+
if (input.trim().length === 0) {
|
|
402
|
+
process.stderr.write(style.red("MCP server URL is required.") + "\n");
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
new URL(input.trim());
|
|
407
|
+
} catch {
|
|
408
|
+
process.stderr.write(
|
|
409
|
+
style.red(
|
|
410
|
+
"\u2717 That does not look like a valid URL. Please enter a full URL including the scheme (e.g. https://your-server.example.com/mcp)."
|
|
411
|
+
) + "\n"
|
|
412
|
+
);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
targetUrl = input.trim();
|
|
416
|
+
}
|
|
417
|
+
const defaultShortName = normalizeAgentName(agentName) || "shield-mcp";
|
|
418
|
+
const shortNameInput = await ask(
|
|
419
|
+
`
|
|
420
|
+
Short name (a nickname for this connection, used in your proxy URL): ${style.dim(`(${defaultShortName})`)} `
|
|
421
|
+
);
|
|
422
|
+
const shortName = shortNameInput.trim().length > 0 ? normalizeAgentName(shortNameInput.trim()) || defaultShortName : defaultShortName;
|
|
423
|
+
return { targetUrl, shortName };
|
|
424
|
+
}
|
|
425
|
+
async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverName, platform) {
|
|
426
|
+
let response;
|
|
427
|
+
try {
|
|
428
|
+
response = await fetch(`${baseUrl}/api/v1/proxy/config`, {
|
|
429
|
+
method: "POST",
|
|
430
|
+
headers: {
|
|
431
|
+
"Content-Type": "application/json",
|
|
432
|
+
"X-Multicorn-Key": apiKey
|
|
433
|
+
},
|
|
434
|
+
body: JSON.stringify({
|
|
435
|
+
server_name: serverName,
|
|
436
|
+
target_url: targetUrl,
|
|
437
|
+
platform,
|
|
438
|
+
agent_name: agentName
|
|
439
|
+
}),
|
|
440
|
+
signal: AbortSignal.timeout(1e4)
|
|
441
|
+
});
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
444
|
+
throw new Error(`Failed to create proxy config: ${detail}`);
|
|
445
|
+
}
|
|
446
|
+
if (!response.ok) {
|
|
447
|
+
let errorMsg = `Shield API returned an error (HTTP ${String(response.status)}). Check your agent name and target URL, then try again.`;
|
|
448
|
+
try {
|
|
449
|
+
const errBody = await response.json();
|
|
450
|
+
const errObj = errBody["error"];
|
|
451
|
+
if (typeof errObj?.["message"] === "string") {
|
|
452
|
+
errorMsg = stripAnsi(errObj["message"]);
|
|
453
|
+
} else if (typeof errBody["message"] === "string") {
|
|
454
|
+
errorMsg = stripAnsi(errBody["message"]);
|
|
455
|
+
} else if (typeof errBody["detail"] === "string") {
|
|
456
|
+
errorMsg = stripAnsi(errBody["detail"]);
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
throw new Error(errorMsg);
|
|
461
|
+
}
|
|
462
|
+
const envelope = await response.json();
|
|
463
|
+
const data = envelope["data"];
|
|
464
|
+
return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
|
|
465
|
+
}
|
|
466
|
+
function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
467
|
+
const authHeader = platform === "cursor" ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
|
|
468
|
+
const mcpSnippet = JSON.stringify(
|
|
469
|
+
{
|
|
470
|
+
mcpServers: {
|
|
471
|
+
[shortName]: {
|
|
472
|
+
url: routingToken,
|
|
473
|
+
headers: {
|
|
474
|
+
Authorization: authHeader
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
null,
|
|
480
|
+
2
|
|
481
|
+
);
|
|
482
|
+
if (platform === "openclaw") {
|
|
483
|
+
process.stderr.write("\n" + style.dim("Add this to your OpenClaw agent config:") + "\n\n");
|
|
484
|
+
} else if (platform === "claude-code") {
|
|
485
|
+
process.stderr.write("\n" + style.dim("Add this to your Claude Code MCP config:") + "\n\n");
|
|
486
|
+
} else {
|
|
487
|
+
process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
|
|
488
|
+
}
|
|
489
|
+
process.stderr.write(style.cyan(mcpSnippet) + "\n\n");
|
|
490
|
+
if (platform !== "cursor") {
|
|
491
|
+
process.stderr.write(
|
|
492
|
+
style.dim(
|
|
493
|
+
"Replace YOUR_SHIELD_API_KEY with your API key. Find it in Settings > API keys at https://app.multicorn.ai/settings/api-keys"
|
|
494
|
+
) + "\n"
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (platform === "cursor") {
|
|
498
|
+
process.stderr.write(
|
|
499
|
+
style.dim(
|
|
500
|
+
"Then restart Cursor and check Settings > Tools & MCPs for a green status indicator."
|
|
501
|
+
) + "\n"
|
|
502
|
+
);
|
|
503
|
+
process.stderr.write(
|
|
504
|
+
style.dim(
|
|
505
|
+
`Ask Cursor to use your MCP server by its short name. For example: "use the ${shortName} tool to list files in /tmp"`
|
|
506
|
+
) + "\n"
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function runInit(baseUrl = "https://api.multicorn.ai") {
|
|
301
511
|
if (!process.stdin.isTTY) {
|
|
302
512
|
process.stderr.write(
|
|
303
513
|
style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
|
|
@@ -305,20 +515,15 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
305
515
|
process.exit(1);
|
|
306
516
|
}
|
|
307
517
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
resolve(answer);
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
}
|
|
518
|
+
const ask = (question) => new Promise((resolve) => {
|
|
519
|
+
rl.question(question, resolve);
|
|
520
|
+
});
|
|
315
521
|
process.stderr.write("\n" + BANNER + "\n");
|
|
316
522
|
process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
|
|
317
523
|
process.stderr.write(style.bold(style.violet("Multicorn Shield proxy setup")) + "\n\n");
|
|
318
524
|
process.stderr.write(
|
|
319
525
|
style.dim("Get your API key at https://app.multicorn.ai/settings/api-keys") + "\n\n"
|
|
320
526
|
);
|
|
321
|
-
let apiKey = "";
|
|
322
527
|
const existing = await loadConfig().catch(() => null);
|
|
323
528
|
if (baseUrl === "https://api.multicorn.ai") {
|
|
324
529
|
if (existing !== null && existing.baseUrl.length > 0) {
|
|
@@ -330,6 +535,7 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
330
535
|
}
|
|
331
536
|
}
|
|
332
537
|
}
|
|
538
|
+
let apiKey = "";
|
|
333
539
|
if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
|
|
334
540
|
const masked = "mcs_..." + existing.apiKey.slice(-4);
|
|
335
541
|
process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
|
|
@@ -360,62 +566,46 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
360
566
|
spinner.stop(true, "Key validated");
|
|
361
567
|
apiKey = key;
|
|
362
568
|
}
|
|
363
|
-
|
|
569
|
+
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost") && !baseUrl.startsWith("http://127.0.0.1")) {
|
|
570
|
+
process.stderr.write(
|
|
571
|
+
style.red(`\u2717 Shield API base URL must use HTTPS. Got: ${baseUrl}`) + "\n"
|
|
572
|
+
);
|
|
573
|
+
rl.close();
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
const configuredAgents = [];
|
|
577
|
+
let currentAgents = collectAgentsFromConfig(existing);
|
|
364
578
|
let lastConfig = {
|
|
365
579
|
apiKey,
|
|
366
580
|
baseUrl,
|
|
367
|
-
...{
|
|
581
|
+
...currentAgents.length > 0 ? {
|
|
582
|
+
agents: currentAgents,
|
|
583
|
+
defaultAgent: existing !== null && typeof existing.defaultAgent === "string" && existing.defaultAgent.length > 0 ? existing.defaultAgent : currentAgents[currentAgents.length - 1]?.name ?? ""
|
|
584
|
+
} : {}
|
|
368
585
|
};
|
|
369
586
|
let configuring = true;
|
|
370
587
|
while (configuring) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
const claudeCodeConnected = isClaudeCodeConnected();
|
|
377
|
-
const claudeDesktopConnected = await isClaudeDesktopConnected();
|
|
378
|
-
for (let i = 0; i < platformLabels.length; i++) {
|
|
379
|
-
const sessionMarker = configuredPlatforms.has(i + 1) ? " " + style.green("\u2713") : "";
|
|
380
|
-
let connectedMarker = "";
|
|
381
|
-
if (!configuredPlatforms.has(i + 1)) {
|
|
382
|
-
if (i === 0 && openClawConnected) {
|
|
383
|
-
connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
|
|
384
|
-
} else if (i === 1 && claudeCodeConnected) {
|
|
385
|
-
connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
|
|
386
|
-
} else if (i === 2 && claudeDesktopConnected) {
|
|
387
|
-
connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
|
|
388
|
-
}
|
|
389
|
-
}
|
|
588
|
+
const selection = await promptPlatformSelection(ask);
|
|
589
|
+
const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
|
|
590
|
+
const selectedLabel = PLATFORM_LABELS[selection - 1] ?? "Cursor";
|
|
591
|
+
const existingForPlatform = currentAgents.find((a) => a.platform === selectedPlatform);
|
|
592
|
+
if (existingForPlatform !== void 0) {
|
|
390
593
|
process.stderr.write(
|
|
391
|
-
`
|
|
594
|
+
`
|
|
595
|
+
An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.name)}
|
|
392
596
|
`
|
|
393
597
|
);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
selection = num;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
let agentName = "";
|
|
404
|
-
while (agentName.length === 0) {
|
|
405
|
-
const input = await ask("\nWhat would you like to call this agent? ");
|
|
406
|
-
if (input.trim().length === 0) continue;
|
|
407
|
-
const transformed = normalizeAgentName(input);
|
|
408
|
-
if (transformed.length === 0) {
|
|
409
|
-
process.stderr.write(
|
|
410
|
-
style.red("Agent name must contain letters or numbers. Please try again.") + "\n"
|
|
411
|
-
);
|
|
598
|
+
const replace = await ask("Replace it? (Y/n) ");
|
|
599
|
+
if (replace.trim().toLowerCase() === "n") {
|
|
600
|
+
const another2 = await ask("\nConnect another agent? (Y/n) ");
|
|
601
|
+
if (another2.trim().toLowerCase() === "n") {
|
|
602
|
+
configuring = false;
|
|
603
|
+
}
|
|
412
604
|
continue;
|
|
413
605
|
}
|
|
414
|
-
if (transformed !== input.trim()) {
|
|
415
|
-
process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
|
|
416
|
-
}
|
|
417
|
-
agentName = transformed;
|
|
418
606
|
}
|
|
607
|
+
const agentName = await promptAgentName(ask, selectedPlatform);
|
|
608
|
+
let setupSucceeded = false;
|
|
419
609
|
if (selection === 1) {
|
|
420
610
|
let detection;
|
|
421
611
|
try {
|
|
@@ -489,6 +679,13 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
489
679
|
spinner.stop(false, `Failed to update OpenClaw config: ${detail}`);
|
|
490
680
|
}
|
|
491
681
|
}
|
|
682
|
+
configuredAgents.push({
|
|
683
|
+
selection,
|
|
684
|
+
platform: selectedPlatform,
|
|
685
|
+
platformLabel: selectedLabel,
|
|
686
|
+
agentName
|
|
687
|
+
});
|
|
688
|
+
setupSucceeded = true;
|
|
492
689
|
} else if (selection === 2) {
|
|
493
690
|
process.stderr.write("\nTo connect Claude Code to Shield:\n\n");
|
|
494
691
|
process.stderr.write(
|
|
@@ -497,164 +694,126 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
497
694
|
process.stderr.write(
|
|
498
695
|
" " + style.bold("Step 2") + " - Install the plugin:\n " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n\n"
|
|
499
696
|
);
|
|
500
|
-
process.stderr.write(
|
|
501
|
-
" " + style.bold("Step 3") + " - Start Claude Code:\n " + style.cyan("claude") + "\n\n"
|
|
502
|
-
);
|
|
503
|
-
process.stderr.write(
|
|
504
|
-
style.dim("Run /plugin inside Claude Code to confirm multicorn-shield is installed.") + "\n"
|
|
505
|
-
);
|
|
506
697
|
process.stderr.write(
|
|
507
698
|
style.dim("Requires Claude Code to be installed. Get it at https://code.claude.com") + "\n"
|
|
508
699
|
);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
args: [
|
|
523
|
-
"multicorn-proxy",
|
|
524
|
-
"--wrap",
|
|
525
|
-
"<your-mcp-server-command>",
|
|
526
|
-
"--agent-name",
|
|
527
|
-
agentName
|
|
528
|
-
]
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
null,
|
|
533
|
-
2
|
|
534
|
-
);
|
|
535
|
-
process.stderr.write(style.cyan(snippet) + "\n\n");
|
|
536
|
-
} else {
|
|
537
|
-
let shouldWrite = true;
|
|
538
|
-
const spinner = withSpinner("Updating Claude Desktop config...");
|
|
700
|
+
configuredAgents.push({
|
|
701
|
+
selection,
|
|
702
|
+
platform: selectedPlatform,
|
|
703
|
+
platformLabel: selectedLabel,
|
|
704
|
+
agentName
|
|
705
|
+
});
|
|
706
|
+
setupSucceeded = true;
|
|
707
|
+
} else {
|
|
708
|
+
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
709
|
+
let proxyUrl = "";
|
|
710
|
+
let created = false;
|
|
711
|
+
while (!created) {
|
|
712
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
539
713
|
try {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
);
|
|
551
|
-
} else {
|
|
552
|
-
shouldWrite = false;
|
|
553
|
-
process.stderr.write(style.dim("Skipped. Existing config left unchanged.") + "\n");
|
|
554
|
-
}
|
|
555
|
-
} else if (result === "parse-error") {
|
|
556
|
-
spinner.stop(false, "Claude Desktop config file contains invalid JSON.");
|
|
557
|
-
const configPath = getClaudeDesktopConfigPath();
|
|
558
|
-
process.stderr.write(
|
|
559
|
-
style.yellow("\u26A0") + " Fix the JSON in " + style.cyan(configPath) + " or add this entry manually:\n\n"
|
|
560
|
-
);
|
|
561
|
-
const snippet = JSON.stringify(
|
|
562
|
-
{
|
|
563
|
-
mcpServers: {
|
|
564
|
-
[agentName]: {
|
|
565
|
-
command: "npx",
|
|
566
|
-
args: [
|
|
567
|
-
"multicorn-proxy",
|
|
568
|
-
"--wrap",
|
|
569
|
-
...mcpCommand.trim().split(/\s+/),
|
|
570
|
-
"--agent-name",
|
|
571
|
-
agentName
|
|
572
|
-
]
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
},
|
|
576
|
-
null,
|
|
577
|
-
2
|
|
578
|
-
);
|
|
579
|
-
process.stderr.write(style.cyan(snippet) + "\n\n");
|
|
580
|
-
} else {
|
|
581
|
-
const verb = result === "created" ? "Created" : "Updated";
|
|
582
|
-
spinner.stop(
|
|
583
|
-
true,
|
|
584
|
-
verb + " Claude Desktop config at " + style.cyan(getClaudeDesktopConfigPath())
|
|
585
|
-
);
|
|
586
|
-
process.stderr.write(style.dim("Restart Claude Desktop to pick up changes.") + "\n");
|
|
587
|
-
}
|
|
714
|
+
proxyUrl = await createProxyConfig(
|
|
715
|
+
baseUrl,
|
|
716
|
+
apiKey,
|
|
717
|
+
agentName,
|
|
718
|
+
targetUrl,
|
|
719
|
+
shortName,
|
|
720
|
+
selectedPlatform
|
|
721
|
+
);
|
|
722
|
+
spinner.stop(true, "Proxy config created!");
|
|
723
|
+
created = true;
|
|
588
724
|
} catch (error) {
|
|
589
725
|
const detail = error instanceof Error ? error.message : String(error);
|
|
590
|
-
spinner.stop(false,
|
|
591
|
-
|
|
726
|
+
spinner.stop(false, detail);
|
|
727
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
728
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
592
731
|
}
|
|
593
732
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
} catch (error) {
|
|
609
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
610
|
-
process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
|
|
733
|
+
if (created && proxyUrl.length > 0) {
|
|
734
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
735
|
+
process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
|
|
736
|
+
printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
|
|
737
|
+
configuredAgents.push({
|
|
738
|
+
selection,
|
|
739
|
+
platform: selectedPlatform,
|
|
740
|
+
platformLabel: selectedLabel,
|
|
741
|
+
agentName,
|
|
742
|
+
shortName,
|
|
743
|
+
proxyUrl
|
|
744
|
+
});
|
|
745
|
+
setupSucceeded = true;
|
|
746
|
+
}
|
|
611
747
|
}
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
748
|
+
if (setupSucceeded) {
|
|
749
|
+
currentAgents = currentAgents.filter((a) => a.platform !== selectedPlatform);
|
|
750
|
+
currentAgents.push({ name: agentName, platform: selectedPlatform });
|
|
751
|
+
const raw = existing !== null ? { ...existing } : {};
|
|
752
|
+
raw["apiKey"] = apiKey;
|
|
753
|
+
raw["baseUrl"] = baseUrl;
|
|
754
|
+
raw["agents"] = currentAgents;
|
|
755
|
+
raw["defaultAgent"] = agentName;
|
|
756
|
+
delete raw["agentName"];
|
|
757
|
+
delete raw["platform"];
|
|
758
|
+
lastConfig = raw;
|
|
759
|
+
try {
|
|
760
|
+
await saveConfig(lastConfig);
|
|
761
|
+
process.stderr.write(
|
|
762
|
+
style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
|
|
763
|
+
`
|
|
764
|
+
);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
767
|
+
process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
|
|
768
|
+
}
|
|
615
769
|
}
|
|
616
|
-
const another = await ask("\
|
|
617
|
-
if (another.trim().toLowerCase()
|
|
770
|
+
const another = await ask("\nConnect another agent? (Y/n) ");
|
|
771
|
+
if (another.trim().toLowerCase() === "n") {
|
|
618
772
|
configuring = false;
|
|
619
773
|
}
|
|
620
774
|
}
|
|
621
775
|
rl.close();
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
`)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
776
|
+
if (configuredAgents.length > 0) {
|
|
777
|
+
process.stderr.write("\n" + style.bold(style.violet("Setup complete")) + "\n\n");
|
|
778
|
+
for (const agent of configuredAgents) {
|
|
779
|
+
process.stderr.write(
|
|
780
|
+
` ${style.green("\u2713")} ${agent.platformLabel} - ${style.cyan(agent.agentName)}${agent.proxyUrl != null ? ` ${style.dim(`(${agent.proxyUrl})`)}` : ""}
|
|
781
|
+
`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
process.stderr.write("\n");
|
|
785
|
+
const configuredPlatforms = new Set(configuredAgents.map((a) => a.platform));
|
|
786
|
+
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
787
|
+
const blocks = [];
|
|
788
|
+
if (configuredPlatforms.has("openclaw")) {
|
|
789
|
+
blocks.push(
|
|
790
|
+
"\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"
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
if (configuredPlatforms.has("claude-code")) {
|
|
794
|
+
blocks.push(
|
|
795
|
+
"\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"
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
if (configuredPlatforms.has("claude-desktop")) {
|
|
799
|
+
blocks.push(
|
|
800
|
+
"\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
if (configuredPlatforms.has("cursor")) {
|
|
804
|
+
blocks.push(
|
|
805
|
+
"\n" + style.bold("To complete your Cursor setup:") + "\n \u2192 Restart Cursor to pick up MCP config changes\n"
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
if (configuredPlatforms.has("other-mcp")) {
|
|
809
|
+
blocks.push(
|
|
810
|
+
"\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"
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
process.stderr.write(blocks.join("") + "\n");
|
|
649
814
|
}
|
|
650
|
-
process.stderr.write(blocks.join("") + "\n");
|
|
651
815
|
return lastConfig;
|
|
652
816
|
}
|
|
653
|
-
function isProxyConfig(value) {
|
|
654
|
-
if (typeof value !== "object" || value === null) return false;
|
|
655
|
-
const obj = value;
|
|
656
|
-
return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
|
|
657
|
-
}
|
|
658
817
|
|
|
659
818
|
// src/types/index.ts
|
|
660
819
|
var PERMISSION_LEVELS = {
|
|
@@ -1340,7 +1499,6 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform)
|
|
|
1340
1499
|
const cachedScopes = await loadCachedScopes(agentName, apiKey);
|
|
1341
1500
|
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
1342
1501
|
logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
|
|
1343
|
-
return { id: "", name: agentName, scopes: cachedScopes };
|
|
1344
1502
|
}
|
|
1345
1503
|
let agent = await findAgentByName(agentName, apiKey, baseUrl);
|
|
1346
1504
|
if (agent?.authInvalid) {
|
|
@@ -1357,6 +1515,10 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform)
|
|
|
1357
1515
|
return { id: "", name: agentName, scopes: [], authInvalid: true };
|
|
1358
1516
|
}
|
|
1359
1517
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1518
|
+
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
1519
|
+
logger.warn("Service unreachable. Using cached scopes.", { error: detail });
|
|
1520
|
+
return { id: "", name: agentName, scopes: cachedScopes };
|
|
1521
|
+
}
|
|
1360
1522
|
logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
|
|
1361
1523
|
error: detail
|
|
1362
1524
|
});
|
|
@@ -1485,16 +1647,26 @@ function createProxyServer(config) {
|
|
|
1485
1647
|
}
|
|
1486
1648
|
const service = extractServiceFromToolName(toolParams.name);
|
|
1487
1649
|
const action = extractActionFromToolName(toolParams.name);
|
|
1650
|
+
config.logger.debug("Extracted tool identity.", {
|
|
1651
|
+
tool: toolParams.name,
|
|
1652
|
+
service,
|
|
1653
|
+
action
|
|
1654
|
+
});
|
|
1488
1655
|
const requestedScope = { service, permissionLevel: "execute" };
|
|
1489
1656
|
const validation = validateScopeAccess(grantedScopes, requestedScope);
|
|
1490
|
-
config.logger.debug("
|
|
1657
|
+
config.logger.debug("Scope validation result.", {
|
|
1491
1658
|
tool: toolParams.name,
|
|
1492
|
-
|
|
1493
|
-
|
|
1659
|
+
allowed: validation.allowed,
|
|
1660
|
+
scopeCount: grantedScopes.length
|
|
1494
1661
|
});
|
|
1495
1662
|
if (!validation.allowed) {
|
|
1496
1663
|
await ensureConsent(requestedScope);
|
|
1497
1664
|
const revalidation = validateScopeAccess(grantedScopes, requestedScope);
|
|
1665
|
+
config.logger.debug("Post-consent revalidation result.", {
|
|
1666
|
+
tool: toolParams.name,
|
|
1667
|
+
allowed: revalidation.allowed,
|
|
1668
|
+
scopeCount: grantedScopes.length
|
|
1669
|
+
});
|
|
1498
1670
|
if (!revalidation.allowed) {
|
|
1499
1671
|
if (actionLogger !== null) {
|
|
1500
1672
|
if (!config.agentName || config.agentName.trim().length === 0) {
|
|
@@ -1502,12 +1674,18 @@ function createProxyServer(config) {
|
|
|
1502
1674
|
"[multicorn-proxy] Cannot log action: agent name not resolved\n"
|
|
1503
1675
|
);
|
|
1504
1676
|
} else {
|
|
1677
|
+
config.logger.debug("Logging blocked action (post-consent).", {
|
|
1678
|
+
agent: config.agentName,
|
|
1679
|
+
service,
|
|
1680
|
+
action
|
|
1681
|
+
});
|
|
1505
1682
|
await actionLogger.logAction({
|
|
1506
1683
|
agent: config.agentName,
|
|
1507
1684
|
service,
|
|
1508
1685
|
actionType: action,
|
|
1509
1686
|
status: "blocked"
|
|
1510
1687
|
});
|
|
1688
|
+
config.logger.debug("Blocked action logged.", { tool: toolParams.name });
|
|
1511
1689
|
}
|
|
1512
1690
|
}
|
|
1513
1691
|
return JSON.stringify(
|
|
@@ -1526,12 +1704,18 @@ function createProxyServer(config) {
|
|
|
1526
1704
|
"[multicorn-proxy] Cannot log action: agent name not resolved\n"
|
|
1527
1705
|
);
|
|
1528
1706
|
} else {
|
|
1707
|
+
config.logger.debug("Logging blocked action (spending).", {
|
|
1708
|
+
agent: config.agentName,
|
|
1709
|
+
service,
|
|
1710
|
+
action
|
|
1711
|
+
});
|
|
1529
1712
|
await actionLogger.logAction({
|
|
1530
1713
|
agent: config.agentName,
|
|
1531
1714
|
service,
|
|
1532
1715
|
actionType: action,
|
|
1533
1716
|
status: "blocked"
|
|
1534
1717
|
});
|
|
1718
|
+
config.logger.debug("Spending-blocked action logged.", { tool: toolParams.name });
|
|
1535
1719
|
}
|
|
1536
1720
|
}
|
|
1537
1721
|
const blocked = buildSpendingBlockedResponse(
|
|
@@ -1548,12 +1732,18 @@ function createProxyServer(config) {
|
|
|
1548
1732
|
if (!config.agentName || config.agentName.trim().length === 0) {
|
|
1549
1733
|
process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
|
|
1550
1734
|
} else {
|
|
1735
|
+
config.logger.debug("Logging approved action.", {
|
|
1736
|
+
agent: config.agentName,
|
|
1737
|
+
service,
|
|
1738
|
+
action
|
|
1739
|
+
});
|
|
1551
1740
|
await actionLogger.logAction({
|
|
1552
1741
|
agent: config.agentName,
|
|
1553
1742
|
service,
|
|
1554
1743
|
actionType: action,
|
|
1555
1744
|
status: "approved"
|
|
1556
1745
|
});
|
|
1746
|
+
config.logger.debug("Approved action logged.", { tool: toolParams.name });
|
|
1557
1747
|
}
|
|
1558
1748
|
}
|
|
1559
1749
|
return null;
|
|
@@ -1733,10 +1923,23 @@ function parseArgs(argv) {
|
|
|
1733
1923
|
let baseUrl = "https://api.multicorn.ai";
|
|
1734
1924
|
let dashboardUrl = "";
|
|
1735
1925
|
let agentName = "";
|
|
1926
|
+
let deleteAgentName = "";
|
|
1736
1927
|
for (let i = 0; i < args.length; i++) {
|
|
1737
1928
|
const arg = args[i];
|
|
1738
1929
|
if (arg === "init") {
|
|
1739
1930
|
subcommand = "init";
|
|
1931
|
+
} else if (arg === "agents") {
|
|
1932
|
+
subcommand = "agents";
|
|
1933
|
+
} else if (arg === "delete-agent") {
|
|
1934
|
+
subcommand = "delete-agent";
|
|
1935
|
+
const name = args[i + 1];
|
|
1936
|
+
if (name === void 0 || name.startsWith("-")) {
|
|
1937
|
+
process.stderr.write("Error: delete-agent requires an agent name.\n");
|
|
1938
|
+
process.stderr.write("Example: npx multicorn-proxy delete-agent my-agent\n");
|
|
1939
|
+
process.exit(1);
|
|
1940
|
+
}
|
|
1941
|
+
deleteAgentName = name;
|
|
1942
|
+
i++;
|
|
1740
1943
|
} else if (arg === "--wrap") {
|
|
1741
1944
|
subcommand = "wrap";
|
|
1742
1945
|
const next = args[i + 1];
|
|
@@ -1747,6 +1950,38 @@ function parseArgs(argv) {
|
|
|
1747
1950
|
}
|
|
1748
1951
|
wrapCommand = next;
|
|
1749
1952
|
wrapArgs = args.slice(i + 2);
|
|
1953
|
+
const cleaned = [];
|
|
1954
|
+
for (let j = 0; j < wrapArgs.length; j++) {
|
|
1955
|
+
const token = wrapArgs[j];
|
|
1956
|
+
if (token === "--agent-name") {
|
|
1957
|
+
const value = wrapArgs[j + 1];
|
|
1958
|
+
if (value !== void 0) {
|
|
1959
|
+
agentName = value;
|
|
1960
|
+
j++;
|
|
1961
|
+
}
|
|
1962
|
+
} else if (token === "--log-level") {
|
|
1963
|
+
const value = wrapArgs[j + 1];
|
|
1964
|
+
if (value !== void 0 && isValidLogLevel(value)) {
|
|
1965
|
+
logLevel = value;
|
|
1966
|
+
j++;
|
|
1967
|
+
}
|
|
1968
|
+
} else if (token === "--base-url") {
|
|
1969
|
+
const value = wrapArgs[j + 1];
|
|
1970
|
+
if (value !== void 0) {
|
|
1971
|
+
baseUrl = value;
|
|
1972
|
+
j++;
|
|
1973
|
+
}
|
|
1974
|
+
} else if (token === "--dashboard-url") {
|
|
1975
|
+
const value = wrapArgs[j + 1];
|
|
1976
|
+
if (value !== void 0) {
|
|
1977
|
+
dashboardUrl = value;
|
|
1978
|
+
j++;
|
|
1979
|
+
}
|
|
1980
|
+
} else if (token !== void 0) {
|
|
1981
|
+
cleaned.push(token);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
wrapArgs = cleaned;
|
|
1750
1985
|
break;
|
|
1751
1986
|
} else if (arg === "--log-level") {
|
|
1752
1987
|
const next = args[i + 1];
|
|
@@ -1774,7 +2009,16 @@ function parseArgs(argv) {
|
|
|
1774
2009
|
}
|
|
1775
2010
|
}
|
|
1776
2011
|
}
|
|
1777
|
-
return {
|
|
2012
|
+
return {
|
|
2013
|
+
subcommand,
|
|
2014
|
+
wrapCommand,
|
|
2015
|
+
wrapArgs,
|
|
2016
|
+
logLevel,
|
|
2017
|
+
baseUrl,
|
|
2018
|
+
dashboardUrl,
|
|
2019
|
+
agentName,
|
|
2020
|
+
deleteAgentName
|
|
2021
|
+
};
|
|
1778
2022
|
}
|
|
1779
2023
|
function printHelp() {
|
|
1780
2024
|
process.stderr.write(
|
|
@@ -1785,6 +2029,12 @@ function printHelp() {
|
|
|
1785
2029
|
" npx multicorn-proxy init",
|
|
1786
2030
|
" Interactive setup. Saves API key to ~/.multicorn/config.json.",
|
|
1787
2031
|
"",
|
|
2032
|
+
" npx multicorn-proxy agents",
|
|
2033
|
+
" List configured agents and show which is the default.",
|
|
2034
|
+
"",
|
|
2035
|
+
" npx multicorn-proxy delete-agent <name>",
|
|
2036
|
+
" Remove a saved agent.",
|
|
2037
|
+
"",
|
|
1788
2038
|
" npx multicorn-proxy --wrap <command> [args...]",
|
|
1789
2039
|
" Start <command> as an MCP server and proxy all tool calls through",
|
|
1790
2040
|
" Shield's permission layer.",
|
|
@@ -1814,6 +2064,40 @@ async function main() {
|
|
|
1814
2064
|
await runInit(cli.baseUrl);
|
|
1815
2065
|
return;
|
|
1816
2066
|
}
|
|
2067
|
+
if (cli.subcommand === "agents") {
|
|
2068
|
+
const config2 = await loadConfig();
|
|
2069
|
+
if (config2 === null) {
|
|
2070
|
+
process.stderr.write(
|
|
2071
|
+
"No config found. Run `npx multicorn-proxy init` to set up your API key.\n"
|
|
2072
|
+
);
|
|
2073
|
+
process.exit(1);
|
|
2074
|
+
}
|
|
2075
|
+
const agents = collectAgentsFromConfig(config2);
|
|
2076
|
+
if (agents.length === 0) {
|
|
2077
|
+
process.stderr.write("No agents configured. Run `npx multicorn-proxy init` to add one.\n");
|
|
2078
|
+
process.exit(0);
|
|
2079
|
+
}
|
|
2080
|
+
const def = config2.defaultAgent;
|
|
2081
|
+
process.stdout.write("Configured agents:\n");
|
|
2082
|
+
for (const a of agents) {
|
|
2083
|
+
const mark = a.name === def ? " (default)" : "";
|
|
2084
|
+
process.stdout.write(`${a.name} (${a.platform})${mark}
|
|
2085
|
+
`);
|
|
2086
|
+
}
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
if (cli.subcommand === "delete-agent") {
|
|
2090
|
+
const safeName = cli.deleteAgentName.replace(/[^\x20-\x7E]/g, "");
|
|
2091
|
+
const ok = await deleteAgentByName(cli.deleteAgentName);
|
|
2092
|
+
if (!ok) {
|
|
2093
|
+
process.stderr.write(`No agent named "${safeName}" in config.
|
|
2094
|
+
`);
|
|
2095
|
+
process.exit(1);
|
|
2096
|
+
}
|
|
2097
|
+
process.stdout.write(`Removed agent "${safeName}".
|
|
2098
|
+
`);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
1817
2101
|
if (!cli.baseUrl.startsWith("https://") && !cli.baseUrl.startsWith("http://localhost") && !cli.baseUrl.startsWith("http://127.0.0.1")) {
|
|
1818
2102
|
process.stderr.write(
|
|
1819
2103
|
`Error: --base-url must use HTTPS. Received: "${cli.baseUrl}"
|
|
@@ -1829,9 +2113,11 @@ Use https:// or http://localhost for local development.
|
|
|
1829
2113
|
);
|
|
1830
2114
|
process.exit(1);
|
|
1831
2115
|
}
|
|
1832
|
-
const agentName = cli
|
|
2116
|
+
const agentName = resolveWrapAgentName(cli, config);
|
|
1833
2117
|
const finalBaseUrl = cli.baseUrl !== "https://api.multicorn.ai" ? cli.baseUrl : config.baseUrl;
|
|
1834
2118
|
const finalDashboardUrl = cli.dashboardUrl !== "" ? cli.dashboardUrl : deriveDashboardUrl(finalBaseUrl);
|
|
2119
|
+
const legacyPlatform = config.platform;
|
|
2120
|
+
const platformForServer = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "other-mcp";
|
|
1835
2121
|
const proxy = createProxyServer({
|
|
1836
2122
|
command: cli.wrapCommand,
|
|
1837
2123
|
commandArgs: cli.wrapArgs,
|
|
@@ -1840,7 +2126,7 @@ Use https:// or http://localhost for local development.
|
|
|
1840
2126
|
baseUrl: finalBaseUrl,
|
|
1841
2127
|
dashboardUrl: finalDashboardUrl,
|
|
1842
2128
|
logger,
|
|
1843
|
-
platform:
|
|
2129
|
+
platform: platformForServer
|
|
1844
2130
|
});
|
|
1845
2131
|
async function shutdown() {
|
|
1846
2132
|
logger.info("Shutting down.");
|
|
@@ -1855,6 +2141,26 @@ Use https:// or http://localhost for local development.
|
|
|
1855
2141
|
});
|
|
1856
2142
|
await proxy.start();
|
|
1857
2143
|
}
|
|
2144
|
+
function resolveWrapAgentName(cli, config) {
|
|
2145
|
+
if (cli.agentName.length > 0) {
|
|
2146
|
+
return cli.agentName;
|
|
2147
|
+
}
|
|
2148
|
+
const legacyPlatform = config.platform;
|
|
2149
|
+
const legacyAgentName = config.agentName;
|
|
2150
|
+
const platformKey = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "other-mcp";
|
|
2151
|
+
const fromPlatform = getAgentByPlatform(config, platformKey);
|
|
2152
|
+
if (fromPlatform !== void 0) {
|
|
2153
|
+
return fromPlatform.name;
|
|
2154
|
+
}
|
|
2155
|
+
const fallbackDefault = getDefaultAgent(config);
|
|
2156
|
+
if (fallbackDefault !== void 0) {
|
|
2157
|
+
return fallbackDefault.name;
|
|
2158
|
+
}
|
|
2159
|
+
if (typeof legacyAgentName === "string" && legacyAgentName.length > 0) {
|
|
2160
|
+
return legacyAgentName;
|
|
2161
|
+
}
|
|
2162
|
+
return deriveAgentName(cli.wrapCommand);
|
|
2163
|
+
}
|
|
1858
2164
|
function deriveAgentName(command) {
|
|
1859
2165
|
const base = command.split("/").pop() ?? command;
|
|
1860
2166
|
return base.replace(/\.[cm]?[jt]s$/, "");
|