multicorn-shield 0.2.2 → 0.4.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/dist/multicorn-proxy.js +353 -455
- package/dist/shield-extension.js +771 -30
- package/package.json +22 -33
package/dist/multicorn-proxy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
|
-
import { readFile,
|
|
3
|
+
import { readFile, mkdir, writeFile, unlink } from 'fs/promises';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { createInterface } from 'readline';
|
|
@@ -45,7 +45,18 @@ 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 isProxyConfig(value) {
|
|
56
|
+
if (typeof value !== "object" || value === null) return false;
|
|
57
|
+
const obj = value;
|
|
58
|
+
return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
|
|
59
|
+
}
|
|
49
60
|
async function loadConfig() {
|
|
50
61
|
try {
|
|
51
62
|
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
@@ -63,113 +74,6 @@ async function saveConfig(config) {
|
|
|
63
74
|
mode: 384
|
|
64
75
|
});
|
|
65
76
|
}
|
|
66
|
-
function isErrnoException(e) {
|
|
67
|
-
return typeof e === "object" && e !== null && "code" in e;
|
|
68
|
-
}
|
|
69
|
-
async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
|
|
70
|
-
let raw;
|
|
71
|
-
try {
|
|
72
|
-
raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
73
|
-
} catch (e) {
|
|
74
|
-
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
75
|
-
return "not-found";
|
|
76
|
-
}
|
|
77
|
-
throw e;
|
|
78
|
-
}
|
|
79
|
-
let obj;
|
|
80
|
-
try {
|
|
81
|
-
obj = JSON.parse(raw);
|
|
82
|
-
} catch {
|
|
83
|
-
return "parse-error";
|
|
84
|
-
}
|
|
85
|
-
let hooks = obj["hooks"];
|
|
86
|
-
if (hooks === void 0 || typeof hooks !== "object") {
|
|
87
|
-
hooks = {};
|
|
88
|
-
obj["hooks"] = hooks;
|
|
89
|
-
}
|
|
90
|
-
let internal = hooks["internal"];
|
|
91
|
-
if (internal === void 0 || typeof internal !== "object") {
|
|
92
|
-
internal = { enabled: true, entries: {} };
|
|
93
|
-
hooks["internal"] = internal;
|
|
94
|
-
}
|
|
95
|
-
let entries = internal["entries"];
|
|
96
|
-
if (entries === void 0 || typeof entries !== "object") {
|
|
97
|
-
entries = {};
|
|
98
|
-
internal["entries"] = entries;
|
|
99
|
-
}
|
|
100
|
-
let shield = entries["multicorn-shield"];
|
|
101
|
-
if (shield === void 0 || typeof shield !== "object") {
|
|
102
|
-
shield = { enabled: true, env: {} };
|
|
103
|
-
entries["multicorn-shield"] = shield;
|
|
104
|
-
}
|
|
105
|
-
let env = shield["env"];
|
|
106
|
-
if (env === void 0 || typeof env !== "object") {
|
|
107
|
-
env = {};
|
|
108
|
-
shield["env"] = env;
|
|
109
|
-
}
|
|
110
|
-
env["MULTICORN_API_KEY"] = apiKey;
|
|
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
|
-
}
|
|
130
|
-
await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
|
|
131
|
-
encoding: "utf8"
|
|
132
|
-
});
|
|
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;
|
|
172
|
-
}
|
|
173
77
|
async function validateApiKey(apiKey, baseUrl) {
|
|
174
78
|
try {
|
|
175
79
|
const response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
@@ -194,9 +98,6 @@ async function validateApiKey(apiKey, baseUrl) {
|
|
|
194
98
|
};
|
|
195
99
|
}
|
|
196
100
|
}
|
|
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
101
|
async function isOpenClawConnected() {
|
|
201
102
|
try {
|
|
202
103
|
const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
@@ -219,84 +120,227 @@ function isClaudeCodeConnected() {
|
|
|
219
120
|
return false;
|
|
220
121
|
}
|
|
221
122
|
}
|
|
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";
|
|
123
|
+
function getCursorConfigPath() {
|
|
124
|
+
return join(homedir(), ".cursor", "mcp.json");
|
|
283
125
|
}
|
|
284
|
-
async function
|
|
126
|
+
async function isCursorConnected() {
|
|
285
127
|
try {
|
|
286
|
-
const raw = await readFile(
|
|
128
|
+
const raw = await readFile(getCursorConfigPath(), "utf8");
|
|
287
129
|
const obj = JSON.parse(raw);
|
|
288
130
|
const mcpServers = obj["mcpServers"];
|
|
289
131
|
if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
|
|
290
132
|
for (const entry of Object.values(mcpServers)) {
|
|
291
133
|
if (typeof entry !== "object" || entry === null) continue;
|
|
292
|
-
const
|
|
134
|
+
const rec = entry;
|
|
135
|
+
const url = rec["url"];
|
|
136
|
+
if (typeof url === "string" && url.includes("multicorn")) return true;
|
|
137
|
+
const args = rec["args"];
|
|
293
138
|
if (Array.isArray(args) && args.includes("multicorn-proxy")) return true;
|
|
294
139
|
}
|
|
295
140
|
return false;
|
|
296
|
-
} catch {
|
|
141
|
+
} catch (err) {
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
`Warning: could not check Cursor connection status: ${err instanceof Error ? err.message : String(err)}
|
|
144
|
+
`
|
|
145
|
+
);
|
|
297
146
|
return false;
|
|
298
147
|
}
|
|
299
148
|
}
|
|
149
|
+
var PLATFORM_LABELS = ["OpenClaw", "Claude Code", "Cursor"];
|
|
150
|
+
var PLATFORM_BY_SELECTION = {
|
|
151
|
+
1: "openclaw",
|
|
152
|
+
2: "claude-code",
|
|
153
|
+
3: "cursor"
|
|
154
|
+
};
|
|
155
|
+
var DEFAULT_AGENT_NAMES = {
|
|
156
|
+
openclaw: "my-openclaw-agent",
|
|
157
|
+
"claude-code": "my-claude-code-agent",
|
|
158
|
+
cursor: "my-cursor-agent"
|
|
159
|
+
};
|
|
160
|
+
async function promptPlatformSelection(ask) {
|
|
161
|
+
process.stderr.write(
|
|
162
|
+
"\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n"
|
|
163
|
+
);
|
|
164
|
+
const connectedFlags = [
|
|
165
|
+
await isOpenClawConnected(),
|
|
166
|
+
isClaudeCodeConnected(),
|
|
167
|
+
await isCursorConnected()
|
|
168
|
+
];
|
|
169
|
+
for (let i = 0; i < PLATFORM_LABELS.length; i++) {
|
|
170
|
+
const marker = connectedFlags[i] ? " " + style.green("\u2713") + style.dim(" connected") : "";
|
|
171
|
+
process.stderr.write(
|
|
172
|
+
` ${style.violet(String(i + 1))}. ${PLATFORM_LABELS[i] ?? ""}${marker}
|
|
173
|
+
`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
let selection = 0;
|
|
177
|
+
while (selection === 0) {
|
|
178
|
+
const input = await ask("Select (1-3): ");
|
|
179
|
+
const num = parseInt(input.trim(), 10);
|
|
180
|
+
if (num >= 1 && num <= 3) {
|
|
181
|
+
selection = num;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return selection;
|
|
185
|
+
}
|
|
186
|
+
async function promptAgentName(ask, platform) {
|
|
187
|
+
const defaultAgentName = DEFAULT_AGENT_NAMES[platform] ?? "my-agent";
|
|
188
|
+
let agentName = "";
|
|
189
|
+
while (agentName.length === 0) {
|
|
190
|
+
const input = await ask(
|
|
191
|
+
`
|
|
192
|
+
What would you like to call this agent? ${style.dim(`(${defaultAgentName})`)} `
|
|
193
|
+
);
|
|
194
|
+
const raw = input.trim().length > 0 ? input.trim() : defaultAgentName;
|
|
195
|
+
const transformed = normalizeAgentName(raw);
|
|
196
|
+
if (transformed.length === 0) {
|
|
197
|
+
process.stderr.write(
|
|
198
|
+
style.red("Agent name must contain letters or numbers. Please try again.") + "\n"
|
|
199
|
+
);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (transformed !== raw) {
|
|
203
|
+
process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
|
|
204
|
+
}
|
|
205
|
+
agentName = transformed;
|
|
206
|
+
}
|
|
207
|
+
return agentName;
|
|
208
|
+
}
|
|
209
|
+
async function promptProxyConfig(ask, agentName) {
|
|
210
|
+
let targetUrl = "";
|
|
211
|
+
while (targetUrl.length === 0) {
|
|
212
|
+
process.stderr.write(
|
|
213
|
+
"\n" + style.bold("Target MCP server URL:") + "\n" + style.dim(
|
|
214
|
+
"The URL of the MCP server you want Shield to protect. Example: https://your-server.example.com/mcp"
|
|
215
|
+
) + "\n"
|
|
216
|
+
);
|
|
217
|
+
const input = await ask("URL: ");
|
|
218
|
+
if (input.trim().length === 0) {
|
|
219
|
+
process.stderr.write(style.red("MCP server URL is required.") + "\n");
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
new URL(input.trim());
|
|
224
|
+
} catch {
|
|
225
|
+
process.stderr.write(
|
|
226
|
+
style.red(
|
|
227
|
+
"\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)."
|
|
228
|
+
) + "\n"
|
|
229
|
+
);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
targetUrl = input.trim();
|
|
233
|
+
}
|
|
234
|
+
const defaultShortName = normalizeAgentName(agentName) || "shield-mcp";
|
|
235
|
+
const shortNameInput = await ask(
|
|
236
|
+
`
|
|
237
|
+
Short name (a nickname for this connection, used in your proxy URL): ${style.dim(`(${defaultShortName})`)} `
|
|
238
|
+
);
|
|
239
|
+
const shortName = shortNameInput.trim().length > 0 ? normalizeAgentName(shortNameInput.trim()) || defaultShortName : defaultShortName;
|
|
240
|
+
return { targetUrl, shortName };
|
|
241
|
+
}
|
|
242
|
+
async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverName, platform) {
|
|
243
|
+
let response;
|
|
244
|
+
try {
|
|
245
|
+
response = await fetch(`${baseUrl}/api/v1/proxy/config`, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers: {
|
|
248
|
+
"Content-Type": "application/json",
|
|
249
|
+
"X-Multicorn-Key": apiKey
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
server_name: serverName,
|
|
253
|
+
target_url: targetUrl,
|
|
254
|
+
platform,
|
|
255
|
+
agent_name: agentName
|
|
256
|
+
}),
|
|
257
|
+
signal: AbortSignal.timeout(1e4)
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
261
|
+
throw new Error(`Failed to create proxy config: ${detail}`);
|
|
262
|
+
}
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
let errorMsg = `Shield API returned an error (HTTP ${String(response.status)}). Check your agent name and target URL, then try again.`;
|
|
265
|
+
try {
|
|
266
|
+
const errBody = await response.json();
|
|
267
|
+
const errObj = errBody["error"];
|
|
268
|
+
if (typeof errObj?.["message"] === "string") {
|
|
269
|
+
errorMsg = stripAnsi(errObj["message"]);
|
|
270
|
+
} else if (typeof errBody["message"] === "string") {
|
|
271
|
+
errorMsg = stripAnsi(errBody["message"]);
|
|
272
|
+
} else if (typeof errBody["detail"] === "string") {
|
|
273
|
+
errorMsg = stripAnsi(errBody["detail"]);
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
throw new Error(errorMsg);
|
|
278
|
+
}
|
|
279
|
+
const envelope = await response.json();
|
|
280
|
+
const data = envelope["data"];
|
|
281
|
+
return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
|
|
282
|
+
}
|
|
283
|
+
function printPlatformSnippet(platform, routingToken, shortName) {
|
|
284
|
+
const mcpSnippet = JSON.stringify(
|
|
285
|
+
{
|
|
286
|
+
mcpServers: {
|
|
287
|
+
[shortName]: {
|
|
288
|
+
url: routingToken,
|
|
289
|
+
headers: {
|
|
290
|
+
Authorization: "Bearer YOUR_SHIELD_API_KEY"
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
null,
|
|
296
|
+
2
|
|
297
|
+
);
|
|
298
|
+
if (platform === "openclaw") {
|
|
299
|
+
process.stderr.write("\n" + style.dim("Add this to your OpenClaw agent config:") + "\n\n");
|
|
300
|
+
} else if (platform === "claude-code") {
|
|
301
|
+
process.stderr.write("\n" + style.dim("Add this to your Claude Code MCP config:") + "\n\n");
|
|
302
|
+
} else {
|
|
303
|
+
process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
|
|
304
|
+
}
|
|
305
|
+
process.stderr.write(style.cyan(mcpSnippet) + "\n\n");
|
|
306
|
+
process.stderr.write(
|
|
307
|
+
style.dim(
|
|
308
|
+
"Replace YOUR_SHIELD_API_KEY with your API key. Find it in Settings > API keys at https://app.multicorn.ai/settings/api-keys"
|
|
309
|
+
) + "\n"
|
|
310
|
+
);
|
|
311
|
+
if (platform === "cursor") {
|
|
312
|
+
process.stderr.write(
|
|
313
|
+
style.dim(
|
|
314
|
+
"Then restart Cursor and check Settings > Tools & MCPs for a green status indicator."
|
|
315
|
+
) + "\n"
|
|
316
|
+
);
|
|
317
|
+
process.stderr.write(
|
|
318
|
+
style.dim(
|
|
319
|
+
`Ask Cursor to use your MCP server by its short name. For example: "use the ${shortName} tool to list files in /tmp"`
|
|
320
|
+
) + "\n"
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function printOpenClawInstructions() {
|
|
325
|
+
process.stderr.write("\n" + style.green("\u2713") + " Agent registered!\n");
|
|
326
|
+
process.stderr.write(
|
|
327
|
+
"\nTo connect this agent, add the Multicorn Shield plugin to your OpenClaw agent:\n"
|
|
328
|
+
);
|
|
329
|
+
process.stderr.write("\n " + style.cyan("openclaw plugins add multicorn-shield") + "\n");
|
|
330
|
+
process.stderr.write(
|
|
331
|
+
"\nThen start your agent. Shield will monitor and protect tool calls automatically.\n"
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
function printClaudeCodeInstructions() {
|
|
335
|
+
process.stderr.write("\n" + style.green("\u2713") + " Agent registered!\n");
|
|
336
|
+
process.stderr.write(
|
|
337
|
+
"\nTo connect this agent, install the Multicorn Shield plugin in Claude Code:\n"
|
|
338
|
+
);
|
|
339
|
+
process.stderr.write("\n " + style.cyan("claude plugins install multicorn-shield") + "\n");
|
|
340
|
+
process.stderr.write(
|
|
341
|
+
"\nThen start a new Claude Code session. Shield will monitor and protect tool calls automatically.\n"
|
|
342
|
+
);
|
|
343
|
+
}
|
|
300
344
|
async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
301
345
|
if (!process.stdin.isTTY) {
|
|
302
346
|
process.stderr.write(
|
|
@@ -305,20 +349,15 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
305
349
|
process.exit(1);
|
|
306
350
|
}
|
|
307
351
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
resolve(answer);
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
}
|
|
352
|
+
const ask = (question) => new Promise((resolve) => {
|
|
353
|
+
rl.question(question, resolve);
|
|
354
|
+
});
|
|
315
355
|
process.stderr.write("\n" + BANNER + "\n");
|
|
316
356
|
process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
|
|
317
357
|
process.stderr.write(style.bold(style.violet("Multicorn Shield proxy setup")) + "\n\n");
|
|
318
358
|
process.stderr.write(
|
|
319
359
|
style.dim("Get your API key at https://app.multicorn.ai/settings/api-keys") + "\n\n"
|
|
320
360
|
);
|
|
321
|
-
let apiKey = "";
|
|
322
361
|
const existing = await loadConfig().catch(() => null);
|
|
323
362
|
if (baseUrl === "https://api.multicorn.ai") {
|
|
324
363
|
if (existing !== null && existing.baseUrl.length > 0) {
|
|
@@ -330,6 +369,7 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
330
369
|
}
|
|
331
370
|
}
|
|
332
371
|
}
|
|
372
|
+
let apiKey = "";
|
|
333
373
|
if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
|
|
334
374
|
const masked = "mcs_..." + existing.apiKey.slice(-4);
|
|
335
375
|
process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
|
|
@@ -360,7 +400,14 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
360
400
|
spinner.stop(true, "Key validated");
|
|
361
401
|
apiKey = key;
|
|
362
402
|
}
|
|
363
|
-
|
|
403
|
+
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost") && !baseUrl.startsWith("http://127.0.0.1")) {
|
|
404
|
+
process.stderr.write(
|
|
405
|
+
style.red(`\u2717 Shield API base URL must use HTTPS. Got: ${baseUrl}`) + "\n"
|
|
406
|
+
);
|
|
407
|
+
rl.close();
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const configuredAgents = [];
|
|
364
411
|
let lastConfig = {
|
|
365
412
|
apiKey,
|
|
366
413
|
baseUrl,
|
|
@@ -368,239 +415,55 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
368
415
|
};
|
|
369
416
|
let configuring = true;
|
|
370
417
|
while (configuring) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
const openClawConnected = await isOpenClawConnected();
|
|
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
|
-
}
|
|
390
|
-
process.stderr.write(
|
|
391
|
-
` ${style.violet(String(i + 1))}. ${platformLabels[i] ?? ""}${sessionMarker}${connectedMarker}
|
|
392
|
-
`
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
let selection = 0;
|
|
396
|
-
while (selection === 0) {
|
|
397
|
-
const input = await ask("Select (1-4): ");
|
|
398
|
-
const num = parseInt(input.trim(), 10);
|
|
399
|
-
if (num >= 1 && num <= 4) {
|
|
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
|
-
);
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
if (transformed !== input.trim()) {
|
|
415
|
-
process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
|
|
416
|
-
}
|
|
417
|
-
agentName = transformed;
|
|
418
|
-
}
|
|
418
|
+
const selection = await promptPlatformSelection(ask);
|
|
419
|
+
const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
|
|
420
|
+
const selectedLabel = PLATFORM_LABELS[selection - 1] ?? "Cursor";
|
|
421
|
+
const agentName = await promptAgentName(ask, selectedPlatform);
|
|
419
422
|
if (selection === 1) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
detection = await detectOpenClaw();
|
|
423
|
-
} catch (error) {
|
|
424
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
425
|
-
process.stderr.write(style.red("\u2717") + ` Failed to read OpenClaw config: ${detail}
|
|
426
|
-
`);
|
|
427
|
-
rl.close();
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
if (detection.status === "not-found") {
|
|
431
|
-
process.stderr.write(
|
|
432
|
-
style.red("\u2717") + " OpenClaw is not installed. Install OpenClaw first, then run npx multicorn-proxy init again.\n"
|
|
433
|
-
);
|
|
434
|
-
rl.close();
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
if (detection.status === "parse-error") {
|
|
438
|
-
process.stderr.write(
|
|
439
|
-
style.red("\u2717") + " Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually.\n"
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
if (detection.status === "detected") {
|
|
443
|
-
if (detection.version !== null) {
|
|
444
|
-
process.stderr.write(
|
|
445
|
-
style.green("\u2713") + ` OpenClaw detected ${style.dim(`(${detection.version})`)}
|
|
446
|
-
`
|
|
447
|
-
);
|
|
448
|
-
if (isVersionAtLeast(detection.version, OPENCLAW_MIN_VERSION)) {
|
|
449
|
-
process.stderr.write(
|
|
450
|
-
style.green("\u2713") + " " + style.green("Version compatible") + "\n"
|
|
451
|
-
);
|
|
452
|
-
} else {
|
|
453
|
-
process.stderr.write(
|
|
454
|
-
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)}.
|
|
455
|
-
`
|
|
456
|
-
);
|
|
457
|
-
const answer = await ask("Continue anyway? (y/N) ");
|
|
458
|
-
if (answer.trim().toLowerCase() !== "y") {
|
|
459
|
-
rl.close();
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
process.stderr.write(
|
|
465
|
-
style.yellow("\u26A0") + " Could not detect OpenClaw version. Continuing anyway.\n"
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
const spinner = withSpinner("Updating OpenClaw config...");
|
|
469
|
-
try {
|
|
470
|
-
const result = await updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName);
|
|
471
|
-
if (result === "not-found") {
|
|
472
|
-
spinner.stop(false, "OpenClaw config disappeared unexpectedly.");
|
|
473
|
-
rl.close();
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
if (result === "parse-error") {
|
|
477
|
-
spinner.stop(
|
|
478
|
-
false,
|
|
479
|
-
"Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually."
|
|
480
|
-
);
|
|
481
|
-
} else {
|
|
482
|
-
spinner.stop(
|
|
483
|
-
true,
|
|
484
|
-
"OpenClaw config updated at " + style.cyan("~/.openclaw/openclaw.json")
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
} catch (error) {
|
|
488
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
489
|
-
spinner.stop(false, `Failed to update OpenClaw config: ${detail}`);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
423
|
+
printOpenClawInstructions();
|
|
424
|
+
configuredAgents.push({ platformLabel: selectedLabel, agentName });
|
|
492
425
|
} else if (selection === 2) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
)
|
|
500
|
-
|
|
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
|
-
process.stderr.write(
|
|
507
|
-
style.dim("Requires Claude Code to be installed. Get it at https://code.claude.com") + "\n"
|
|
508
|
-
);
|
|
509
|
-
} else if (selection === 3) {
|
|
510
|
-
const mcpCommand = await ask(
|
|
511
|
-
"\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: "
|
|
512
|
-
);
|
|
513
|
-
if (mcpCommand.trim().length === 0) {
|
|
514
|
-
const configPath = getClaudeDesktopConfigPath();
|
|
515
|
-
process.stderr.write("\n" + style.dim("Add this to your Claude Desktop config at:") + "\n");
|
|
516
|
-
process.stderr.write(" " + style.cyan(configPath) + "\n\n");
|
|
517
|
-
const snippet = JSON.stringify(
|
|
518
|
-
{
|
|
519
|
-
mcpServers: {
|
|
520
|
-
[agentName]: {
|
|
521
|
-
command: "npx",
|
|
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...");
|
|
426
|
+
printClaudeCodeInstructions();
|
|
427
|
+
configuredAgents.push({ platformLabel: selectedLabel, agentName });
|
|
428
|
+
} else {
|
|
429
|
+
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
430
|
+
let proxyUrl = "";
|
|
431
|
+
let created = false;
|
|
432
|
+
while (!created) {
|
|
433
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
539
434
|
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
|
-
}
|
|
435
|
+
proxyUrl = await createProxyConfig(
|
|
436
|
+
baseUrl,
|
|
437
|
+
apiKey,
|
|
438
|
+
agentName,
|
|
439
|
+
targetUrl,
|
|
440
|
+
shortName,
|
|
441
|
+
selectedPlatform
|
|
442
|
+
);
|
|
443
|
+
spinner.stop(true, "Proxy config created!");
|
|
444
|
+
created = true;
|
|
588
445
|
} catch (error) {
|
|
589
446
|
const detail = error instanceof Error ? error.message : String(error);
|
|
590
|
-
spinner.stop(false,
|
|
591
|
-
|
|
447
|
+
spinner.stop(false, detail);
|
|
448
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
449
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
592
452
|
}
|
|
593
453
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
454
|
+
if (created && proxyUrl.length > 0) {
|
|
455
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
456
|
+
process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
|
|
457
|
+
printPlatformSnippet(selectedPlatform, proxyUrl, shortName);
|
|
458
|
+
configuredAgents.push({
|
|
459
|
+
platformLabel: selectedLabel,
|
|
460
|
+
agentName,
|
|
461
|
+
shortName,
|
|
462
|
+
proxyUrl
|
|
463
|
+
});
|
|
464
|
+
}
|
|
601
465
|
}
|
|
602
|
-
|
|
603
|
-
lastConfig = { apiKey, baseUrl, agentName, ...{} };
|
|
466
|
+
lastConfig = { apiKey, baseUrl, agentName, platform: selectedPlatform };
|
|
604
467
|
try {
|
|
605
468
|
await saveConfig(lastConfig);
|
|
606
469
|
process.stderr.write(style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
|
|
@@ -609,52 +472,24 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
|
|
|
609
472
|
const detail = error instanceof Error ? error.message : String(error);
|
|
610
473
|
process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
|
|
611
474
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
const another = await ask("\nWould you like to configure another agent? (y/N) ");
|
|
617
|
-
if (another.trim().toLowerCase() !== "y") {
|
|
475
|
+
const another = await ask("\nConnect another agent? (Y/n) ");
|
|
476
|
+
if (another.trim().toLowerCase() === "n") {
|
|
618
477
|
configuring = false;
|
|
619
478
|
}
|
|
620
479
|
}
|
|
621
480
|
rl.close();
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
`)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
blocks.push(
|
|
632
|
-
"\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"
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
if (configuredPlatforms.has(2)) {
|
|
636
|
-
blocks.push(
|
|
637
|
-
"\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"
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
if (configuredPlatforms.has(3)) {
|
|
641
|
-
blocks.push(
|
|
642
|
-
"\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
|
|
643
|
-
);
|
|
644
|
-
}
|
|
645
|
-
if (configuredPlatforms.has(4)) {
|
|
646
|
-
blocks.push(
|
|
647
|
-
"\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"
|
|
648
|
-
);
|
|
481
|
+
if (configuredAgents.length > 0) {
|
|
482
|
+
process.stderr.write("\n" + style.bold(style.violet("Setup complete")) + "\n\n");
|
|
483
|
+
for (const agent of configuredAgents) {
|
|
484
|
+
process.stderr.write(
|
|
485
|
+
` ${style.green("\u2713")} ${agent.platformLabel} - ${style.cyan(agent.agentName)}${agent.proxyUrl != null ? ` ${style.dim(`(${agent.proxyUrl})`)}` : ""}
|
|
486
|
+
`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
process.stderr.write("\n");
|
|
649
490
|
}
|
|
650
|
-
process.stderr.write(blocks.join("") + "\n");
|
|
651
491
|
return lastConfig;
|
|
652
492
|
}
|
|
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
493
|
|
|
659
494
|
// src/types/index.ts
|
|
660
495
|
var PERMISSION_LEVELS = {
|
|
@@ -1340,7 +1175,6 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform)
|
|
|
1340
1175
|
const cachedScopes = await loadCachedScopes(agentName, apiKey);
|
|
1341
1176
|
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
1342
1177
|
logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
|
|
1343
|
-
return { id: "", name: agentName, scopes: cachedScopes };
|
|
1344
1178
|
}
|
|
1345
1179
|
let agent = await findAgentByName(agentName, apiKey, baseUrl);
|
|
1346
1180
|
if (agent?.authInvalid) {
|
|
@@ -1357,6 +1191,10 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform)
|
|
|
1357
1191
|
return { id: "", name: agentName, scopes: [], authInvalid: true };
|
|
1358
1192
|
}
|
|
1359
1193
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1194
|
+
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
1195
|
+
logger.warn("Service unreachable. Using cached scopes.", { error: detail });
|
|
1196
|
+
return { id: "", name: agentName, scopes: cachedScopes };
|
|
1197
|
+
}
|
|
1360
1198
|
logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
|
|
1361
1199
|
error: detail
|
|
1362
1200
|
});
|
|
@@ -1485,16 +1323,26 @@ function createProxyServer(config) {
|
|
|
1485
1323
|
}
|
|
1486
1324
|
const service = extractServiceFromToolName(toolParams.name);
|
|
1487
1325
|
const action = extractActionFromToolName(toolParams.name);
|
|
1326
|
+
config.logger.debug("Extracted tool identity.", {
|
|
1327
|
+
tool: toolParams.name,
|
|
1328
|
+
service,
|
|
1329
|
+
action
|
|
1330
|
+
});
|
|
1488
1331
|
const requestedScope = { service, permissionLevel: "execute" };
|
|
1489
1332
|
const validation = validateScopeAccess(grantedScopes, requestedScope);
|
|
1490
|
-
config.logger.debug("
|
|
1333
|
+
config.logger.debug("Scope validation result.", {
|
|
1491
1334
|
tool: toolParams.name,
|
|
1492
|
-
|
|
1493
|
-
|
|
1335
|
+
allowed: validation.allowed,
|
|
1336
|
+
scopeCount: grantedScopes.length
|
|
1494
1337
|
});
|
|
1495
1338
|
if (!validation.allowed) {
|
|
1496
1339
|
await ensureConsent(requestedScope);
|
|
1497
1340
|
const revalidation = validateScopeAccess(grantedScopes, requestedScope);
|
|
1341
|
+
config.logger.debug("Post-consent revalidation result.", {
|
|
1342
|
+
tool: toolParams.name,
|
|
1343
|
+
allowed: revalidation.allowed,
|
|
1344
|
+
scopeCount: grantedScopes.length
|
|
1345
|
+
});
|
|
1498
1346
|
if (!revalidation.allowed) {
|
|
1499
1347
|
if (actionLogger !== null) {
|
|
1500
1348
|
if (!config.agentName || config.agentName.trim().length === 0) {
|
|
@@ -1502,12 +1350,18 @@ function createProxyServer(config) {
|
|
|
1502
1350
|
"[multicorn-proxy] Cannot log action: agent name not resolved\n"
|
|
1503
1351
|
);
|
|
1504
1352
|
} else {
|
|
1353
|
+
config.logger.debug("Logging blocked action (post-consent).", {
|
|
1354
|
+
agent: config.agentName,
|
|
1355
|
+
service,
|
|
1356
|
+
action
|
|
1357
|
+
});
|
|
1505
1358
|
await actionLogger.logAction({
|
|
1506
1359
|
agent: config.agentName,
|
|
1507
1360
|
service,
|
|
1508
1361
|
actionType: action,
|
|
1509
1362
|
status: "blocked"
|
|
1510
1363
|
});
|
|
1364
|
+
config.logger.debug("Blocked action logged.", { tool: toolParams.name });
|
|
1511
1365
|
}
|
|
1512
1366
|
}
|
|
1513
1367
|
return JSON.stringify(
|
|
@@ -1526,12 +1380,18 @@ function createProxyServer(config) {
|
|
|
1526
1380
|
"[multicorn-proxy] Cannot log action: agent name not resolved\n"
|
|
1527
1381
|
);
|
|
1528
1382
|
} else {
|
|
1383
|
+
config.logger.debug("Logging blocked action (spending).", {
|
|
1384
|
+
agent: config.agentName,
|
|
1385
|
+
service,
|
|
1386
|
+
action
|
|
1387
|
+
});
|
|
1529
1388
|
await actionLogger.logAction({
|
|
1530
1389
|
agent: config.agentName,
|
|
1531
1390
|
service,
|
|
1532
1391
|
actionType: action,
|
|
1533
1392
|
status: "blocked"
|
|
1534
1393
|
});
|
|
1394
|
+
config.logger.debug("Spending-blocked action logged.", { tool: toolParams.name });
|
|
1535
1395
|
}
|
|
1536
1396
|
}
|
|
1537
1397
|
const blocked = buildSpendingBlockedResponse(
|
|
@@ -1548,12 +1408,18 @@ function createProxyServer(config) {
|
|
|
1548
1408
|
if (!config.agentName || config.agentName.trim().length === 0) {
|
|
1549
1409
|
process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
|
|
1550
1410
|
} else {
|
|
1411
|
+
config.logger.debug("Logging approved action.", {
|
|
1412
|
+
agent: config.agentName,
|
|
1413
|
+
service,
|
|
1414
|
+
action
|
|
1415
|
+
});
|
|
1551
1416
|
await actionLogger.logAction({
|
|
1552
1417
|
agent: config.agentName,
|
|
1553
1418
|
service,
|
|
1554
1419
|
actionType: action,
|
|
1555
1420
|
status: "approved"
|
|
1556
1421
|
});
|
|
1422
|
+
config.logger.debug("Approved action logged.", { tool: toolParams.name });
|
|
1557
1423
|
}
|
|
1558
1424
|
}
|
|
1559
1425
|
return null;
|
|
@@ -1747,6 +1613,38 @@ function parseArgs(argv) {
|
|
|
1747
1613
|
}
|
|
1748
1614
|
wrapCommand = next;
|
|
1749
1615
|
wrapArgs = args.slice(i + 2);
|
|
1616
|
+
const cleaned = [];
|
|
1617
|
+
for (let j = 0; j < wrapArgs.length; j++) {
|
|
1618
|
+
const token = wrapArgs[j];
|
|
1619
|
+
if (token === "--agent-name") {
|
|
1620
|
+
const value = wrapArgs[j + 1];
|
|
1621
|
+
if (value !== void 0) {
|
|
1622
|
+
agentName = value;
|
|
1623
|
+
j++;
|
|
1624
|
+
}
|
|
1625
|
+
} else if (token === "--log-level") {
|
|
1626
|
+
const value = wrapArgs[j + 1];
|
|
1627
|
+
if (value !== void 0 && isValidLogLevel(value)) {
|
|
1628
|
+
logLevel = value;
|
|
1629
|
+
j++;
|
|
1630
|
+
}
|
|
1631
|
+
} else if (token === "--base-url") {
|
|
1632
|
+
const value = wrapArgs[j + 1];
|
|
1633
|
+
if (value !== void 0) {
|
|
1634
|
+
baseUrl = value;
|
|
1635
|
+
j++;
|
|
1636
|
+
}
|
|
1637
|
+
} else if (token === "--dashboard-url") {
|
|
1638
|
+
const value = wrapArgs[j + 1];
|
|
1639
|
+
if (value !== void 0) {
|
|
1640
|
+
dashboardUrl = value;
|
|
1641
|
+
j++;
|
|
1642
|
+
}
|
|
1643
|
+
} else if (token !== void 0) {
|
|
1644
|
+
cleaned.push(token);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
wrapArgs = cleaned;
|
|
1750
1648
|
break;
|
|
1751
1649
|
} else if (arg === "--log-level") {
|
|
1752
1650
|
const next = args[i + 1];
|
|
@@ -1840,7 +1738,7 @@ Use https:// or http://localhost for local development.
|
|
|
1840
1738
|
baseUrl: finalBaseUrl,
|
|
1841
1739
|
dashboardUrl: finalDashboardUrl,
|
|
1842
1740
|
logger,
|
|
1843
|
-
platform: "other-mcp"
|
|
1741
|
+
platform: config.platform ?? "other-mcp"
|
|
1844
1742
|
});
|
|
1845
1743
|
async function shutdown() {
|
|
1846
1744
|
logger.info("Shutting down.");
|