multicorn-shield 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/multicorn-proxy.js +2651 -2442
- package/dist/multicorn-shield.js +1651 -1441
- package/dist/shield-extension.js +19 -19
- package/package.json +1 -1
- package/plugins/cline/hooks/scripts/shared.cjs +49 -12
- package/plugins/gemini-cli/hooks/scripts/shared.cjs +49 -12
- package/plugins/windsurf/hooks/scripts/post-action.cjs +60 -12
- package/plugins/windsurf/hooks/scripts/pre-action.cjs +60 -12
package/dist/multicorn-shield.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, statSync } from 'fs';
|
|
3
3
|
import { readFile, mkdir, writeFile, copyFile, chmod, unlink } from 'fs/promises';
|
|
4
|
-
import { join, dirname } from 'path';
|
|
4
|
+
import { join, dirname, resolve, basename, sep } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { createInterface } from 'readline';
|
|
@@ -9,714 +9,1083 @@ import { spawn } from 'child_process';
|
|
|
9
9
|
import { createHash } from 'crypto';
|
|
10
10
|
import 'stream';
|
|
11
11
|
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
red: (s) => `\x1B[38;2;239;68;68m${s}\x1B[0m`,
|
|
18
|
-
cyan: (s) => `\x1B[38;2;6;182;212m${s}\x1B[0m`,
|
|
19
|
-
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
20
|
-
dim: (s) => `\x1B[2m${s}\x1B[0m`
|
|
21
|
-
};
|
|
22
|
-
var BANNER = [
|
|
23
|
-
" \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588 \u2588\u2588\u2584 ",
|
|
24
|
-
" \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
|
|
25
|
-
" \u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588",
|
|
26
|
-
" \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
|
|
27
|
-
" \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2580 "
|
|
28
|
-
].map((line) => style.violet(line)).join("\n");
|
|
29
|
-
function withSpinner(message) {
|
|
30
|
-
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
31
|
-
let i = 0;
|
|
32
|
-
const interval = setInterval(() => {
|
|
33
|
-
const frame = frames[i % frames.length];
|
|
34
|
-
process.stderr.write(`\r${style.violet(frame ?? "\u280B")} ${message}`);
|
|
35
|
-
i++;
|
|
36
|
-
}, 80);
|
|
37
|
-
return {
|
|
38
|
-
stop(success, result) {
|
|
39
|
-
clearInterval(interval);
|
|
40
|
-
const icon = success ? style.green("\u2713") : style.red("\u2717");
|
|
41
|
-
process.stderr.write(`\r\x1B[2K${icon} ${result}
|
|
42
|
-
`);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
12
|
+
var MULTICORN_DIR = join(homedir(), ".multicorn");
|
|
13
|
+
var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
|
|
14
|
+
var CACHE_META_PATH = join(MULTICORN_DIR, "cache-meta.json");
|
|
15
|
+
function cacheKey(agentName, apiKey) {
|
|
16
|
+
return createHash("sha256").update(`${agentName}:${apiKey}`).digest("hex").slice(0, 16);
|
|
45
17
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.name = "NativePluginPrerequisiteMissingError";
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
function isExistingDirectory(path) {
|
|
18
|
+
async function ensureCacheIdentity(apiKey) {
|
|
19
|
+
const currentHash = createHash("sha256").update(apiKey).digest("hex");
|
|
20
|
+
let storedHash = null;
|
|
53
21
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
22
|
+
const raw = await readFile(CACHE_META_PATH, "utf8");
|
|
23
|
+
const meta = JSON.parse(raw);
|
|
24
|
+
if (typeof meta === "object" && meta !== null && "apiKeyHash" in meta) {
|
|
25
|
+
storedHash = meta.apiKeyHash;
|
|
26
|
+
}
|
|
56
27
|
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function nativePluginSkippedSaveNote(wizardCommand, productName) {
|
|
61
|
-
return "\n" + style.dim("Your agent config has been saved. Run ") + style.cyan(wizardCommand) + style.dim(` again after installing ${productName} to complete hook setup.`) + "\n";
|
|
62
|
-
}
|
|
63
|
-
var CONFIG_DIR = join(homedir(), ".multicorn");
|
|
64
|
-
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
65
|
-
var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
|
66
|
-
var ANSI_PATTERN = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
|
|
67
|
-
function stripAnsi(str) {
|
|
68
|
-
return str.replace(ANSI_PATTERN, "");
|
|
69
|
-
}
|
|
70
|
-
function normalizeAgentName(raw) {
|
|
71
|
-
return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
72
|
-
}
|
|
73
|
-
function isErrnoException(e) {
|
|
74
|
-
return typeof e === "object" && e !== null && "code" in e;
|
|
75
|
-
}
|
|
76
|
-
function isProxyConfig(value) {
|
|
77
|
-
if (typeof value !== "object" || value === null) return false;
|
|
78
|
-
const obj = value;
|
|
79
|
-
return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
|
|
80
|
-
}
|
|
81
|
-
function isAgentEntry(value) {
|
|
82
|
-
if (typeof value !== "object" || value === null) return false;
|
|
83
|
-
const o = value;
|
|
84
|
-
return typeof o["name"] === "string" && typeof o["platform"] === "string";
|
|
85
|
-
}
|
|
86
|
-
function getAgentByPlatform(config, platform) {
|
|
87
|
-
const list = config.agents;
|
|
88
|
-
if (list === void 0 || list.length === 0) return void 0;
|
|
89
|
-
return list.find((a) => a.platform === platform);
|
|
90
|
-
}
|
|
91
|
-
function getDefaultAgent(config) {
|
|
92
|
-
const list = config.agents;
|
|
93
|
-
if (list === void 0 || list.length === 0) return void 0;
|
|
94
|
-
const defName = config.defaultAgent;
|
|
95
|
-
if (typeof defName === "string" && defName.length > 0) {
|
|
96
|
-
const match = list.find((a) => a.name === defName);
|
|
97
|
-
if (match !== void 0) return match;
|
|
98
|
-
}
|
|
99
|
-
return list[0];
|
|
100
|
-
}
|
|
101
|
-
function collectAgentsFromConfig(cfg) {
|
|
102
|
-
if (cfg === null) return [];
|
|
103
|
-
if (cfg.agents !== void 0 && cfg.agents.length > 0) {
|
|
104
|
-
return cfg.agents.map((a) => ({ name: a.name, platform: a.platform }));
|
|
105
|
-
}
|
|
106
|
-
const raw = cfg;
|
|
107
|
-
const legacyName = raw["agentName"];
|
|
108
|
-
const legacyPlatform = raw["platform"];
|
|
109
|
-
if (typeof legacyName === "string" && legacyName.length > 0) {
|
|
110
|
-
const plat = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "unknown";
|
|
111
|
-
return [{ name: legacyName, platform: plat }];
|
|
112
28
|
}
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
async function parseConfigFile() {
|
|
116
|
-
try {
|
|
117
|
-
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
29
|
+
if (storedHash === null || storedHash !== currentHash) {
|
|
118
30
|
try {
|
|
119
|
-
|
|
31
|
+
await unlink(SCOPES_PATH);
|
|
120
32
|
} catch {
|
|
121
|
-
return { kind: "parseError" };
|
|
122
|
-
}
|
|
123
|
-
} catch (error) {
|
|
124
|
-
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
125
|
-
return { kind: "missing" };
|
|
126
33
|
}
|
|
127
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
128
|
-
return { kind: "readError", message };
|
|
129
34
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (result.kind !== "ok") return null;
|
|
137
|
-
const parsed = result.value;
|
|
138
|
-
if (!isProxyConfig(parsed)) return null;
|
|
139
|
-
const obj = parsed;
|
|
140
|
-
const agentNameRaw = obj["agentName"];
|
|
141
|
-
const agentsRaw = obj["agents"];
|
|
142
|
-
const hasNonEmptyAgents = Array.isArray(agentsRaw) && agentsRaw.length > 0 && agentsRaw.every((e) => isAgentEntry(e));
|
|
143
|
-
const needsMigrate = typeof agentNameRaw === "string" && agentNameRaw.length > 0 && !hasNonEmptyAgents;
|
|
144
|
-
if (!needsMigrate) {
|
|
145
|
-
return parsed;
|
|
35
|
+
if (storedHash !== currentHash) {
|
|
36
|
+
await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
|
|
37
|
+
await writeFile(CACHE_META_PATH, JSON.stringify({ apiKeyHash: currentHash }, null, 2) + "\n", {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
mode: 384
|
|
40
|
+
});
|
|
146
41
|
}
|
|
147
|
-
const platform = typeof obj["platform"] === "string" && obj["platform"].length > 0 ? obj["platform"] : "unknown";
|
|
148
|
-
const next = { ...obj };
|
|
149
|
-
delete next["agentName"];
|
|
150
|
-
delete next["platform"];
|
|
151
|
-
next["agents"] = [{ name: agentNameRaw, platform }];
|
|
152
|
-
next["defaultAgent"] = agentNameRaw;
|
|
153
|
-
const migrated = next;
|
|
154
|
-
await saveConfig(migrated);
|
|
155
|
-
return migrated;
|
|
156
42
|
}
|
|
157
|
-
async function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
170
|
-
return void 0;
|
|
43
|
+
async function loadCachedScopes(agentName, apiKey) {
|
|
44
|
+
if (apiKey.length === 0) return null;
|
|
45
|
+
await ensureCacheIdentity(apiKey);
|
|
46
|
+
const key = cacheKey(agentName, apiKey);
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readFile(SCOPES_PATH, "utf8");
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (!isScopesCacheFile(parsed)) return null;
|
|
51
|
+
const entry = parsed[key];
|
|
52
|
+
return entry?.scopes ?? null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
171
55
|
}
|
|
172
|
-
const parsed = result.value;
|
|
173
|
-
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
174
|
-
const u = parsed["baseUrl"];
|
|
175
|
-
if (typeof u !== "string" || u.length === 0) return void 0;
|
|
176
|
-
return u;
|
|
177
56
|
}
|
|
178
|
-
async function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
const raw = { ...config };
|
|
190
|
-
if (nextAgents.length > 0) {
|
|
191
|
-
raw["agents"] = nextAgents;
|
|
192
|
-
} else {
|
|
193
|
-
delete raw["agents"];
|
|
194
|
-
}
|
|
195
|
-
if (defaultAgent !== void 0 && defaultAgent.length > 0) {
|
|
196
|
-
raw["defaultAgent"] = defaultAgent;
|
|
197
|
-
} else {
|
|
198
|
-
delete raw["defaultAgent"];
|
|
57
|
+
async function saveCachedScopes(agentName, agentId, scopes, apiKey) {
|
|
58
|
+
if (apiKey.length === 0) return;
|
|
59
|
+
await ensureCacheIdentity(apiKey);
|
|
60
|
+
const key = cacheKey(agentName, apiKey);
|
|
61
|
+
await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
|
|
62
|
+
let existing = {};
|
|
63
|
+
try {
|
|
64
|
+
const raw = await readFile(SCOPES_PATH, "utf8");
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
if (isScopesCacheFile(parsed)) existing = parsed;
|
|
67
|
+
} catch {
|
|
199
68
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
69
|
+
const updated = {
|
|
70
|
+
...existing,
|
|
71
|
+
[key]: {
|
|
72
|
+
agentId,
|
|
73
|
+
scopes,
|
|
74
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
await writeFile(SCOPES_PATH, JSON.stringify(updated, null, 2) + "\n", {
|
|
206
78
|
encoding: "utf8",
|
|
207
79
|
mode: 384
|
|
208
80
|
});
|
|
209
81
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
82
|
+
function isScopesCacheFile(value) {
|
|
83
|
+
return typeof value === "object" && value !== null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/proxy/consent.ts
|
|
87
|
+
var CONSENT_POLL_INTERVAL_MS = 3e3;
|
|
88
|
+
var CONSENT_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
89
|
+
function deriveDashboardUrl(baseUrl) {
|
|
213
90
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
91
|
+
const url = new URL(baseUrl);
|
|
92
|
+
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
93
|
+
url.port = "5173";
|
|
94
|
+
url.protocol = "http:";
|
|
95
|
+
return url.toString();
|
|
218
96
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const v = meta["lastTouchedVersion"];
|
|
230
|
-
if (typeof v === "string" && v.length > 0) {
|
|
231
|
-
return { status: "detected", version: v };
|
|
97
|
+
if (url.hostname === "api.multicorn.ai") {
|
|
98
|
+
url.hostname = "app.multicorn.ai";
|
|
99
|
+
return url.toString();
|
|
100
|
+
}
|
|
101
|
+
if (url.hostname.includes("api")) {
|
|
102
|
+
url.hostname = url.hostname.replace("api", "app");
|
|
103
|
+
return url.toString();
|
|
104
|
+
}
|
|
105
|
+
if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
106
|
+
return "https://app.multicorn.ai";
|
|
232
107
|
}
|
|
108
|
+
return "https://app.multicorn.ai";
|
|
109
|
+
} catch {
|
|
110
|
+
return "https://app.multicorn.ai";
|
|
233
111
|
}
|
|
234
|
-
return { status: "detected", version: null };
|
|
235
112
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const v = vParts[i] ?? 0;
|
|
242
|
-
const m = mParts[i] ?? 0;
|
|
243
|
-
if (Number.isNaN(v) || Number.isNaN(m)) return false;
|
|
244
|
-
if (v > m) return true;
|
|
245
|
-
if (v < m) return false;
|
|
113
|
+
var ShieldAuthError = class _ShieldAuthError extends Error {
|
|
114
|
+
constructor(message) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = "ShieldAuthError";
|
|
117
|
+
Object.setPrototypeOf(this, _ShieldAuthError.prototype);
|
|
246
118
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
let raw;
|
|
119
|
+
};
|
|
120
|
+
async function findAgentByName(agentName, apiKey, baseUrl) {
|
|
121
|
+
let response;
|
|
251
122
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
123
|
+
response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
124
|
+
headers: { "X-Multicorn-Key": apiKey },
|
|
125
|
+
signal: AbortSignal.timeout(8e3)
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
if (response.status === 401 || response.status === 403) {
|
|
132
|
+
return { id: "", name: agentName, scopes: [], authInvalid: true };
|
|
256
133
|
}
|
|
257
|
-
|
|
134
|
+
return null;
|
|
258
135
|
}
|
|
259
|
-
let
|
|
136
|
+
let body;
|
|
260
137
|
try {
|
|
261
|
-
|
|
138
|
+
body = await response.json();
|
|
262
139
|
} catch {
|
|
263
|
-
return
|
|
264
|
-
}
|
|
265
|
-
let hooks = obj["hooks"];
|
|
266
|
-
if (hooks === void 0 || typeof hooks !== "object") {
|
|
267
|
-
hooks = {};
|
|
268
|
-
obj["hooks"] = hooks;
|
|
269
|
-
}
|
|
270
|
-
let internal = hooks["internal"];
|
|
271
|
-
if (internal === void 0 || typeof internal !== "object") {
|
|
272
|
-
internal = { enabled: true, entries: {} };
|
|
273
|
-
hooks["internal"] = internal;
|
|
274
|
-
}
|
|
275
|
-
let entries = internal["entries"];
|
|
276
|
-
if (entries === void 0 || typeof entries !== "object") {
|
|
277
|
-
entries = {};
|
|
278
|
-
internal["entries"] = entries;
|
|
279
|
-
}
|
|
280
|
-
let shield = entries["multicorn-shield"];
|
|
281
|
-
if (shield === void 0 || typeof shield !== "object") {
|
|
282
|
-
shield = { enabled: true, env: {} };
|
|
283
|
-
entries["multicorn-shield"] = shield;
|
|
284
|
-
}
|
|
285
|
-
let env = shield["env"];
|
|
286
|
-
if (env === void 0 || typeof env !== "object") {
|
|
287
|
-
env = {};
|
|
288
|
-
shield["env"] = env;
|
|
289
|
-
}
|
|
290
|
-
env["MULTICORN_API_KEY"] = apiKey;
|
|
291
|
-
env["MULTICORN_BASE_URL"] = baseUrl;
|
|
292
|
-
if (agentName !== void 0) {
|
|
293
|
-
env["MULTICORN_AGENT_NAME"] = agentName;
|
|
294
|
-
const agentsList = obj["agents"];
|
|
295
|
-
const list = agentsList?.["list"];
|
|
296
|
-
if (Array.isArray(list) && list.length > 0) {
|
|
297
|
-
const first = list[0];
|
|
298
|
-
if (first["id"] !== agentName) {
|
|
299
|
-
first["id"] = agentName;
|
|
300
|
-
first["name"] = agentName;
|
|
301
|
-
}
|
|
302
|
-
} else {
|
|
303
|
-
if (agentsList !== void 0 && typeof agentsList === "object") {
|
|
304
|
-
agentsList["list"] = [{ id: agentName, name: agentName }];
|
|
305
|
-
} else {
|
|
306
|
-
obj["agents"] = { list: [{ id: agentName, name: agentName }] };
|
|
307
|
-
}
|
|
308
|
-
}
|
|
140
|
+
return null;
|
|
309
141
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
142
|
+
if (!isApiSuccessResponse(body)) return null;
|
|
143
|
+
const agents = body.data;
|
|
144
|
+
if (!Array.isArray(agents)) return null;
|
|
145
|
+
const match = agents.find(
|
|
146
|
+
(a) => isAgentSummaryShape(a) && a.name === agentName
|
|
147
|
+
);
|
|
148
|
+
if (match === void 0) return null;
|
|
149
|
+
return { id: match.id, name: match.name, scopes: [] };
|
|
314
150
|
}
|
|
315
|
-
async function
|
|
151
|
+
async function fetchRemoteAgentsSummaries(apiKey, baseUrl) {
|
|
152
|
+
let response;
|
|
316
153
|
try {
|
|
317
|
-
|
|
154
|
+
response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
318
155
|
headers: { "X-Multicorn-Key": apiKey },
|
|
319
156
|
signal: AbortSignal.timeout(8e3)
|
|
320
157
|
});
|
|
321
|
-
if (response.status === 401) {
|
|
322
|
-
return { valid: false, error: "API key not recognised. Check the key and try again." };
|
|
323
|
-
}
|
|
324
|
-
if (!response.ok) {
|
|
325
|
-
return {
|
|
326
|
-
valid: false,
|
|
327
|
-
error: `Service returned ${String(response.status)}. Check your base URL and try again.`
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
return { valid: true };
|
|
331
|
-
} catch (error) {
|
|
332
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
333
|
-
return {
|
|
334
|
-
valid: false,
|
|
335
|
-
error: `Could not reach ${baseUrl}. Check your network connection. (${detail})`
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
async function isOpenClawConnected() {
|
|
340
|
-
try {
|
|
341
|
-
const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
342
|
-
const obj = JSON.parse(raw);
|
|
343
|
-
const hooks = obj["hooks"];
|
|
344
|
-
const internal = hooks?.["internal"];
|
|
345
|
-
const entries = internal?.["entries"];
|
|
346
|
-
const shield = entries?.["multicorn-shield"];
|
|
347
|
-
const env = shield?.["env"];
|
|
348
|
-
const key = env?.["MULTICORN_API_KEY"];
|
|
349
|
-
return typeof key === "string" && key.length > 0;
|
|
350
158
|
} catch {
|
|
351
|
-
return
|
|
159
|
+
return [];
|
|
352
160
|
}
|
|
353
|
-
|
|
354
|
-
|
|
161
|
+
if (!response.ok) return [];
|
|
162
|
+
let body;
|
|
355
163
|
try {
|
|
356
|
-
|
|
164
|
+
body = await response.json();
|
|
357
165
|
} catch {
|
|
358
|
-
return
|
|
166
|
+
return [];
|
|
359
167
|
}
|
|
168
|
+
if (!isApiSuccessResponse(body)) return [];
|
|
169
|
+
const agents = body.data;
|
|
170
|
+
if (!Array.isArray(agents)) return [];
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const a of agents) {
|
|
173
|
+
if (typeof a !== "object" || a === null) continue;
|
|
174
|
+
const o = a;
|
|
175
|
+
const rawName = o["name"];
|
|
176
|
+
if (typeof rawName !== "string" || rawName.length === 0) continue;
|
|
177
|
+
const plat = o["platform"];
|
|
178
|
+
const platform = plat === null || plat === void 0 ? null : typeof plat === "string" ? plat : null;
|
|
179
|
+
out.push({ name: rawName, platform });
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
360
182
|
}
|
|
361
|
-
function
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (Array.isArray(args) && (args.includes("multicorn-shield") || args.includes("multicorn-proxy")))
|
|
377
|
-
return true;
|
|
183
|
+
async function registerAgent(agentName, apiKey, baseUrl, platform) {
|
|
184
|
+
const response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
"X-Multicorn-Key": apiKey
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({ name: agentName, ...platform ? { platform } : {} }),
|
|
191
|
+
signal: AbortSignal.timeout(8e3)
|
|
192
|
+
});
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
if (response.status === 401 || response.status === 403) {
|
|
195
|
+
throw new ShieldAuthError(
|
|
196
|
+
`Failed to register agent "${agentName}": service returned ${String(response.status)}.`
|
|
197
|
+
);
|
|
378
198
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
process.stderr.write(
|
|
382
|
-
`Warning: could not check Cursor connection status: ${err instanceof Error ? err.message : String(err)}
|
|
383
|
-
`
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Failed to register agent "${agentName}": service returned ${String(response.status)}.`
|
|
384
201
|
);
|
|
385
|
-
return false;
|
|
386
202
|
}
|
|
203
|
+
const body = await response.json();
|
|
204
|
+
if (!isApiSuccessResponse(body)) {
|
|
205
|
+
throw new Error(`Failed to register agent "${agentName}": unexpected response format.`);
|
|
206
|
+
}
|
|
207
|
+
if (!isAgentSummaryShape(body.data)) {
|
|
208
|
+
throw new Error(`Failed to register agent "${agentName}": response missing agent ID.`);
|
|
209
|
+
}
|
|
210
|
+
return body.data.id;
|
|
387
211
|
}
|
|
388
|
-
function
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
async function isWindsurfConnected() {
|
|
212
|
+
async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
|
|
213
|
+
let response;
|
|
392
214
|
try {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
for (const entry of Object.values(mcpServers)) {
|
|
398
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
399
|
-
const rec = entry;
|
|
400
|
-
const url = rec["serverUrl"];
|
|
401
|
-
if (typeof url === "string" && url.includes("multicorn")) return true;
|
|
402
|
-
}
|
|
403
|
-
return false;
|
|
215
|
+
response = await fetch(`${baseUrl}/api/v1/agents/${agentId}`, {
|
|
216
|
+
headers: { "X-Multicorn-Key": apiKey },
|
|
217
|
+
signal: AbortSignal.timeout(8e3)
|
|
218
|
+
});
|
|
404
219
|
} catch {
|
|
405
|
-
return
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
if (!response.ok) return [];
|
|
223
|
+
const body = await response.json();
|
|
224
|
+
if (!isApiSuccessResponse(body)) return [];
|
|
225
|
+
const agentDetail = body.data;
|
|
226
|
+
if (!isAgentDetailShape(agentDetail)) return [];
|
|
227
|
+
const scopes = [];
|
|
228
|
+
for (const perm of agentDetail.permissions) {
|
|
229
|
+
if (!isPermissionShape(perm)) continue;
|
|
230
|
+
if (perm.revoked_at !== null) continue;
|
|
231
|
+
if (perm.read) scopes.push({ service: perm.service, permissionLevel: "read" });
|
|
232
|
+
if (perm.write) scopes.push({ service: perm.service, permissionLevel: "write" });
|
|
233
|
+
if (perm.execute) scopes.push({ service: perm.service, permissionLevel: "execute" });
|
|
406
234
|
}
|
|
235
|
+
return scopes;
|
|
407
236
|
}
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
return join(homedir(), ".codeium", "windsurf", "hooks.json");
|
|
416
|
-
}
|
|
417
|
-
function isShieldWindsurfHookCommand(cmd) {
|
|
418
|
-
return cmd.includes("windsurf-hooks/pre-action.cjs") || cmd.includes("windsurf-hooks\\pre-action.cjs") || cmd.includes("windsurf-hooks/post-action.cjs") || cmd.includes("windsurf-hooks\\post-action.cjs");
|
|
237
|
+
function openBrowser(url) {
|
|
238
|
+
if (process.env["NODE_ENV"] === "test" || process.env["VITEST"] === "true") {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const platform = process.platform;
|
|
242
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
243
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
419
244
|
}
|
|
420
|
-
function
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
245
|
+
async function waitForConsent(agentId, agentName, apiKey, baseUrl, dashboardUrl, logger, scope, platform) {
|
|
246
|
+
const scopeStrings = scope ? [`${scope.service}:${scope.permissionLevel}`] : detectScopeHints();
|
|
247
|
+
const consentUrl = buildConsentUrl(agentName, scopeStrings, dashboardUrl, platform);
|
|
248
|
+
logger.info("Opening consent page in your browser.", { url: consentUrl });
|
|
249
|
+
process.stderr.write(
|
|
250
|
+
`
|
|
251
|
+
Action requires permission. Opening consent page...
|
|
252
|
+
${consentUrl}
|
|
253
|
+
|
|
254
|
+
Waiting for you to grant access in the Multicorn dashboard...
|
|
255
|
+
`
|
|
256
|
+
);
|
|
257
|
+
openBrowser(consentUrl);
|
|
258
|
+
const deadline = Date.now() + CONSENT_POLL_TIMEOUT_MS;
|
|
259
|
+
while (Date.now() < deadline) {
|
|
260
|
+
await sleep(CONSENT_POLL_INTERVAL_MS);
|
|
261
|
+
const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl);
|
|
262
|
+
if (scopes.length > 0) {
|
|
263
|
+
logger.info("Permissions granted.", { agent: agentName, scopeCount: scopes.length });
|
|
264
|
+
return scopes;
|
|
265
|
+
}
|
|
435
266
|
}
|
|
436
|
-
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Consent not granted within ${String(CONSENT_POLL_TIMEOUT_MS / 6e4)} minutes. Grant access at ${dashboardUrl} and restart the proxy.`
|
|
269
|
+
);
|
|
437
270
|
}
|
|
438
|
-
async function
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (!existsSync(srcPre) || !existsSync(srcPost)) {
|
|
443
|
-
throw new Error(
|
|
444
|
-
`Could not find Shield Windsurf hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
445
|
-
);
|
|
271
|
+
async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform) {
|
|
272
|
+
const cachedScopes = await loadCachedScopes(agentName, apiKey);
|
|
273
|
+
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
274
|
+
logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
|
|
446
275
|
}
|
|
447
|
-
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
style.yellow("\u26A0") + " Windsurf does not appear to be installed (~/.codeium/windsurf/ not found).\n\n"
|
|
451
|
-
);
|
|
452
|
-
process.stderr.write(
|
|
453
|
-
"Open Windsurf at least once so this folder exists, or install from:\n " + style.cyan("https://windsurf.com/download") + "\n\n"
|
|
454
|
-
);
|
|
455
|
-
process.stderr.write("Then run this wizard again:\n");
|
|
456
|
-
process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
|
|
457
|
-
throw new NativePluginPrerequisiteMissingError();
|
|
276
|
+
let agent = await findAgentByName(agentName, apiKey, baseUrl);
|
|
277
|
+
if (agent?.authInvalid) {
|
|
278
|
+
return agent;
|
|
458
279
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
280
|
+
if (agent === null) {
|
|
281
|
+
try {
|
|
282
|
+
logger.info("Agent not found. Registering.", { agent: agentName });
|
|
283
|
+
const id = await registerAgent(agentName, apiKey, baseUrl, platform);
|
|
284
|
+
agent = { id, name: agentName, scopes: [] };
|
|
285
|
+
logger.info("Agent registered.", { agent: agentName, id });
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error instanceof ShieldAuthError) {
|
|
288
|
+
return { id: "", name: agentName, scopes: [], authInvalid: true };
|
|
289
|
+
}
|
|
290
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
291
|
+
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
292
|
+
logger.warn("Service unreachable. Using cached scopes.", { error: detail });
|
|
293
|
+
return { id: "", name: agentName, scopes: cachedScopes };
|
|
294
|
+
}
|
|
295
|
+
logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
|
|
296
|
+
error: detail
|
|
297
|
+
});
|
|
298
|
+
return { id: "", name: agentName, scopes: [] };
|
|
477
299
|
}
|
|
478
300
|
}
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
"pre_write_code",
|
|
483
|
-
"pre_run_command",
|
|
484
|
-
"pre_mcp_tool_use"
|
|
485
|
-
];
|
|
486
|
-
const postKeys = [
|
|
487
|
-
"post_read_code",
|
|
488
|
-
"post_write_code",
|
|
489
|
-
"post_run_command",
|
|
490
|
-
"post_mcp_tool_use"
|
|
491
|
-
];
|
|
492
|
-
const nextHooks = { ...hooks };
|
|
493
|
-
for (const k of preKeys) {
|
|
494
|
-
const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
|
|
495
|
-
nextHooks[k] = [...merged, preEntry];
|
|
496
|
-
}
|
|
497
|
-
for (const k of postKeys) {
|
|
498
|
-
const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
|
|
499
|
-
nextHooks[k] = [...merged, postEntry];
|
|
301
|
+
const scopes = await fetchGrantedScopes(agent.id, apiKey, baseUrl);
|
|
302
|
+
if (scopes.length > 0) {
|
|
303
|
+
await saveCachedScopes(agentName, agent.id, scopes, apiKey);
|
|
500
304
|
}
|
|
501
|
-
|
|
502
|
-
const hooksDir = dirname(hooksPath);
|
|
503
|
-
await mkdir(hooksDir, { recursive: true });
|
|
504
|
-
await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", { encoding: "utf8" });
|
|
505
|
-
}
|
|
506
|
-
function getClineHooksInstallDir() {
|
|
507
|
-
return join(homedir(), ".multicorn", "cline-hooks");
|
|
508
|
-
}
|
|
509
|
-
function getClineGlobalHooksDir() {
|
|
510
|
-
return join(homedir(), "Documents", "Cline", "Hooks");
|
|
305
|
+
return { ...agent, scopes };
|
|
511
306
|
}
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (!existsSync(srcPre) || !existsSync(srcPost) || !existsSync(srcShared)) {
|
|
518
|
-
throw new Error(
|
|
519
|
-
`Could not find Shield Cline hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
520
|
-
);
|
|
307
|
+
function buildConsentUrl(agentName, scopes, dashboardUrl, platform) {
|
|
308
|
+
const base = dashboardUrl.replace(/\/+$/, "");
|
|
309
|
+
const params = new URLSearchParams({ agent: agentName });
|
|
310
|
+
if (scopes.length > 0) {
|
|
311
|
+
params.set("scopes", scopes.join(","));
|
|
521
312
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
process.stderr.write(
|
|
525
|
-
style.yellow("\u26A0") + " Cline does not appear to be installed (~/Documents/Cline/ not found).\n\n"
|
|
526
|
-
);
|
|
527
|
-
process.stderr.write("Install the Cline VS Code extension first. See:\n");
|
|
528
|
-
process.stderr.write(
|
|
529
|
-
" " + style.cyan("https://docs.cline.bot/getting-started/installing-cline") + "\n\n"
|
|
530
|
-
);
|
|
531
|
-
process.stderr.write("Then run this wizard again:\n");
|
|
532
|
-
process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
|
|
533
|
-
throw new NativePluginPrerequisiteMissingError();
|
|
313
|
+
if (platform) {
|
|
314
|
+
params.set("platform", platform);
|
|
534
315
|
}
|
|
535
|
-
|
|
536
|
-
await mkdir(installDir, { recursive: true });
|
|
537
|
-
const destPre = join(installDir, "pre-tool-use.cjs");
|
|
538
|
-
const destPost = join(installDir, "post-tool-use.cjs");
|
|
539
|
-
const destShared = join(installDir, "shared.cjs");
|
|
540
|
-
await copyFile(srcPre, destPre);
|
|
541
|
-
await copyFile(srcPost, destPost);
|
|
542
|
-
await copyFile(srcShared, destShared);
|
|
543
|
-
const hookScriptMode = 493;
|
|
544
|
-
await chmod(destPre, hookScriptMode);
|
|
545
|
-
await chmod(destPost, hookScriptMode);
|
|
546
|
-
await chmod(destShared, hookScriptMode);
|
|
547
|
-
const hooksDir = getClineGlobalHooksDir();
|
|
548
|
-
await mkdir(hooksDir, { recursive: true });
|
|
549
|
-
const preWrapper = join(hooksDir, "PreToolUse");
|
|
550
|
-
const postWrapper = join(hooksDir, "PostToolUse");
|
|
551
|
-
const preContent = `#!/usr/bin/env node
|
|
552
|
-
require(${JSON.stringify(destPre)});
|
|
553
|
-
`;
|
|
554
|
-
const postContent = `#!/usr/bin/env node
|
|
555
|
-
require(${JSON.stringify(destPost)});
|
|
556
|
-
`;
|
|
557
|
-
await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
|
|
558
|
-
await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
|
|
316
|
+
return `${base}/consent?${params.toString()}`;
|
|
559
317
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
process.stderr.write(
|
|
563
|
-
" " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
|
|
564
|
-
);
|
|
565
|
-
process.stderr.write(
|
|
566
|
-
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
|
|
567
|
-
);
|
|
568
|
-
let choice = 0;
|
|
569
|
-
while (choice === 0) {
|
|
570
|
-
const input = await ask("Choose integration (1-2): ");
|
|
571
|
-
const num = parseInt(input.trim(), 10);
|
|
572
|
-
if (num === 1) choice = 1;
|
|
573
|
-
if (num === 2) choice = 2;
|
|
574
|
-
}
|
|
575
|
-
return choice === 1 ? "native" : "hosted";
|
|
318
|
+
function detectScopeHints() {
|
|
319
|
+
return [];
|
|
576
320
|
}
|
|
577
|
-
function
|
|
578
|
-
return
|
|
321
|
+
function sleep(ms) {
|
|
322
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
579
323
|
}
|
|
580
|
-
function
|
|
581
|
-
|
|
324
|
+
function isApiSuccessResponse(value) {
|
|
325
|
+
if (typeof value !== "object" || value === null) return false;
|
|
326
|
+
const obj = value;
|
|
327
|
+
return obj["success"] === true;
|
|
582
328
|
}
|
|
583
|
-
function
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const rec = h;
|
|
588
|
-
if (rec["name"] === multicornName) return true;
|
|
589
|
-
const cmd = rec["command"];
|
|
590
|
-
if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return true;
|
|
591
|
-
}
|
|
592
|
-
return false;
|
|
329
|
+
function isAgentSummaryShape(value) {
|
|
330
|
+
if (typeof value !== "object" || value === null) return false;
|
|
331
|
+
const obj = value;
|
|
332
|
+
return typeof obj["id"] === "string" && typeof obj["name"] === "string";
|
|
593
333
|
}
|
|
594
|
-
function
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const hooks = entry["hooks"];
|
|
599
|
-
if (geminiInnerHooksReferenceShield(hooks, "multicorn-shield") || geminiInnerHooksReferenceShield(hooks, "multicorn-shield-log")) {
|
|
600
|
-
return true;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
605
|
-
function geminiSettingsHasMulticornHooks(hooks) {
|
|
606
|
-
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return false;
|
|
607
|
-
const h = hooks;
|
|
608
|
-
return geminiHookEventsReferenceShield(h["BeforeTool"]) || geminiHookEventsReferenceShield(h["AfterTool"]);
|
|
334
|
+
function isAgentDetailShape(value) {
|
|
335
|
+
if (typeof value !== "object" || value === null) return false;
|
|
336
|
+
const obj = value;
|
|
337
|
+
return Array.isArray(obj["permissions"]);
|
|
609
338
|
}
|
|
610
|
-
function
|
|
611
|
-
if (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const rec = h;
|
|
615
|
-
if (rec["name"] === "multicorn-shield" || rec["name"] === "multicorn-shield-log") return false;
|
|
616
|
-
const cmd = rec["command"];
|
|
617
|
-
if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return false;
|
|
618
|
-
return true;
|
|
619
|
-
});
|
|
339
|
+
function isPermissionShape(value) {
|
|
340
|
+
if (typeof value !== "object" || value === null) return false;
|
|
341
|
+
const obj = value;
|
|
342
|
+
return typeof obj["service"] === "string" && typeof obj["read"] === "boolean" && typeof obj["write"] === "boolean" && typeof obj["execute"] === "boolean" && (obj["revoked_at"] === null || obj["revoked_at"] === void 0 || typeof obj["revoked_at"] === "string");
|
|
620
343
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
344
|
+
|
|
345
|
+
// src/proxy/config.ts
|
|
346
|
+
var style = {
|
|
347
|
+
violet: (s) => `\x1B[38;2;124;58;237m${s}\x1B[0m`,
|
|
348
|
+
violetLight: (s) => `\x1B[38;2;167;139;250m${s}\x1B[0m`,
|
|
349
|
+
green: (s) => `\x1B[38;2;34;197;94m${s}\x1B[0m`,
|
|
350
|
+
yellow: (s) => `\x1B[38;2;245;158;11m${s}\x1B[0m`,
|
|
351
|
+
red: (s) => `\x1B[38;2;239;68;68m${s}\x1B[0m`,
|
|
352
|
+
cyan: (s) => `\x1B[38;2;6;182;212m${s}\x1B[0m`,
|
|
353
|
+
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
354
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`
|
|
355
|
+
};
|
|
356
|
+
var BANNER = [
|
|
357
|
+
" \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588 \u2588\u2588\u2584 ",
|
|
358
|
+
" \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
|
|
359
|
+
" \u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588",
|
|
360
|
+
" \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
|
|
361
|
+
" \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2580 "
|
|
362
|
+
].map((line) => style.violet(line)).join("\n");
|
|
363
|
+
function withSpinner(message) {
|
|
364
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
365
|
+
let i = 0;
|
|
366
|
+
const interval = setInterval(() => {
|
|
367
|
+
const frame = frames[i % frames.length];
|
|
368
|
+
process.stderr.write(`\r${style.violet(frame ?? "\u280B")} ${message}`);
|
|
369
|
+
i++;
|
|
370
|
+
}, 80);
|
|
371
|
+
return {
|
|
372
|
+
stop(success, result) {
|
|
373
|
+
clearInterval(interval);
|
|
374
|
+
const icon = success ? style.green("\u2713") : style.red("\u2717");
|
|
375
|
+
process.stderr.write(`\r\x1B[2K${icon} ${result}
|
|
376
|
+
`);
|
|
630
377
|
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
var NativePluginPrerequisiteMissingError = class extends Error {
|
|
381
|
+
constructor() {
|
|
382
|
+
super("Native plugin prerequisites not met");
|
|
383
|
+
this.name = "NativePluginPrerequisiteMissingError";
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
function isExistingDirectory(path) {
|
|
387
|
+
try {
|
|
388
|
+
if (!existsSync(path)) return false;
|
|
389
|
+
return statSync(path).isDirectory();
|
|
390
|
+
} catch {
|
|
391
|
+
return false;
|
|
631
392
|
}
|
|
632
|
-
return out;
|
|
633
393
|
}
|
|
634
|
-
function
|
|
635
|
-
|
|
636
|
-
out["BeforeTool"] = geminiStripMatcherGroups(out["BeforeTool"]);
|
|
637
|
-
out["AfterTool"] = geminiStripMatcherGroups(out["AfterTool"]);
|
|
638
|
-
return out;
|
|
394
|
+
function nativePluginSkippedSaveNote(wizardCommand, productName) {
|
|
395
|
+
return "\n" + style.dim("Your agent config has been saved. Run ") + style.cyan(wizardCommand) + style.dim(` again after installing ${productName} to complete hook setup.`) + "\n";
|
|
639
396
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
397
|
+
var CONFIG_DIR = join(homedir(), ".multicorn");
|
|
398
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
399
|
+
var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
|
400
|
+
var ANSI_PATTERN = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
|
|
401
|
+
function stripAnsi(str) {
|
|
402
|
+
return str.replace(ANSI_PATTERN, "");
|
|
403
|
+
}
|
|
404
|
+
function normalizeAgentName(raw) {
|
|
405
|
+
return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
406
|
+
}
|
|
407
|
+
function isErrnoException(e) {
|
|
408
|
+
return typeof e === "object" && e !== null && "code" in e;
|
|
409
|
+
}
|
|
410
|
+
function isProxyConfig(value) {
|
|
411
|
+
if (typeof value !== "object" || value === null) return false;
|
|
412
|
+
const obj = value;
|
|
413
|
+
return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
|
|
414
|
+
}
|
|
415
|
+
function isAgentEntry(value) {
|
|
416
|
+
if (typeof value !== "object" || value === null) return false;
|
|
417
|
+
const o = value;
|
|
418
|
+
if (typeof o["name"] !== "string" || typeof o["platform"] !== "string") return false;
|
|
419
|
+
if (o["workspacePath"] !== void 0 && typeof o["workspacePath"] !== "string") return false;
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
function cwdUnderWorkspacePath(cwdResolved, workspacePath) {
|
|
423
|
+
const w = resolve(workspacePath);
|
|
424
|
+
if (cwdResolved === w) return true;
|
|
425
|
+
const prefix = w.endsWith(sep) ? w : w + sep;
|
|
426
|
+
return cwdResolved.startsWith(prefix);
|
|
427
|
+
}
|
|
428
|
+
function getAgentByPlatform(config, platform, cwd) {
|
|
429
|
+
const list = config.agents;
|
|
430
|
+
if (list === void 0 || list.length === 0) return void 0;
|
|
431
|
+
const matches = list.filter((a) => a.platform === platform);
|
|
432
|
+
if (matches.length === 0) return void 0;
|
|
433
|
+
if (cwd === void 0 || cwd.length === 0) return matches[0];
|
|
434
|
+
const resolvedCwd = resolve(cwd);
|
|
435
|
+
const withPath = matches.filter(
|
|
436
|
+
(a) => typeof a.workspacePath === "string" && a.workspacePath.length > 0
|
|
437
|
+
);
|
|
438
|
+
if (withPath.length === 0) return matches[0];
|
|
439
|
+
let best;
|
|
440
|
+
let bestLen = -1;
|
|
441
|
+
for (const a of withPath) {
|
|
442
|
+
const ws = a.workspacePath;
|
|
443
|
+
if (typeof ws !== "string" || ws.length === 0) continue;
|
|
444
|
+
if (!cwdUnderWorkspacePath(resolvedCwd, ws)) continue;
|
|
445
|
+
const len = resolve(ws).length;
|
|
446
|
+
if (len > bestLen) {
|
|
447
|
+
bestLen = len;
|
|
448
|
+
best = a;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (best !== void 0) return best;
|
|
452
|
+
return matches[0];
|
|
453
|
+
}
|
|
454
|
+
function getDefaultAgent(config) {
|
|
455
|
+
const list = config.agents;
|
|
456
|
+
if (list === void 0 || list.length === 0) return void 0;
|
|
457
|
+
const defName = config.defaultAgent;
|
|
458
|
+
if (typeof defName === "string" && defName.length > 0) {
|
|
459
|
+
const match = list.find((a) => a.name === defName);
|
|
460
|
+
if (match !== void 0) return match;
|
|
649
461
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
462
|
+
return list[0];
|
|
463
|
+
}
|
|
464
|
+
function collectAgentsFromConfig(cfg) {
|
|
465
|
+
if (cfg === null) return [];
|
|
466
|
+
if (cfg.agents !== void 0 && cfg.agents.length > 0) {
|
|
467
|
+
return cfg.agents.map((a) => {
|
|
468
|
+
const e = { name: a.name, platform: a.platform };
|
|
469
|
+
if (typeof a.workspacePath === "string" && a.workspacePath.length > 0) {
|
|
470
|
+
return { ...e, workspacePath: a.workspacePath };
|
|
471
|
+
}
|
|
472
|
+
return e;
|
|
473
|
+
});
|
|
660
474
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
await chmod(destAfter, mode);
|
|
672
|
-
await chmod(destShared, mode);
|
|
673
|
-
const settingsPath = getGeminiCliSettingsPath();
|
|
674
|
-
let existing = {};
|
|
475
|
+
const raw = cfg;
|
|
476
|
+
const legacyName = raw["agentName"];
|
|
477
|
+
const legacyPlatform = raw["platform"];
|
|
478
|
+
if (typeof legacyName === "string" && legacyName.length > 0) {
|
|
479
|
+
const plat = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "unknown";
|
|
480
|
+
return [{ name: legacyName, platform: plat }];
|
|
481
|
+
}
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
async function parseConfigFile() {
|
|
675
485
|
try {
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
486
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
487
|
+
try {
|
|
488
|
+
return { kind: "ok", value: JSON.parse(raw) };
|
|
489
|
+
} catch {
|
|
490
|
+
return { kind: "parseError" };
|
|
680
491
|
}
|
|
681
|
-
} catch (
|
|
682
|
-
if (isErrnoException(
|
|
683
|
-
|
|
684
|
-
} else {
|
|
685
|
-
process.stderr.write(
|
|
686
|
-
style.yellow("\u26A0") + ` Could not parse ${settingsPath}. Create valid JSON or remove the file, then run init again.
|
|
687
|
-
`
|
|
688
|
-
);
|
|
689
|
-
throw new Error(`Invalid Gemini CLI settings at ${settingsPath}`);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
494
|
+
return { kind: "missing" };
|
|
690
495
|
}
|
|
496
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
497
|
+
return { kind: "readError", message };
|
|
691
498
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
499
|
+
}
|
|
500
|
+
function isAllowedShieldApiBaseUrl(url) {
|
|
501
|
+
return url.startsWith("https://") || url.startsWith("http://localhost") || url.startsWith("http://127.0.0.1");
|
|
502
|
+
}
|
|
503
|
+
async function loadConfig() {
|
|
504
|
+
const result = await parseConfigFile();
|
|
505
|
+
if (result.kind !== "ok") return null;
|
|
506
|
+
const parsed = result.value;
|
|
507
|
+
if (!isProxyConfig(parsed)) return null;
|
|
508
|
+
const obj = parsed;
|
|
509
|
+
const agentNameRaw = obj["agentName"];
|
|
510
|
+
const agentsRaw = obj["agents"];
|
|
511
|
+
const hasNonEmptyAgents = Array.isArray(agentsRaw) && agentsRaw.length > 0 && agentsRaw.every((e) => isAgentEntry(e));
|
|
512
|
+
const needsMigrate = typeof agentNameRaw === "string" && agentNameRaw.length > 0 && !hasNonEmptyAgents;
|
|
513
|
+
if (!needsMigrate) {
|
|
514
|
+
return parsed;
|
|
515
|
+
}
|
|
516
|
+
const platform = typeof obj["platform"] === "string" && obj["platform"].length > 0 ? obj["platform"] : "unknown";
|
|
517
|
+
const next = { ...obj };
|
|
518
|
+
delete next["agentName"];
|
|
519
|
+
delete next["platform"];
|
|
520
|
+
next["agents"] = [{ name: agentNameRaw, platform }];
|
|
521
|
+
next["defaultAgent"] = agentNameRaw;
|
|
522
|
+
const migrated = next;
|
|
523
|
+
await saveConfig(migrated);
|
|
524
|
+
return migrated;
|
|
525
|
+
}
|
|
526
|
+
async function readBaseUrlFromConfig() {
|
|
527
|
+
const result = await parseConfigFile();
|
|
528
|
+
if (result.kind === "missing") return void 0;
|
|
529
|
+
if (result.kind === "readError") {
|
|
530
|
+
process.stderr.write(
|
|
531
|
+
style.yellow(`Warning: could not read base URL from config file: ${result.message}`) + "\n"
|
|
697
532
|
);
|
|
698
|
-
|
|
699
|
-
throw new Error("Installation cancelled: existing Shield hooks left unchanged.");
|
|
700
|
-
}
|
|
533
|
+
return void 0;
|
|
701
534
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
535
|
+
if (result.kind === "parseError") {
|
|
536
|
+
process.stderr.write(
|
|
537
|
+
style.yellow("Warning: could not parse ~/.multicorn/config.json as JSON.") + "\n"
|
|
538
|
+
);
|
|
539
|
+
return void 0;
|
|
540
|
+
}
|
|
541
|
+
const parsed = result.value;
|
|
542
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
543
|
+
const u = parsed["baseUrl"];
|
|
544
|
+
if (typeof u !== "string" || u.length === 0) return void 0;
|
|
545
|
+
return u;
|
|
546
|
+
}
|
|
547
|
+
async function deleteAgentByName(name) {
|
|
548
|
+
const config = await loadConfig();
|
|
549
|
+
if (config === null) return false;
|
|
550
|
+
const agents = collectAgentsFromConfig(config);
|
|
551
|
+
const idx = agents.findIndex((a) => a.name === name);
|
|
552
|
+
if (idx === -1) return false;
|
|
553
|
+
const nextAgents = agents.filter((_, i) => i !== idx);
|
|
554
|
+
let defaultAgent = config.defaultAgent;
|
|
555
|
+
if (defaultAgent === name) {
|
|
556
|
+
defaultAgent = void 0;
|
|
557
|
+
}
|
|
558
|
+
const raw = { ...config };
|
|
559
|
+
if (nextAgents.length > 0) {
|
|
560
|
+
raw["agents"] = nextAgents;
|
|
561
|
+
} else {
|
|
562
|
+
delete raw["agents"];
|
|
563
|
+
}
|
|
564
|
+
if (defaultAgent !== void 0 && defaultAgent.length > 0) {
|
|
565
|
+
raw["defaultAgent"] = defaultAgent;
|
|
566
|
+
} else {
|
|
567
|
+
delete raw["defaultAgent"];
|
|
568
|
+
}
|
|
569
|
+
await saveConfig(raw);
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
async function saveConfig(config) {
|
|
573
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
574
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
|
|
575
|
+
encoding: "utf8",
|
|
576
|
+
mode: 384
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
var OPENCLAW_MIN_VERSION = "2026.2.26";
|
|
580
|
+
async function detectOpenClaw() {
|
|
581
|
+
let raw;
|
|
582
|
+
try {
|
|
583
|
+
raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
584
|
+
} catch (e) {
|
|
585
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
586
|
+
return { status: "not-found", version: null };
|
|
587
|
+
}
|
|
588
|
+
throw e;
|
|
589
|
+
}
|
|
590
|
+
let obj;
|
|
591
|
+
try {
|
|
592
|
+
obj = JSON.parse(raw);
|
|
593
|
+
} catch {
|
|
594
|
+
return { status: "parse-error", version: null };
|
|
595
|
+
}
|
|
596
|
+
const meta = obj["meta"];
|
|
597
|
+
if (typeof meta === "object" && meta !== null) {
|
|
598
|
+
const v = meta["lastTouchedVersion"];
|
|
599
|
+
if (typeof v === "string" && v.length > 0) {
|
|
600
|
+
return { status: "detected", version: v };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return { status: "detected", version: null };
|
|
604
|
+
}
|
|
605
|
+
function isVersionAtLeast(version, minimum) {
|
|
606
|
+
const vParts = version.split(".").map(Number);
|
|
607
|
+
const mParts = minimum.split(".").map(Number);
|
|
608
|
+
const len = Math.max(vParts.length, mParts.length);
|
|
609
|
+
for (let i = 0; i < len; i++) {
|
|
610
|
+
const v = vParts[i] ?? 0;
|
|
611
|
+
const m = mParts[i] ?? 0;
|
|
612
|
+
if (Number.isNaN(v) || Number.isNaN(m)) return false;
|
|
613
|
+
if (v > m) return true;
|
|
614
|
+
if (v < m) return false;
|
|
615
|
+
}
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
|
|
619
|
+
let raw;
|
|
620
|
+
try {
|
|
621
|
+
raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
622
|
+
} catch (e) {
|
|
623
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
624
|
+
return "not-found";
|
|
625
|
+
}
|
|
626
|
+
throw e;
|
|
627
|
+
}
|
|
628
|
+
let obj;
|
|
629
|
+
try {
|
|
630
|
+
obj = JSON.parse(raw);
|
|
631
|
+
} catch {
|
|
632
|
+
return "parse-error";
|
|
633
|
+
}
|
|
634
|
+
let hooks = obj["hooks"];
|
|
635
|
+
if (hooks === void 0 || typeof hooks !== "object") {
|
|
636
|
+
hooks = {};
|
|
637
|
+
obj["hooks"] = hooks;
|
|
638
|
+
}
|
|
639
|
+
let internal = hooks["internal"];
|
|
640
|
+
if (internal === void 0 || typeof internal !== "object") {
|
|
641
|
+
internal = { enabled: true, entries: {} };
|
|
642
|
+
hooks["internal"] = internal;
|
|
643
|
+
}
|
|
644
|
+
let entries = internal["entries"];
|
|
645
|
+
if (entries === void 0 || typeof entries !== "object") {
|
|
646
|
+
entries = {};
|
|
647
|
+
internal["entries"] = entries;
|
|
648
|
+
}
|
|
649
|
+
let shield = entries["multicorn-shield"];
|
|
650
|
+
if (shield === void 0 || typeof shield !== "object") {
|
|
651
|
+
shield = { enabled: true, env: {} };
|
|
652
|
+
entries["multicorn-shield"] = shield;
|
|
653
|
+
}
|
|
654
|
+
let env = shield["env"];
|
|
655
|
+
if (env === void 0 || typeof env !== "object") {
|
|
656
|
+
env = {};
|
|
657
|
+
shield["env"] = env;
|
|
658
|
+
}
|
|
659
|
+
env["MULTICORN_API_KEY"] = apiKey;
|
|
660
|
+
env["MULTICORN_BASE_URL"] = baseUrl;
|
|
661
|
+
if (agentName !== void 0) {
|
|
662
|
+
env["MULTICORN_AGENT_NAME"] = agentName;
|
|
663
|
+
const agentsList = obj["agents"];
|
|
664
|
+
const list = agentsList?.["list"];
|
|
665
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
666
|
+
const first = list[0];
|
|
667
|
+
if (first["id"] !== agentName) {
|
|
668
|
+
first["id"] = agentName;
|
|
669
|
+
first["name"] = agentName;
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
if (agentsList !== void 0 && typeof agentsList === "object") {
|
|
673
|
+
agentsList["list"] = [{ id: agentName, name: agentName }];
|
|
674
|
+
} else {
|
|
675
|
+
obj["agents"] = { list: [{ id: agentName, name: agentName }] };
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
|
|
680
|
+
encoding: "utf8"
|
|
681
|
+
});
|
|
682
|
+
return "updated";
|
|
683
|
+
}
|
|
684
|
+
async function validateApiKey(apiKey, baseUrl) {
|
|
685
|
+
try {
|
|
686
|
+
const response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
687
|
+
headers: { "X-Multicorn-Key": apiKey },
|
|
688
|
+
signal: AbortSignal.timeout(8e3)
|
|
689
|
+
});
|
|
690
|
+
if (response.status === 401) {
|
|
691
|
+
return { valid: false, error: "API key not recognised. Check the key and try again." };
|
|
692
|
+
}
|
|
693
|
+
if (!response.ok) {
|
|
694
|
+
return {
|
|
695
|
+
valid: false,
|
|
696
|
+
error: `Service returned ${String(response.status)}. Check your base URL and try again.`
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
return { valid: true };
|
|
700
|
+
} catch (error) {
|
|
701
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
702
|
+
return {
|
|
703
|
+
valid: false,
|
|
704
|
+
error: `Could not reach ${baseUrl}. Check your network connection. (${detail})`
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async function isOpenClawConnected() {
|
|
709
|
+
try {
|
|
710
|
+
const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
711
|
+
const obj = JSON.parse(raw);
|
|
712
|
+
const hooks = obj["hooks"];
|
|
713
|
+
const internal = hooks?.["internal"];
|
|
714
|
+
const entries = internal?.["entries"];
|
|
715
|
+
const shield = entries?.["multicorn-shield"];
|
|
716
|
+
const env = shield?.["env"];
|
|
717
|
+
const key = env?.["MULTICORN_API_KEY"];
|
|
718
|
+
return typeof key === "string" && key.length > 0;
|
|
719
|
+
} catch {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function isClaudeCodeConnected() {
|
|
724
|
+
try {
|
|
725
|
+
return existsSync(join(homedir(), ".claude", "plugins", "cache", "multicorn-shield"));
|
|
726
|
+
} catch {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function getCursorConfigPath() {
|
|
731
|
+
return join(homedir(), ".cursor", "mcp.json");
|
|
732
|
+
}
|
|
733
|
+
async function isCursorConnected() {
|
|
734
|
+
try {
|
|
735
|
+
const raw = await readFile(getCursorConfigPath(), "utf8");
|
|
736
|
+
const obj = JSON.parse(raw);
|
|
737
|
+
const mcpServers = obj["mcpServers"];
|
|
738
|
+
if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
|
|
739
|
+
for (const entry of Object.values(mcpServers)) {
|
|
740
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
741
|
+
const rec = entry;
|
|
742
|
+
const url = rec["url"];
|
|
743
|
+
if (typeof url === "string" && url.includes("multicorn")) return true;
|
|
744
|
+
const args = rec["args"];
|
|
745
|
+
if (Array.isArray(args) && (args.includes("multicorn-shield") || args.includes("multicorn-proxy")))
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
return false;
|
|
749
|
+
} catch (err) {
|
|
750
|
+
process.stderr.write(
|
|
751
|
+
`Warning: could not check Cursor connection status: ${err instanceof Error ? err.message : String(err)}
|
|
752
|
+
`
|
|
753
|
+
);
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function getWindsurfConfigPath() {
|
|
758
|
+
return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
|
|
759
|
+
}
|
|
760
|
+
async function isWindsurfConnected() {
|
|
761
|
+
try {
|
|
762
|
+
const raw = await readFile(getWindsurfConfigPath(), "utf8");
|
|
763
|
+
const obj = JSON.parse(raw);
|
|
764
|
+
const mcpServers = obj["mcpServers"];
|
|
765
|
+
if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
|
|
766
|
+
for (const entry of Object.values(mcpServers)) {
|
|
767
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
768
|
+
const rec = entry;
|
|
769
|
+
const url = rec["serverUrl"];
|
|
770
|
+
if (typeof url === "string" && url.includes("multicorn")) return true;
|
|
771
|
+
}
|
|
772
|
+
return false;
|
|
773
|
+
} catch {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function multicornShieldPackageRoot() {
|
|
778
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
779
|
+
}
|
|
780
|
+
function getWindsurfHooksInstallDir() {
|
|
781
|
+
return join(homedir(), ".multicorn", "windsurf-hooks");
|
|
782
|
+
}
|
|
783
|
+
function getWindsurfCascadeHooksJsonPath() {
|
|
784
|
+
return join(homedir(), ".codeium", "windsurf", "hooks.json");
|
|
785
|
+
}
|
|
786
|
+
function isShieldWindsurfHookCommand(cmd) {
|
|
787
|
+
return cmd.includes("windsurf-hooks/pre-action.cjs") || cmd.includes("windsurf-hooks\\pre-action.cjs") || cmd.includes("windsurf-hooks/post-action.cjs") || cmd.includes("windsurf-hooks\\post-action.cjs");
|
|
788
|
+
}
|
|
789
|
+
function filterOutShieldWindsurfHooks(entries) {
|
|
790
|
+
if (!Array.isArray(entries)) return [];
|
|
791
|
+
const out = [];
|
|
792
|
+
for (const e of entries) {
|
|
793
|
+
if (typeof e !== "object" || e === null) continue;
|
|
794
|
+
const rec = e;
|
|
795
|
+
const cmd = rec["command"];
|
|
796
|
+
if (typeof cmd !== "string" || isShieldWindsurfHookCommand(cmd)) continue;
|
|
797
|
+
const powershell = rec["powershell"];
|
|
798
|
+
const show_output = rec["show_output"];
|
|
799
|
+
out.push({
|
|
800
|
+
command: cmd,
|
|
801
|
+
...typeof powershell === "string" ? { powershell } : {},
|
|
802
|
+
...show_output === true ? { show_output: true } : {}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return out;
|
|
806
|
+
}
|
|
807
|
+
async function installWindsurfNativeHooks() {
|
|
808
|
+
const root = multicornShieldPackageRoot();
|
|
809
|
+
const srcPre = join(root, "plugins", "windsurf", "hooks", "scripts", "pre-action.cjs");
|
|
810
|
+
const srcPost = join(root, "plugins", "windsurf", "hooks", "scripts", "post-action.cjs");
|
|
811
|
+
if (!existsSync(srcPre) || !existsSync(srcPost)) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`Could not find Shield Windsurf hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
const windsurfConfigDir = join(homedir(), ".codeium", "windsurf");
|
|
817
|
+
if (!isExistingDirectory(windsurfConfigDir)) {
|
|
818
|
+
process.stderr.write(
|
|
819
|
+
style.yellow("\u26A0") + " Windsurf does not appear to be installed (~/.codeium/windsurf/ not found).\n\n"
|
|
820
|
+
);
|
|
821
|
+
process.stderr.write(
|
|
822
|
+
"Open Windsurf at least once so this folder exists, or install from:\n " + style.cyan("https://windsurf.com/download") + "\n\n"
|
|
823
|
+
);
|
|
824
|
+
process.stderr.write("Then run this wizard again:\n");
|
|
825
|
+
process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
|
|
826
|
+
throw new NativePluginPrerequisiteMissingError();
|
|
827
|
+
}
|
|
828
|
+
const installDir = getWindsurfHooksInstallDir();
|
|
829
|
+
await mkdir(installDir, { recursive: true });
|
|
830
|
+
const destPre = join(installDir, "pre-action.cjs");
|
|
831
|
+
const destPost = join(installDir, "post-action.cjs");
|
|
832
|
+
await copyFile(srcPre, destPre);
|
|
833
|
+
await copyFile(srcPost, destPost);
|
|
834
|
+
const preCmd = `node ${JSON.stringify(destPre)}`;
|
|
835
|
+
const postCmd = `node ${JSON.stringify(destPost)}`;
|
|
836
|
+
const preEntry = { command: preCmd, powershell: preCmd, show_output: true };
|
|
837
|
+
const postEntry = { command: postCmd, powershell: postCmd };
|
|
838
|
+
const hooksPath = getWindsurfCascadeHooksJsonPath();
|
|
839
|
+
let base = { hooks: {} };
|
|
840
|
+
try {
|
|
841
|
+
const raw = await readFile(hooksPath, "utf8");
|
|
842
|
+
base = JSON.parse(raw);
|
|
843
|
+
} catch (err) {
|
|
844
|
+
if (!isErrnoException(err) || err.code !== "ENOENT") {
|
|
845
|
+
throw err;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const hooks = base["hooks"] ?? {};
|
|
849
|
+
const preKeys = [
|
|
850
|
+
"pre_read_code",
|
|
851
|
+
"pre_write_code",
|
|
852
|
+
"pre_run_command",
|
|
853
|
+
"pre_mcp_tool_use"
|
|
854
|
+
];
|
|
855
|
+
const postKeys = [
|
|
856
|
+
"post_read_code",
|
|
857
|
+
"post_write_code",
|
|
858
|
+
"post_run_command",
|
|
859
|
+
"post_mcp_tool_use"
|
|
860
|
+
];
|
|
861
|
+
const nextHooks = { ...hooks };
|
|
862
|
+
for (const k of preKeys) {
|
|
863
|
+
const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
|
|
864
|
+
nextHooks[k] = [...merged, preEntry];
|
|
865
|
+
}
|
|
866
|
+
for (const k of postKeys) {
|
|
867
|
+
const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
|
|
868
|
+
nextHooks[k] = [...merged, postEntry];
|
|
869
|
+
}
|
|
870
|
+
base["hooks"] = nextHooks;
|
|
871
|
+
const hooksDir = dirname(hooksPath);
|
|
872
|
+
await mkdir(hooksDir, { recursive: true });
|
|
873
|
+
await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", { encoding: "utf8" });
|
|
874
|
+
}
|
|
875
|
+
function getClineHooksInstallDir() {
|
|
876
|
+
return join(homedir(), ".multicorn", "cline-hooks");
|
|
877
|
+
}
|
|
878
|
+
function getClineGlobalHooksDir() {
|
|
879
|
+
return join(homedir(), "Documents", "Cline", "Hooks");
|
|
880
|
+
}
|
|
881
|
+
async function installClineNativeHooks() {
|
|
882
|
+
const root = multicornShieldPackageRoot();
|
|
883
|
+
const srcPre = join(root, "plugins", "cline", "hooks", "scripts", "pre-tool-use.cjs");
|
|
884
|
+
const srcPost = join(root, "plugins", "cline", "hooks", "scripts", "post-tool-use.cjs");
|
|
885
|
+
const srcShared = join(root, "plugins", "cline", "hooks", "scripts", "shared.cjs");
|
|
886
|
+
if (!existsSync(srcPre) || !existsSync(srcPost) || !existsSync(srcShared)) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
`Could not find Shield Cline hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
const clineDocsDir = join(homedir(), "Documents", "Cline");
|
|
892
|
+
if (!isExistingDirectory(clineDocsDir)) {
|
|
893
|
+
process.stderr.write(
|
|
894
|
+
style.yellow("\u26A0") + " Cline does not appear to be installed (~/Documents/Cline/ not found).\n\n"
|
|
895
|
+
);
|
|
896
|
+
process.stderr.write("Install the Cline VS Code extension first. See:\n");
|
|
897
|
+
process.stderr.write(
|
|
898
|
+
" " + style.cyan("https://docs.cline.bot/getting-started/installing-cline") + "\n\n"
|
|
899
|
+
);
|
|
900
|
+
process.stderr.write("Then run this wizard again:\n");
|
|
901
|
+
process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
|
|
902
|
+
throw new NativePluginPrerequisiteMissingError();
|
|
903
|
+
}
|
|
904
|
+
const installDir = getClineHooksInstallDir();
|
|
905
|
+
await mkdir(installDir, { recursive: true });
|
|
906
|
+
const destPre = join(installDir, "pre-tool-use.cjs");
|
|
907
|
+
const destPost = join(installDir, "post-tool-use.cjs");
|
|
908
|
+
const destShared = join(installDir, "shared.cjs");
|
|
909
|
+
await copyFile(srcPre, destPre);
|
|
910
|
+
await copyFile(srcPost, destPost);
|
|
911
|
+
await copyFile(srcShared, destShared);
|
|
912
|
+
const hookScriptMode = 493;
|
|
913
|
+
await chmod(destPre, hookScriptMode);
|
|
914
|
+
await chmod(destPost, hookScriptMode);
|
|
915
|
+
await chmod(destShared, hookScriptMode);
|
|
916
|
+
const hooksDir = getClineGlobalHooksDir();
|
|
917
|
+
await mkdir(hooksDir, { recursive: true });
|
|
918
|
+
const preWrapper = join(hooksDir, "PreToolUse");
|
|
919
|
+
const postWrapper = join(hooksDir, "PostToolUse");
|
|
920
|
+
const preContent = `#!/usr/bin/env node
|
|
921
|
+
require(${JSON.stringify(destPre)});
|
|
922
|
+
`;
|
|
923
|
+
const postContent = `#!/usr/bin/env node
|
|
924
|
+
require(${JSON.stringify(destPost)});
|
|
925
|
+
`;
|
|
926
|
+
await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
|
|
927
|
+
await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
|
|
928
|
+
}
|
|
929
|
+
async function promptClineIntegrationMode(ask) {
|
|
930
|
+
process.stderr.write("\n" + style.bold("Cline integration") + "\n");
|
|
931
|
+
process.stderr.write(
|
|
932
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
|
|
933
|
+
);
|
|
934
|
+
process.stderr.write(
|
|
935
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
|
|
936
|
+
);
|
|
937
|
+
let choice = 0;
|
|
938
|
+
while (choice === 0) {
|
|
939
|
+
const input = await ask("Choose integration (1-2): ");
|
|
940
|
+
const num = parseInt(input.trim(), 10);
|
|
941
|
+
if (num === 1) choice = 1;
|
|
942
|
+
if (num === 2) choice = 2;
|
|
943
|
+
}
|
|
944
|
+
return choice === 1 ? "native" : "hosted";
|
|
945
|
+
}
|
|
946
|
+
function getGeminiCliHooksInstallDir() {
|
|
947
|
+
return join(homedir(), ".multicorn", "gemini-cli-hooks");
|
|
948
|
+
}
|
|
949
|
+
function getGeminiCliSettingsPath() {
|
|
950
|
+
return join(homedir(), ".gemini", "settings.json");
|
|
951
|
+
}
|
|
952
|
+
function geminiInnerHooksReferenceShield(inner, multicornName) {
|
|
953
|
+
if (!Array.isArray(inner)) return false;
|
|
954
|
+
for (const h of inner) {
|
|
955
|
+
if (typeof h !== "object" || h === null) continue;
|
|
956
|
+
const rec = h;
|
|
957
|
+
if (rec["name"] === multicornName) return true;
|
|
958
|
+
const cmd = rec["command"];
|
|
959
|
+
if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return true;
|
|
960
|
+
}
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
function geminiHookEventsReferenceShield(arr) {
|
|
964
|
+
if (!Array.isArray(arr)) return false;
|
|
965
|
+
for (const entry of arr) {
|
|
966
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
967
|
+
const hooks = entry["hooks"];
|
|
968
|
+
if (geminiInnerHooksReferenceShield(hooks, "multicorn-shield") || geminiInnerHooksReferenceShield(hooks, "multicorn-shield-log")) {
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
function geminiSettingsHasMulticornHooks(hooks) {
|
|
975
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return false;
|
|
976
|
+
const h = hooks;
|
|
977
|
+
return geminiHookEventsReferenceShield(h["BeforeTool"]) || geminiHookEventsReferenceShield(h["AfterTool"]);
|
|
978
|
+
}
|
|
979
|
+
function geminiFilterInnerHooks(inner) {
|
|
980
|
+
if (!Array.isArray(inner)) return [];
|
|
981
|
+
return inner.filter((h) => {
|
|
982
|
+
if (typeof h !== "object" || h === null) return true;
|
|
983
|
+
const rec = h;
|
|
984
|
+
if (rec["name"] === "multicorn-shield" || rec["name"] === "multicorn-shield-log") return false;
|
|
985
|
+
const cmd = rec["command"];
|
|
986
|
+
if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return false;
|
|
987
|
+
return true;
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
function geminiStripMatcherGroups(arr) {
|
|
991
|
+
if (!Array.isArray(arr)) return [];
|
|
992
|
+
const out = [];
|
|
993
|
+
for (const entry of arr) {
|
|
994
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
995
|
+
const e = entry;
|
|
996
|
+
const filtered = geminiFilterInnerHooks(e["hooks"]);
|
|
997
|
+
if (filtered.length > 0) {
|
|
998
|
+
out.push({ ...e, hooks: filtered });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return out;
|
|
1002
|
+
}
|
|
1003
|
+
function geminiStripMulticornHookEntries(hooks) {
|
|
1004
|
+
const out = { ...hooks };
|
|
1005
|
+
out["BeforeTool"] = geminiStripMatcherGroups(out["BeforeTool"]);
|
|
1006
|
+
out["AfterTool"] = geminiStripMatcherGroups(out["AfterTool"]);
|
|
1007
|
+
return out;
|
|
1008
|
+
}
|
|
1009
|
+
async function installGeminiCliNativeHooks(ask) {
|
|
1010
|
+
const root = multicornShieldPackageRoot();
|
|
1011
|
+
const srcBefore = join(root, "plugins", "gemini-cli", "hooks", "scripts", "before-tool.cjs");
|
|
1012
|
+
const srcAfter = join(root, "plugins", "gemini-cli", "hooks", "scripts", "after-tool.cjs");
|
|
1013
|
+
const srcShared = join(root, "plugins", "gemini-cli", "hooks", "scripts", "shared.cjs");
|
|
1014
|
+
if (!existsSync(srcBefore) || !existsSync(srcAfter) || !existsSync(srcShared)) {
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
`Could not find Shield Gemini CLI hook scripts at ${srcBefore}. If you use npm, install the latest multicorn-shield package.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const geminiConfigDir = join(homedir(), ".gemini");
|
|
1020
|
+
if (!isExistingDirectory(geminiConfigDir)) {
|
|
1021
|
+
process.stderr.write(
|
|
1022
|
+
style.yellow("\u26A0") + " Gemini CLI does not appear to be installed (~/.gemini/ not found).\n\n"
|
|
1023
|
+
);
|
|
1024
|
+
process.stderr.write("Install Gemini CLI first:\n");
|
|
1025
|
+
process.stderr.write(" " + style.cyan("npm install -g @google/gemini-cli") + "\n\n");
|
|
1026
|
+
process.stderr.write("Then run this wizard again:\n");
|
|
1027
|
+
process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
|
|
1028
|
+
throw new NativePluginPrerequisiteMissingError();
|
|
1029
|
+
}
|
|
1030
|
+
const installDir = getGeminiCliHooksInstallDir();
|
|
1031
|
+
await mkdir(installDir, { recursive: true });
|
|
1032
|
+
const destBefore = join(installDir, "before-tool.cjs");
|
|
1033
|
+
const destAfter = join(installDir, "after-tool.cjs");
|
|
1034
|
+
const destShared = join(installDir, "shared.cjs");
|
|
1035
|
+
await copyFile(srcBefore, destBefore);
|
|
1036
|
+
await copyFile(srcAfter, destAfter);
|
|
1037
|
+
await copyFile(srcShared, destShared);
|
|
1038
|
+
const mode = 493;
|
|
1039
|
+
await chmod(destBefore, mode);
|
|
1040
|
+
await chmod(destAfter, mode);
|
|
1041
|
+
await chmod(destShared, mode);
|
|
1042
|
+
const settingsPath = getGeminiCliSettingsPath();
|
|
1043
|
+
let existing = {};
|
|
1044
|
+
try {
|
|
1045
|
+
const rawText = await readFile(settingsPath, "utf8");
|
|
1046
|
+
const parsed = JSON.parse(rawText);
|
|
1047
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1048
|
+
existing = parsed;
|
|
1049
|
+
}
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
1052
|
+
existing = {};
|
|
1053
|
+
} else {
|
|
1054
|
+
process.stderr.write(
|
|
1055
|
+
style.yellow("\u26A0") + ` Could not parse ${settingsPath}. Create valid JSON or remove the file, then run init again.
|
|
1056
|
+
`
|
|
1057
|
+
);
|
|
1058
|
+
throw new Error(`Invalid Gemini CLI settings at ${settingsPath}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const hooksRaw = existing["hooks"];
|
|
1062
|
+
const hooksObj = typeof hooksRaw === "object" && hooksRaw !== null && !Array.isArray(hooksRaw) ? hooksRaw : {};
|
|
1063
|
+
if (geminiSettingsHasMulticornHooks(hooksObj)) {
|
|
1064
|
+
const answer = await ask(
|
|
1065
|
+
"Existing Multicorn Shield hooks were found in ~/.gemini/settings.json. Overwrite? (Y/n) "
|
|
1066
|
+
);
|
|
1067
|
+
if (answer.trim().toLowerCase() === "n") {
|
|
1068
|
+
throw new Error("Installation cancelled: existing Shield hooks left unchanged.");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const cleaned = geminiStripMulticornHookEntries({ ...hooksObj });
|
|
1072
|
+
const beforeArr = Array.isArray(cleaned["BeforeTool"]) ? [...cleaned["BeforeTool"]] : [];
|
|
1073
|
+
const afterArr = Array.isArray(cleaned["AfterTool"]) ? [...cleaned["AfterTool"]] : [];
|
|
1074
|
+
const beforeCmd = `node ${destBefore}`;
|
|
1075
|
+
const afterCmd = `node ${destAfter}`;
|
|
1076
|
+
beforeArr.push({
|
|
1077
|
+
matcher: ".*",
|
|
1078
|
+
hooks: [
|
|
1079
|
+
{
|
|
1080
|
+
type: "command",
|
|
1081
|
+
name: "multicorn-shield",
|
|
1082
|
+
command: beforeCmd,
|
|
1083
|
+
timeout: 6e4
|
|
1084
|
+
}
|
|
1085
|
+
]
|
|
1086
|
+
});
|
|
1087
|
+
afterArr.push({
|
|
1088
|
+
matcher: ".*",
|
|
720
1089
|
hooks: [
|
|
721
1090
|
{
|
|
722
1091
|
type: "command",
|
|
@@ -864,19 +1233,6 @@ function isPlatformDetectedForMenu(slug) {
|
|
|
864
1233
|
return Promise.resolve(false);
|
|
865
1234
|
}
|
|
866
1235
|
}
|
|
867
|
-
var DEFAULT_AGENT_NAMES = {
|
|
868
|
-
openclaw: "my-openclaw-agent",
|
|
869
|
-
"claude-code": "my-claude-code-agent",
|
|
870
|
-
cursor: "my-cursor-agent",
|
|
871
|
-
windsurf: "my-windsurf-agent",
|
|
872
|
-
cline: "my-cline-agent",
|
|
873
|
-
"claude-desktop": "my-claude-desktop-agent",
|
|
874
|
-
"gemini-cli": "my-gemini-cli-agent",
|
|
875
|
-
"kilo-code": "my-kilo-code-agent",
|
|
876
|
-
"github-copilot": "my-github-copilot-agent",
|
|
877
|
-
"continue-dev": "my-continue-agent",
|
|
878
|
-
goose: "my-goose-agent"
|
|
879
|
-
};
|
|
880
1236
|
async function promptPlatformSelection(ask) {
|
|
881
1237
|
process.stderr.write(
|
|
882
1238
|
"\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n\n"
|
|
@@ -937,8 +1293,79 @@ async function promptWindsurfIntegrationMode(ask) {
|
|
|
937
1293
|
}
|
|
938
1294
|
return choice === 1 ? "native" : "hosted";
|
|
939
1295
|
}
|
|
1296
|
+
async function arrowSelect(options, ask, fallbackLabel) {
|
|
1297
|
+
const canRaw = process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
|
|
1298
|
+
if (!canRaw) {
|
|
1299
|
+
for (let i = 0; i < options.length; i++) {
|
|
1300
|
+
const optLine = options.at(i) ?? "";
|
|
1301
|
+
process.stderr.write(` ${style.violet(String(i + 1))}. ${optLine}
|
|
1302
|
+
`);
|
|
1303
|
+
}
|
|
1304
|
+
const label = fallbackLabel ?? "Choose";
|
|
1305
|
+
let sel = -1;
|
|
1306
|
+
while (sel < 0) {
|
|
1307
|
+
const input = await ask(`${label} (1-${String(options.length)}): `);
|
|
1308
|
+
const n = parseInt(input.trim(), 10);
|
|
1309
|
+
if (n >= 1 && n <= options.length) sel = n - 1;
|
|
1310
|
+
}
|
|
1311
|
+
return sel;
|
|
1312
|
+
}
|
|
1313
|
+
let idx = 0;
|
|
1314
|
+
function render() {
|
|
1315
|
+
for (let i = 0; i < options.length; i++) {
|
|
1316
|
+
const opt = options.at(i);
|
|
1317
|
+
if (opt === void 0) continue;
|
|
1318
|
+
const prefix = i === idx ? style.violet("\u276F") : " ";
|
|
1319
|
+
const label = i === idx ? style.cyan(opt) : opt;
|
|
1320
|
+
process.stderr.write(`${prefix} ${label}
|
|
1321
|
+
`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function clearLines() {
|
|
1325
|
+
for (let n = options.length; n > 0; n -= 1) {
|
|
1326
|
+
process.stderr.write("\x1B[1A\x1B[2K");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
process.stderr.write("\n");
|
|
1330
|
+
render();
|
|
1331
|
+
return new Promise((resolvePromise) => {
|
|
1332
|
+
const wasRaw = process.stdin.isRaw;
|
|
1333
|
+
process.stdin.setRawMode(true);
|
|
1334
|
+
process.stdin.resume();
|
|
1335
|
+
function onData(buf) {
|
|
1336
|
+
const s = buf.toString("utf8");
|
|
1337
|
+
if (s === "\x1B[A" || s === "k") {
|
|
1338
|
+
idx = (idx - 1 + options.length) % options.length;
|
|
1339
|
+
clearLines();
|
|
1340
|
+
render();
|
|
1341
|
+
} else if (s === "\x1B[B" || s === "j") {
|
|
1342
|
+
idx = (idx + 1) % options.length;
|
|
1343
|
+
clearLines();
|
|
1344
|
+
render();
|
|
1345
|
+
} else if (s === "\r" || s === "\n") {
|
|
1346
|
+
cleanup();
|
|
1347
|
+
clearLines();
|
|
1348
|
+
const chosen = options.at(idx);
|
|
1349
|
+
if (chosen !== void 0) {
|
|
1350
|
+
process.stderr.write(`${style.violet("\u276F")} ${style.cyan(chosen)}
|
|
1351
|
+
`);
|
|
1352
|
+
}
|
|
1353
|
+
resolvePromise(idx);
|
|
1354
|
+
} else if (s === "") {
|
|
1355
|
+
cleanup();
|
|
1356
|
+
process.exit(130);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function cleanup() {
|
|
1360
|
+
process.stdin.removeListener("data", onData);
|
|
1361
|
+
process.stdin.setRawMode(wasRaw);
|
|
1362
|
+
}
|
|
1363
|
+
process.stdin.on("data", onData);
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
940
1366
|
async function promptAgentName(ask, platform) {
|
|
941
|
-
const
|
|
1367
|
+
const dirPart = normalizeAgentName(basename(process.cwd()));
|
|
1368
|
+
const defaultAgentName = dirPart.length > 0 ? normalizeAgentName(`${dirPart}-${platform}`) || platform : normalizeAgentName(platform) || platform;
|
|
942
1369
|
let agentName = "";
|
|
943
1370
|
while (agentName.length === 0) {
|
|
944
1371
|
const input = await ask(
|
|
@@ -1208,6 +1635,18 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
1208
1635
|
process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
|
|
1209
1636
|
}
|
|
1210
1637
|
}
|
|
1638
|
+
function mergeAgentsForPlatform(localAgents, remoteAgents, selectedPlatform) {
|
|
1639
|
+
const localMatches = localAgents.filter((a) => a.platform === selectedPlatform);
|
|
1640
|
+
const seen = new Set(localMatches.map((a) => a.name));
|
|
1641
|
+
const out = localMatches.map((a) => ({ ...a }));
|
|
1642
|
+
for (const r of remoteAgents) {
|
|
1643
|
+
if (r.platform !== selectedPlatform) continue;
|
|
1644
|
+
if (seen.has(r.name)) continue;
|
|
1645
|
+
seen.add(r.name);
|
|
1646
|
+
out.push({ name: r.name, platform: selectedPlatform });
|
|
1647
|
+
}
|
|
1648
|
+
return out;
|
|
1649
|
+
}
|
|
1211
1650
|
var DEFAULT_SHIELD_API_BASE_URL = "https://api.multicorn.ai";
|
|
1212
1651
|
async function runInit(explicitBaseUrl) {
|
|
1213
1652
|
if (!process.stdin.isTTY) {
|
|
@@ -1217,8 +1656,8 @@ async function runInit(explicitBaseUrl) {
|
|
|
1217
1656
|
process.exit(1);
|
|
1218
1657
|
}
|
|
1219
1658
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1220
|
-
const ask = (question) => new Promise((
|
|
1221
|
-
rl.question(question,
|
|
1659
|
+
const ask = (question) => new Promise((resolve2) => {
|
|
1660
|
+
rl.question(question, resolve2);
|
|
1222
1661
|
});
|
|
1223
1662
|
process.stderr.write("\n" + BANNER + "\n");
|
|
1224
1663
|
process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
|
|
@@ -1294,6 +1733,8 @@ async function runInit(explicitBaseUrl) {
|
|
|
1294
1733
|
let configuring = true;
|
|
1295
1734
|
while (configuring) {
|
|
1296
1735
|
let postSaveNativeSkipNote = null;
|
|
1736
|
+
let removeAgentNameBeforeSave = void 0;
|
|
1737
|
+
const initWorkspacePath = resolve(process.cwd());
|
|
1297
1738
|
const selection = await promptPlatformSelection(ask);
|
|
1298
1739
|
const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
|
|
1299
1740
|
const selectedLabel = platformMenuLabelForSelection(selection);
|
|
@@ -1331,20 +1772,83 @@ async function runInit(explicitBaseUrl) {
|
|
|
1331
1772
|
}
|
|
1332
1773
|
continue;
|
|
1333
1774
|
}
|
|
1334
|
-
const
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1775
|
+
const remoteAccountAgents = await fetchRemoteAgentsSummaries(apiKey, resolvedBaseUrl);
|
|
1776
|
+
const agentsForPlatform = mergeAgentsForPlatform(
|
|
1777
|
+
currentAgents,
|
|
1778
|
+
remoteAccountAgents,
|
|
1779
|
+
selectedPlatform
|
|
1780
|
+
);
|
|
1781
|
+
const localForPlatformCount = currentAgents.filter(
|
|
1782
|
+
(a) => a.platform === selectedPlatform
|
|
1783
|
+
).length;
|
|
1784
|
+
const accountForPlatformCount = remoteAccountAgents.filter(
|
|
1785
|
+
(r) => r.platform === selectedPlatform
|
|
1786
|
+
).length;
|
|
1787
|
+
const savedSummary = currentAgents.length === 0 ? "none on disk" : currentAgents.map((a) => `${a.name} (${a.platform})`).join(", ");
|
|
1788
|
+
process.stderr.write(
|
|
1789
|
+
style.dim(
|
|
1790
|
+
`[shield init] Menu option ${String(selection)} -> platform slug "${selectedPlatform}". ${String(agentsForPlatform.length)} agent(s) for this platform (local file: ${String(localForPlatformCount)}, account API: ${String(accountForPlatformCount)}). On-disk entries: ${savedSummary}.`
|
|
1791
|
+
) + "\n"
|
|
1792
|
+
);
|
|
1793
|
+
if (agentsForPlatform.length > 0) {
|
|
1794
|
+
const exactForWorkspace = agentsForPlatform.find(
|
|
1795
|
+
(a) => typeof a.workspacePath === "string" && a.workspacePath.length > 0 && resolve(a.workspacePath) === initWorkspacePath
|
|
1340
1796
|
);
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1797
|
+
if (exactForWorkspace !== void 0) {
|
|
1798
|
+
process.stderr.write(
|
|
1799
|
+
`
|
|
1800
|
+
This workspace already has a ${selectedLabel} agent registered (${style.cyan(
|
|
1801
|
+
exactForWorkspace.name
|
|
1802
|
+
)}).
|
|
1803
|
+
`
|
|
1804
|
+
);
|
|
1805
|
+
process.stderr.write(
|
|
1806
|
+
style.dim(
|
|
1807
|
+
"Replace updates this directory's saved agent. (n) returns to platform selection \u2014 the wizard keeps running."
|
|
1808
|
+
) + "\n"
|
|
1809
|
+
);
|
|
1810
|
+
const replace = await ask("Replace it? (Y/n) ");
|
|
1811
|
+
if (replace.trim().toLowerCase() === "n") {
|
|
1812
|
+
process.stderr.write(style.dim("Skipping. Returning to platform selection.") + "\n");
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
removeAgentNameBeforeSave = exactForWorkspace.name;
|
|
1816
|
+
} else {
|
|
1817
|
+
process.stderr.write(
|
|
1818
|
+
`
|
|
1819
|
+
You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLabel}:
|
|
1820
|
+
`
|
|
1821
|
+
);
|
|
1822
|
+
for (const a of agentsForPlatform) {
|
|
1823
|
+
const wsHint = typeof a.workspacePath === "string" && a.workspacePath.length > 0 ? ` ${style.dim(a.workspacePath)}` : "";
|
|
1824
|
+
process.stderr.write(` ${style.dim("\u2022")} ${style.cyan(a.name)}${wsHint}
|
|
1825
|
+
`);
|
|
1826
|
+
}
|
|
1827
|
+
process.stderr.write("\n" + style.bold("What would you like to do?") + "\n");
|
|
1828
|
+
const actionIdx = await arrowSelect(
|
|
1829
|
+
[
|
|
1830
|
+
"Add a new agent alongside these",
|
|
1831
|
+
"Replace an existing agent",
|
|
1832
|
+
"Skip \u2014 choose a different platform"
|
|
1833
|
+
],
|
|
1834
|
+
ask,
|
|
1835
|
+
"Action"
|
|
1836
|
+
);
|
|
1837
|
+
if (actionIdx === 2) {
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
if (actionIdx === 1) {
|
|
1841
|
+
process.stderr.write("\n" + style.bold("Which agent to replace?") + "\n");
|
|
1842
|
+
const replaceIdx = await arrowSelect(
|
|
1843
|
+
agentsForPlatform.map((a) => a.name),
|
|
1844
|
+
ask,
|
|
1845
|
+
"Agent"
|
|
1846
|
+
);
|
|
1847
|
+
const victim = agentsForPlatform[replaceIdx];
|
|
1848
|
+
if (victim !== void 0) {
|
|
1849
|
+
removeAgentNameBeforeSave = victim.name;
|
|
1850
|
+
}
|
|
1346
1851
|
}
|
|
1347
|
-
continue;
|
|
1348
1852
|
}
|
|
1349
1853
|
}
|
|
1350
1854
|
const prereqEntry = INIT_WIZARD_PLATFORM_REGISTRY.find((e) => e.slug === selectedPlatform);
|
|
@@ -1756,8 +2260,14 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1756
2260
|
}
|
|
1757
2261
|
}
|
|
1758
2262
|
if (setupSucceeded) {
|
|
1759
|
-
|
|
1760
|
-
|
|
2263
|
+
if (removeAgentNameBeforeSave !== void 0) {
|
|
2264
|
+
currentAgents = currentAgents.filter((a) => a.name !== removeAgentNameBeforeSave);
|
|
2265
|
+
}
|
|
2266
|
+
currentAgents.push({
|
|
2267
|
+
name: agentName,
|
|
2268
|
+
platform: selectedPlatform,
|
|
2269
|
+
workspacePath: initWorkspacePath
|
|
2270
|
+
});
|
|
1761
2271
|
const raw = existing !== null ? { ...existing } : {};
|
|
1762
2272
|
raw["apiKey"] = apiKey;
|
|
1763
2273
|
raw["baseUrl"] = resolvedBaseUrl;
|
|
@@ -1807,848 +2317,548 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1807
2317
|
);
|
|
1808
2318
|
}
|
|
1809
2319
|
if (configuredPlatforms.has("claude-code")) {
|
|
1810
|
-
blocks.push(
|
|
1811
|
-
"\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"
|
|
1812
|
-
);
|
|
1813
|
-
}
|
|
1814
|
-
if (configuredPlatforms.has("claude-desktop")) {
|
|
1815
|
-
blocks.push(
|
|
1816
|
-
"\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
|
|
1817
|
-
);
|
|
1818
|
-
}
|
|
1819
|
-
if (configuredPlatforms.has("cursor")) {
|
|
1820
|
-
blocks.push(
|
|
1821
|
-
"\n" + style.bold("To complete your Cursor setup:") + "\n 1. If you don't have Cursor yet, download it from " + style.cyan("https://cursor.com/downloads") + "\n 2. Open " + style.cyan("~/.cursor/mcp.json") + " and paste the config snippet shown above\n 3. Restart Cursor (or launch it for the first time) to load the new MCP server\n"
|
|
1822
|
-
);
|
|
1823
|
-
}
|
|
1824
|
-
if (configuredPlatforms.has("kilo-code")) {
|
|
1825
|
-
blocks.push(
|
|
1826
|
-
"\n" + style.bold("To complete your Kilo Code setup:") + "\n 1. Save the snippet to " + style.cyan(".kilocode/mcp.json") + " in your project root, or under the mcp key in " + style.cyan("kilo.jsonc") + "\n 2. Run your next task in Kilo Code so it picks up the MCP server\n"
|
|
1827
|
-
);
|
|
1828
|
-
}
|
|
1829
|
-
if (configuredPlatforms.has("github-copilot")) {
|
|
1830
|
-
blocks.push(
|
|
1831
|
-
"\n" + style.bold("GitHub Copilot MCP:") + "\n 1. Open VS Code Command Palette: Preferences: Open User Settings (JSON)\n 2. Merge the snippet under the " + style.cyan("mcp") + " key and save\n 3. Use Copilot Agent mode and verify the MCP server connects\n"
|
|
1832
|
-
);
|
|
1833
|
-
}
|
|
1834
|
-
if (configuredPlatforms.has("continue-dev")) {
|
|
1835
|
-
blocks.push(
|
|
1836
|
-
"\n" + style.bold("Continue MCP:") + "\n 1. If you don't have Continue yet, install from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n 2. Save JSON as " + style.cyan(".continue/mcpServers/shield.json") + " in your workspace, or add to " + style.cyan("~/.continue/config.yaml") + "\n 3. Reload VS Code and open Continue agent mode\n"
|
|
1837
|
-
);
|
|
1838
|
-
}
|
|
1839
|
-
if (configuredPlatforms.has("goose")) {
|
|
1840
|
-
blocks.push(
|
|
1841
|
-
"\n" + style.bold("Goose MCP extension:") + "\n 1. Edit " + style.cyan("~/.config/goose/config.yaml") + " (or use goose configure)\n 2. Restart Goose CLI or Desktop\n"
|
|
1842
|
-
);
|
|
1843
|
-
}
|
|
1844
|
-
const windsurfNativeConfigured = configuredAgents.some(
|
|
1845
|
-
(a) => a.platform === "windsurf" && a.windsurfIntegration === "native"
|
|
1846
|
-
);
|
|
1847
|
-
const windsurfHostedConfigured = configuredAgents.some(
|
|
1848
|
-
(a) => a.platform === "windsurf" && a.windsurfIntegration === "hosted"
|
|
1849
|
-
);
|
|
1850
|
-
if (windsurfNativeConfigured) {
|
|
1851
|
-
blocks.push(
|
|
1852
|
-
"\n" + style.bold("To complete native Windsurf (Shield) setup:") + "\n 1. Hook scripts: " + style.cyan(getWindsurfHooksInstallDir()) + "\n 2. Hooks config: " + style.cyan(getWindsurfCascadeHooksJsonPath()) + "\n 3. Restart Windsurf (quit fully, then reopen)\n"
|
|
1853
|
-
);
|
|
1854
|
-
}
|
|
1855
|
-
if (windsurfHostedConfigured) {
|
|
1856
|
-
blocks.push(
|
|
1857
|
-
"\n" + style.bold("To complete your Windsurf hosted-proxy setup:") + "\n 1. If you don't have Windsurf yet, download it from " + style.cyan("https://windsurf.com/download") + "\n 2. Open " + style.cyan("~/.codeium/windsurf/mcp_config.json") + " and paste the config snippet shown above\n 3. Restart Windsurf (or launch it for the first time) to load the new MCP server\n"
|
|
1858
|
-
);
|
|
1859
|
-
}
|
|
1860
|
-
const clineNativeConfigured = configuredAgents.some(
|
|
1861
|
-
(a) => a.platform === "cline" && a.clineIntegration === "native"
|
|
1862
|
-
);
|
|
1863
|
-
const clineHostedConfigured = configuredAgents.some(
|
|
1864
|
-
(a) => a.platform === "cline" && a.clineIntegration === "hosted"
|
|
1865
|
-
);
|
|
1866
|
-
if (clineNativeConfigured) {
|
|
1867
|
-
blocks.push(
|
|
1868
|
-
"\n" + style.bold("To complete native Cline (Shield) setup:") + "\n 1. Enable Hooks in Cline: open VS Code, click the Cline sidebar icon, click the gear icon,\n scroll down to the Advanced section, and toggle Hooks on.\n 2. Reload the VS Code window (Cmd+Shift+P > Reload Window)\n 3. Trigger any tool call to verify Shield is intercepting\n"
|
|
1869
|
-
);
|
|
1870
|
-
}
|
|
1871
|
-
if (clineHostedConfigured) {
|
|
1872
|
-
blocks.push(
|
|
1873
|
-
"\n" + style.bold("To complete your Cline hosted-proxy setup:") + "\n 1. If you don't have Cline yet, install it from the VS Code marketplace\n 2. Open your Cline MCP settings file and paste the config snippet shown above\n 3. Restart Cline or reload the VS Code window\n"
|
|
1874
|
-
);
|
|
1875
|
-
}
|
|
1876
|
-
const geminiCliNativeConfigured = configuredAgents.some(
|
|
1877
|
-
(a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "native"
|
|
1878
|
-
);
|
|
1879
|
-
const geminiCliHostedConfigured = configuredAgents.some(
|
|
1880
|
-
(a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "hosted"
|
|
1881
|
-
);
|
|
1882
|
-
if (geminiCliNativeConfigured) {
|
|
1883
|
-
blocks.push(
|
|
1884
|
-
"\n" + style.bold("Gemini CLI native hooks:") + "\n Your Gemini CLI hooks are installed. Restart Gemini CLI to activate Shield governance.\n"
|
|
1885
|
-
);
|
|
1886
|
-
}
|
|
1887
|
-
if (geminiCliHostedConfigured) {
|
|
1888
|
-
blocks.push(
|
|
1889
|
-
"\n" + style.bold("To complete your Gemini CLI setup:") + "\n 1. Open " + style.cyan("~/.gemini/settings.json") + "\n 2. Paste the config snippet shown above\n 3. Restart Gemini CLI, then run /mcp to verify\n"
|
|
1890
|
-
);
|
|
1891
|
-
}
|
|
1892
|
-
if (blocks.length > 0) {
|
|
1893
|
-
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
1894
|
-
process.stderr.write(blocks.join("") + "\n");
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
return lastConfig;
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
// src/types/index.ts
|
|
1901
|
-
var PERMISSION_LEVELS = {
|
|
1902
|
-
Read: "read",
|
|
1903
|
-
Write: "write",
|
|
1904
|
-
Execute: "execute",
|
|
1905
|
-
Publish: "publish",
|
|
1906
|
-
Create: "create"
|
|
1907
|
-
};
|
|
1908
|
-
|
|
1909
|
-
// src/scopes/scope-parser.ts
|
|
1910
|
-
var VALID_PERMISSION_LEVELS = new Set(Object.values(PERMISSION_LEVELS));
|
|
1911
|
-
[...VALID_PERMISSION_LEVELS].join(", ");
|
|
1912
|
-
function formatScope(scope) {
|
|
1913
|
-
return `${scope.permissionLevel}:${scope.service}`;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
// src/scopes/scope-validator.ts
|
|
1917
|
-
function validateScopeAccess(grantedScopes, requested) {
|
|
1918
|
-
const isGranted = grantedScopes.some(
|
|
1919
|
-
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
1920
|
-
);
|
|
1921
|
-
if (isGranted) {
|
|
1922
|
-
return { allowed: true };
|
|
1923
|
-
}
|
|
1924
|
-
const serviceScopes = grantedScopes.filter((g) => g.service === requested.service);
|
|
1925
|
-
if (serviceScopes.length > 0) {
|
|
1926
|
-
const grantedLevels = serviceScopes.map((g) => `"${g.permissionLevel}"`).join(", ");
|
|
1927
|
-
return {
|
|
1928
|
-
allowed: false,
|
|
1929
|
-
reason: `Permission "${requested.permissionLevel}" is not granted for service "${requested.service}". Currently granted permission level(s): ${grantedLevels}. Requested scope "${formatScope(requested)}" requires explicit consent.`
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
return {
|
|
1933
|
-
allowed: false,
|
|
1934
|
-
reason: `No permissions granted for service "${requested.service}". The agent has not been authorised to access this service. Request scope "${formatScope(requested)}" via the consent screen.`
|
|
1935
|
-
};
|
|
1936
|
-
}
|
|
1937
|
-
function hasScope(grantedScopes, requested) {
|
|
1938
|
-
return grantedScopes.some(
|
|
1939
|
-
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
1940
|
-
);
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
// src/logger/action-logger.ts
|
|
1944
|
-
function createActionLogger(config) {
|
|
1945
|
-
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
1946
|
-
throw new Error(
|
|
1947
|
-
"[ActionLogger] API key is required. Provide it via the 'apiKey' config option."
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
const baseUrl = config.baseUrl ?? "https://api.multicorn.ai";
|
|
1951
|
-
const timeout = config.timeout ?? 5e3;
|
|
1952
|
-
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
|
|
1953
|
-
throw new Error(
|
|
1954
|
-
`[ActionLogger] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
|
|
1955
|
-
);
|
|
1956
|
-
}
|
|
1957
|
-
const endpoint = `${baseUrl}/api/v1/actions`;
|
|
1958
|
-
const batchEnabled = config.batchMode?.enabled ?? false;
|
|
1959
|
-
const maxBatchSize = config.batchMode?.maxSize ?? 10;
|
|
1960
|
-
const flushInterval = config.batchMode?.flushIntervalMs ?? 5e3;
|
|
1961
|
-
const queue = [];
|
|
1962
|
-
let flushTimer;
|
|
1963
|
-
let isShutdown = false;
|
|
1964
|
-
async function sendActions(actions) {
|
|
1965
|
-
if (actions.length === 0) return;
|
|
1966
|
-
const convertAction = (action) => ({
|
|
1967
|
-
agent: action.agent,
|
|
1968
|
-
service: action.service,
|
|
1969
|
-
actionType: action.actionType,
|
|
1970
|
-
status: action.status,
|
|
1971
|
-
...action.cost !== void 0 ? { cost: action.cost } : {},
|
|
1972
|
-
...action.metadata !== void 0 ? { metadata: action.metadata } : {}
|
|
1973
|
-
});
|
|
1974
|
-
const convertedActions = actions.map(convertAction);
|
|
1975
|
-
const payload = batchEnabled ? { actions: convertedActions } : convertedActions[0];
|
|
1976
|
-
let lastError;
|
|
1977
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1978
|
-
try {
|
|
1979
|
-
const controller = new AbortController();
|
|
1980
|
-
const timeoutId = setTimeout(() => {
|
|
1981
|
-
controller.abort();
|
|
1982
|
-
}, timeout);
|
|
1983
|
-
const response = await fetch(endpoint, {
|
|
1984
|
-
method: "POST",
|
|
1985
|
-
headers: {
|
|
1986
|
-
"Content-Type": "application/json",
|
|
1987
|
-
"X-Multicorn-Key": config.apiKey
|
|
1988
|
-
},
|
|
1989
|
-
body: JSON.stringify(payload),
|
|
1990
|
-
signal: controller.signal
|
|
1991
|
-
});
|
|
1992
|
-
clearTimeout(timeoutId);
|
|
1993
|
-
if (response.ok) {
|
|
1994
|
-
return;
|
|
1995
|
-
}
|
|
1996
|
-
if (response.status >= 400 && response.status < 500) {
|
|
1997
|
-
const body = await response.text().catch(() => "");
|
|
1998
|
-
throw new Error(
|
|
1999
|
-
`[ActionLogger] Client error (${String(response.status)}): ${response.statusText}. Response: ${body}. Check your API key and payload format.`
|
|
2000
|
-
);
|
|
2001
|
-
}
|
|
2002
|
-
if (response.status >= 500 && attempt === 0) {
|
|
2003
|
-
lastError = new Error(
|
|
2004
|
-
`[ActionLogger] Server error (${String(response.status)}): ${response.statusText}. Retrying once...`
|
|
2005
|
-
);
|
|
2006
|
-
await sleep(100 * Math.pow(2, attempt));
|
|
2007
|
-
continue;
|
|
2008
|
-
}
|
|
2009
|
-
throw new Error(
|
|
2010
|
-
`[ActionLogger] Server error (${String(response.status)}) after retry: ${response.statusText}. Multicorn API may be experiencing issues.`
|
|
2011
|
-
);
|
|
2012
|
-
} catch (error) {
|
|
2013
|
-
if (error instanceof Error) {
|
|
2014
|
-
if (error.name === "AbortError") {
|
|
2015
|
-
lastError = new Error(
|
|
2016
|
-
`[ActionLogger] Request timeout after ${String(timeout)}ms. Increase the 'timeout' config option or check your network connection.`
|
|
2017
|
-
);
|
|
2018
|
-
} else if (error.message.includes("Client error") || error.message.includes("Server error")) {
|
|
2019
|
-
lastError = error;
|
|
2020
|
-
} else {
|
|
2021
|
-
lastError = new Error(
|
|
2022
|
-
`[ActionLogger] Network error: ${error.message}. Check your network connection and API endpoint.`
|
|
2023
|
-
);
|
|
2024
|
-
}
|
|
2025
|
-
} else {
|
|
2026
|
-
lastError = new Error(`[ActionLogger] Unknown error: ${String(error)}`);
|
|
2027
|
-
}
|
|
2028
|
-
if (attempt === 0 && !lastError.message.includes("Client error")) {
|
|
2029
|
-
await sleep(100 * Math.pow(2, attempt));
|
|
2030
|
-
continue;
|
|
2031
|
-
}
|
|
2032
|
-
break;
|
|
2033
|
-
}
|
|
2320
|
+
blocks.push(
|
|
2321
|
+
"\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"
|
|
2322
|
+
);
|
|
2034
2323
|
}
|
|
2035
|
-
if (
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2324
|
+
if (configuredPlatforms.has("claude-desktop")) {
|
|
2325
|
+
blocks.push(
|
|
2326
|
+
"\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
|
|
2327
|
+
);
|
|
2039
2328
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
queue.length = 0;
|
|
2045
|
-
await sendActions(actions);
|
|
2046
|
-
}
|
|
2047
|
-
function startFlushTimer() {
|
|
2048
|
-
if (flushTimer !== void 0) return;
|
|
2049
|
-
flushTimer = setInterval(() => {
|
|
2050
|
-
flushQueue().catch(() => {
|
|
2051
|
-
});
|
|
2052
|
-
}, flushInterval);
|
|
2053
|
-
const timer = flushTimer;
|
|
2054
|
-
if (typeof timer.unref === "function") {
|
|
2055
|
-
timer.unref();
|
|
2329
|
+
if (configuredPlatforms.has("cursor")) {
|
|
2330
|
+
blocks.push(
|
|
2331
|
+
"\n" + style.bold("To complete your Cursor setup:") + "\n 1. If you don't have Cursor yet, download it from " + style.cyan("https://cursor.com/downloads") + "\n 2. Open " + style.cyan("~/.cursor/mcp.json") + " and paste the config snippet shown above\n 3. Restart Cursor (or launch it for the first time) to load the new MCP server\n"
|
|
2332
|
+
);
|
|
2056
2333
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
flushTimer = void 0;
|
|
2334
|
+
if (configuredPlatforms.has("kilo-code")) {
|
|
2335
|
+
blocks.push(
|
|
2336
|
+
"\n" + style.bold("To complete your Kilo Code setup:") + "\n 1. Save the snippet to " + style.cyan(".kilocode/mcp.json") + " in your project root, or under the mcp key in " + style.cyan("kilo.jsonc") + "\n 2. Run your next task in Kilo Code so it picks up the MCP server\n"
|
|
2337
|
+
);
|
|
2062
2338
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
return {
|
|
2068
|
-
logAction(action) {
|
|
2069
|
-
if (isShutdown) {
|
|
2070
|
-
throw new Error(
|
|
2071
|
-
"[ActionLogger] Cannot log action after shutdown. Create a new logger instance."
|
|
2072
|
-
);
|
|
2073
|
-
}
|
|
2074
|
-
if (action.agent.trim().length === 0) {
|
|
2075
|
-
throw new Error("[ActionLogger] Action must have a non-empty 'agent' field.");
|
|
2076
|
-
}
|
|
2077
|
-
if (action.service.trim().length === 0) {
|
|
2078
|
-
throw new Error("[ActionLogger] Action must have a non-empty 'service' field.");
|
|
2079
|
-
}
|
|
2080
|
-
if (action.actionType.trim().length === 0) {
|
|
2081
|
-
throw new Error("[ActionLogger] Action must have a non-empty 'actionType' field.");
|
|
2082
|
-
}
|
|
2083
|
-
if (action.status.trim().length === 0) {
|
|
2084
|
-
throw new Error("[ActionLogger] Action must have a non-empty 'status' field.");
|
|
2085
|
-
}
|
|
2086
|
-
if (batchEnabled) {
|
|
2087
|
-
queue.push({ payload: action, timestamp: Date.now() });
|
|
2088
|
-
if (queue.length >= maxBatchSize) {
|
|
2089
|
-
flushQueue().catch(() => {
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
} else {
|
|
2093
|
-
sendActions([action]).catch(() => {
|
|
2094
|
-
});
|
|
2095
|
-
}
|
|
2096
|
-
return Promise.resolve();
|
|
2097
|
-
},
|
|
2098
|
-
async flush() {
|
|
2099
|
-
if (!batchEnabled) return;
|
|
2100
|
-
await flushQueue();
|
|
2101
|
-
},
|
|
2102
|
-
async shutdown() {
|
|
2103
|
-
if (isShutdown) return;
|
|
2104
|
-
isShutdown = true;
|
|
2105
|
-
stopFlushTimer();
|
|
2106
|
-
if (batchEnabled) {
|
|
2107
|
-
await flushQueue();
|
|
2108
|
-
}
|
|
2339
|
+
if (configuredPlatforms.has("github-copilot")) {
|
|
2340
|
+
blocks.push(
|
|
2341
|
+
"\n" + style.bold("GitHub Copilot MCP:") + "\n 1. Open VS Code Command Palette: Preferences: Open User Settings (JSON)\n 2. Merge the snippet under the " + style.cyan("mcp") + " key and save\n 3. Use Copilot Agent mode and verify the MCP server connects\n"
|
|
2342
|
+
);
|
|
2109
2343
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
// src/spending/spending-checker.ts
|
|
2117
|
-
function createSpendingChecker(config) {
|
|
2118
|
-
validateLimits(config.limits);
|
|
2119
|
-
let dailySpendCents = 0;
|
|
2120
|
-
let monthlySpendCents = 0;
|
|
2121
|
-
let lastDailyReset = /* @__PURE__ */ new Date();
|
|
2122
|
-
let lastMonthlyReset = /* @__PURE__ */ new Date();
|
|
2123
|
-
function validateAmount(amountCents, context) {
|
|
2124
|
-
if (!Number.isInteger(amountCents)) {
|
|
2125
|
-
throw new Error(
|
|
2126
|
-
`[SpendingChecker] ${context} must be an integer (cents). Received: ${String(amountCents)}. Convert dollars to cents by multiplying by 100.`
|
|
2344
|
+
if (configuredPlatforms.has("continue-dev")) {
|
|
2345
|
+
blocks.push(
|
|
2346
|
+
"\n" + style.bold("Continue MCP:") + "\n 1. If you don't have Continue yet, install from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n 2. Save JSON as " + style.cyan(".continue/mcpServers/shield.json") + " in your workspace, or add to " + style.cyan("~/.continue/config.yaml") + "\n 3. Reload VS Code and open Continue agent mode\n"
|
|
2127
2347
|
);
|
|
2128
2348
|
}
|
|
2129
|
-
if (
|
|
2130
|
-
|
|
2131
|
-
|
|
2349
|
+
if (configuredPlatforms.has("goose")) {
|
|
2350
|
+
blocks.push(
|
|
2351
|
+
"\n" + style.bold("Goose MCP extension:") + "\n 1. Edit " + style.cyan("~/.config/goose/config.yaml") + " (or use goose configure)\n 2. Restart Goose CLI or Desktop\n"
|
|
2132
2352
|
);
|
|
2133
2353
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
if (shouldResetDaily(lastDailyReset, now)) {
|
|
2145
|
-
dailySpendCents = 0;
|
|
2146
|
-
lastDailyReset = now;
|
|
2354
|
+
const windsurfNativeConfigured = configuredAgents.some(
|
|
2355
|
+
(a) => a.platform === "windsurf" && a.windsurfIntegration === "native"
|
|
2356
|
+
);
|
|
2357
|
+
const windsurfHostedConfigured = configuredAgents.some(
|
|
2358
|
+
(a) => a.platform === "windsurf" && a.windsurfIntegration === "hosted"
|
|
2359
|
+
);
|
|
2360
|
+
if (windsurfNativeConfigured) {
|
|
2361
|
+
blocks.push(
|
|
2362
|
+
"\n" + style.bold("To complete native Windsurf (Shield) setup:") + "\n 1. Hook scripts: " + style.cyan(getWindsurfHooksInstallDir()) + "\n 2. Hooks config: " + style.cyan(getWindsurfCascadeHooksJsonPath()) + "\n 3. Restart Windsurf (quit fully, then reopen)\n"
|
|
2363
|
+
);
|
|
2147
2364
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
monthlySpendCents = 0;
|
|
2153
|
-
lastMonthlyReset = now;
|
|
2365
|
+
if (windsurfHostedConfigured) {
|
|
2366
|
+
blocks.push(
|
|
2367
|
+
"\n" + style.bold("To complete your Windsurf hosted-proxy setup:") + "\n 1. If you don't have Windsurf yet, download it from " + style.cyan("https://windsurf.com/download") + "\n 2. Open " + style.cyan("~/.codeium/windsurf/mcp_config.json") + " and paste the config snippet shown above\n 3. Restart Windsurf (or launch it for the first time) to load the new MCP server\n"
|
|
2368
|
+
);
|
|
2154
2369
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
daily: Math.max(0, config.limits.perDay - dailySpendCents),
|
|
2166
|
-
monthly: Math.max(0, config.limits.perMonth - monthlySpendCents)
|
|
2167
|
-
};
|
|
2168
|
-
}
|
|
2169
|
-
return {
|
|
2170
|
-
checkSpend(amountCents) {
|
|
2171
|
-
validateAmount(amountCents, "Spend amount");
|
|
2172
|
-
checkAndResetDaily();
|
|
2173
|
-
checkAndResetMonthly();
|
|
2174
|
-
if (amountCents > config.limits.perTransaction) {
|
|
2175
|
-
return {
|
|
2176
|
-
allowed: false,
|
|
2177
|
-
reason: `Action blocked: ${formatCents(amountCents)} exceeds per-transaction limit of ${formatCents(config.limits.perTransaction)}`,
|
|
2178
|
-
remainingBudget: calculateRemainingBudget()
|
|
2179
|
-
};
|
|
2180
|
-
}
|
|
2181
|
-
const projectedDaily = dailySpendCents + amountCents;
|
|
2182
|
-
if (projectedDaily > config.limits.perDay) {
|
|
2183
|
-
return {
|
|
2184
|
-
allowed: false,
|
|
2185
|
-
reason: `Action blocked: ${formatCents(amountCents)} would exceed per-day limit. Current spend today: ${formatCents(dailySpendCents)}, limit: ${formatCents(config.limits.perDay)}`,
|
|
2186
|
-
remainingBudget: calculateRemainingBudget()
|
|
2187
|
-
};
|
|
2188
|
-
}
|
|
2189
|
-
const projectedMonthly = monthlySpendCents + amountCents;
|
|
2190
|
-
if (projectedMonthly > config.limits.perMonth) {
|
|
2191
|
-
return {
|
|
2192
|
-
allowed: false,
|
|
2193
|
-
reason: `Action blocked: ${formatCents(amountCents)} would exceed per-month limit. Current spend this month: ${formatCents(monthlySpendCents)}, limit: ${formatCents(config.limits.perMonth)}`,
|
|
2194
|
-
remainingBudget: calculateRemainingBudget()
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
return {
|
|
2198
|
-
allowed: true,
|
|
2199
|
-
remainingBudget: calculateRemainingBudget()
|
|
2200
|
-
};
|
|
2201
|
-
},
|
|
2202
|
-
recordSpend(amountCents) {
|
|
2203
|
-
validateAmount(amountCents, "Spend amount");
|
|
2204
|
-
checkAndResetDaily();
|
|
2205
|
-
checkAndResetMonthly();
|
|
2206
|
-
dailySpendCents += amountCents;
|
|
2207
|
-
monthlySpendCents += amountCents;
|
|
2208
|
-
},
|
|
2209
|
-
getCurrentSpend() {
|
|
2210
|
-
checkAndResetDaily();
|
|
2211
|
-
checkAndResetMonthly();
|
|
2212
|
-
return {
|
|
2213
|
-
daily: dailySpendCents,
|
|
2214
|
-
monthly: monthlySpendCents
|
|
2215
|
-
};
|
|
2216
|
-
},
|
|
2217
|
-
reset() {
|
|
2218
|
-
dailySpendCents = 0;
|
|
2219
|
-
monthlySpendCents = 0;
|
|
2220
|
-
lastDailyReset = /* @__PURE__ */ new Date();
|
|
2221
|
-
lastMonthlyReset = /* @__PURE__ */ new Date();
|
|
2370
|
+
const clineNativeConfigured = configuredAgents.some(
|
|
2371
|
+
(a) => a.platform === "cline" && a.clineIntegration === "native"
|
|
2372
|
+
);
|
|
2373
|
+
const clineHostedConfigured = configuredAgents.some(
|
|
2374
|
+
(a) => a.platform === "cline" && a.clineIntegration === "hosted"
|
|
2375
|
+
);
|
|
2376
|
+
if (clineNativeConfigured) {
|
|
2377
|
+
blocks.push(
|
|
2378
|
+
"\n" + style.bold("To complete native Cline (Shield) setup:") + "\n 1. Enable Hooks in Cline: open VS Code, click the Cline sidebar icon, click the gear icon,\n scroll down to the Advanced section, and toggle Hooks on.\n 2. Reload the VS Code window (Cmd+Shift+P > Reload Window)\n 3. Trigger any tool call to verify Shield is intercepting\n"
|
|
2379
|
+
);
|
|
2222
2380
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2381
|
+
if (clineHostedConfigured) {
|
|
2382
|
+
blocks.push(
|
|
2383
|
+
"\n" + style.bold("To complete your Cline hosted-proxy setup:") + "\n 1. If you don't have Cline yet, install it from the VS Code marketplace\n 2. Open your Cline MCP settings file and paste the config snippet shown above\n 3. Restart Cline or reload the VS Code window\n"
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
const geminiCliNativeConfigured = configuredAgents.some(
|
|
2387
|
+
(a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "native"
|
|
2388
|
+
);
|
|
2389
|
+
const geminiCliHostedConfigured = configuredAgents.some(
|
|
2390
|
+
(a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "hosted"
|
|
2391
|
+
);
|
|
2392
|
+
if (geminiCliNativeConfigured) {
|
|
2393
|
+
blocks.push(
|
|
2394
|
+
"\n" + style.bold("Gemini CLI native hooks:") + "\n Your Gemini CLI hooks are installed. Restart Gemini CLI to activate Shield governance.\n"
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
if (geminiCliHostedConfigured) {
|
|
2398
|
+
blocks.push(
|
|
2399
|
+
"\n" + style.bold("To complete your Gemini CLI setup:") + "\n 1. Open " + style.cyan("~/.gemini/settings.json") + "\n 2. Paste the config snippet shown above\n 3. Restart Gemini CLI, then run /mcp to verify\n"
|
|
2235
2400
|
);
|
|
2236
2401
|
}
|
|
2237
|
-
if (
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
);
|
|
2402
|
+
if (blocks.length > 0) {
|
|
2403
|
+
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
2404
|
+
process.stderr.write(blocks.join("") + "\n");
|
|
2241
2405
|
}
|
|
2242
2406
|
}
|
|
2407
|
+
return lastConfig;
|
|
2243
2408
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2409
|
+
|
|
2410
|
+
// src/types/index.ts
|
|
2411
|
+
var PERMISSION_LEVELS = {
|
|
2412
|
+
Read: "read",
|
|
2413
|
+
Write: "write",
|
|
2414
|
+
Execute: "execute",
|
|
2415
|
+
Publish: "publish",
|
|
2416
|
+
Create: "create"
|
|
2417
|
+
};
|
|
2418
|
+
|
|
2419
|
+
// src/scopes/scope-parser.ts
|
|
2420
|
+
var VALID_PERMISSION_LEVELS = new Set(Object.values(PERMISSION_LEVELS));
|
|
2421
|
+
[...VALID_PERMISSION_LEVELS].join(", ");
|
|
2422
|
+
function formatScope(scope) {
|
|
2423
|
+
return `${scope.permissionLevel}:${scope.service}`;
|
|
2246
2424
|
}
|
|
2247
2425
|
|
|
2248
|
-
// src/
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2426
|
+
// src/scopes/scope-validator.ts
|
|
2427
|
+
function validateScopeAccess(grantedScopes, requested) {
|
|
2428
|
+
const isGranted = grantedScopes.some(
|
|
2429
|
+
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
2430
|
+
);
|
|
2431
|
+
if (isGranted) {
|
|
2432
|
+
return { allowed: true };
|
|
2433
|
+
}
|
|
2434
|
+
const serviceScopes = grantedScopes.filter((g) => g.service === requested.service);
|
|
2435
|
+
if (serviceScopes.length > 0) {
|
|
2436
|
+
const grantedLevels = serviceScopes.map((g) => `"${g.permissionLevel}"`).join(", ");
|
|
2437
|
+
return {
|
|
2438
|
+
allowed: false,
|
|
2439
|
+
reason: `Permission "${requested.permissionLevel}" is not granted for service "${requested.service}". Currently granted permission level(s): ${grantedLevels}. Requested scope "${formatScope(requested)}" requires explicit consent.`
|
|
2440
|
+
};
|
|
2262
2441
|
}
|
|
2263
|
-
return isJsonRpcRequest(parsed) ? parsed : null;
|
|
2264
|
-
}
|
|
2265
|
-
function extractToolCallParams(request) {
|
|
2266
|
-
if (request.method !== "tools/call") return null;
|
|
2267
|
-
if (typeof request.params !== "object" || request.params === null) return null;
|
|
2268
|
-
const params = request.params;
|
|
2269
|
-
const name = params["name"];
|
|
2270
|
-
const args = params["arguments"];
|
|
2271
|
-
if (typeof name !== "string") return null;
|
|
2272
|
-
if (typeof args !== "object" || args === null) return null;
|
|
2273
|
-
return { name, arguments: args };
|
|
2274
|
-
}
|
|
2275
|
-
function buildBlockedResponse(id, service, permissionLevel, dashboardUrl) {
|
|
2276
|
-
const displayService = capitalize(service);
|
|
2277
|
-
const message = `Action blocked by Multicorn Shield: agent does not have ${permissionLevel} access to ${displayService}. Configure permissions at ${dashboardUrl}`;
|
|
2278
|
-
return {
|
|
2279
|
-
jsonrpc: "2.0",
|
|
2280
|
-
id,
|
|
2281
|
-
error: {
|
|
2282
|
-
code: BLOCKED_ERROR_CODE,
|
|
2283
|
-
message
|
|
2284
|
-
}
|
|
2285
|
-
};
|
|
2286
|
-
}
|
|
2287
|
-
function buildSpendingBlockedResponse(id, reason, dashboardUrl) {
|
|
2288
|
-
const message = `Action blocked by Multicorn Shield: ${reason}. Review spending limits at ${dashboardUrl}`;
|
|
2289
|
-
return {
|
|
2290
|
-
jsonrpc: "2.0",
|
|
2291
|
-
id,
|
|
2292
|
-
error: {
|
|
2293
|
-
code: SPENDING_BLOCKED_ERROR_CODE,
|
|
2294
|
-
message
|
|
2295
|
-
}
|
|
2296
|
-
};
|
|
2297
|
-
}
|
|
2298
|
-
function buildInternalErrorResponse(id) {
|
|
2299
|
-
const message = "Action blocked: Shield encountered an internal error and cannot verify permissions. Check proxy logs for details.";
|
|
2300
|
-
return {
|
|
2301
|
-
jsonrpc: "2.0",
|
|
2302
|
-
id,
|
|
2303
|
-
error: {
|
|
2304
|
-
code: INTERNAL_ERROR_CODE,
|
|
2305
|
-
message
|
|
2306
|
-
}
|
|
2307
|
-
};
|
|
2308
|
-
}
|
|
2309
|
-
function buildServiceUnreachableResponse(id, dashboardUrl) {
|
|
2310
|
-
const message = `Action blocked: Shield cannot verify permissions (service unreachable). Configure offline behaviour at ${dashboardUrl}`;
|
|
2311
|
-
return {
|
|
2312
|
-
jsonrpc: "2.0",
|
|
2313
|
-
id,
|
|
2314
|
-
error: {
|
|
2315
|
-
code: SERVICE_UNREACHABLE_ERROR_CODE,
|
|
2316
|
-
message
|
|
2317
|
-
}
|
|
2318
|
-
};
|
|
2319
|
-
}
|
|
2320
|
-
function buildAuthErrorResponse(id) {
|
|
2321
|
-
const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-shield init to reconfigure.";
|
|
2322
2442
|
return {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
error: {
|
|
2326
|
-
code: AUTH_ERROR_CODE,
|
|
2327
|
-
message
|
|
2328
|
-
}
|
|
2443
|
+
allowed: false,
|
|
2444
|
+
reason: `No permissions granted for service "${requested.service}". The agent has not been authorised to access this service. Request scope "${formatScope(requested)}" via the consent screen.`
|
|
2329
2445
|
};
|
|
2330
2446
|
}
|
|
2331
|
-
function
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
function extractActionFromToolName(toolName) {
|
|
2336
|
-
const idx = toolName.indexOf("_");
|
|
2337
|
-
return idx === -1 ? "call" : toolName.slice(idx + 1);
|
|
2338
|
-
}
|
|
2339
|
-
function isJsonRpcRequest(value) {
|
|
2340
|
-
if (typeof value !== "object" || value === null) return false;
|
|
2341
|
-
const obj = value;
|
|
2342
|
-
if (obj["jsonrpc"] !== "2.0") return false;
|
|
2343
|
-
if (typeof obj["method"] !== "string") return false;
|
|
2344
|
-
const id = obj["id"];
|
|
2345
|
-
const validId = id === null || id === void 0 || typeof id === "string" || typeof id === "number";
|
|
2346
|
-
return validId;
|
|
2347
|
-
}
|
|
2348
|
-
function capitalize(str) {
|
|
2349
|
-
if (str.length === 0) return str;
|
|
2350
|
-
const first = str[0];
|
|
2351
|
-
return first !== void 0 ? first.toUpperCase() + str.slice(1) : str;
|
|
2352
|
-
}
|
|
2353
|
-
var MULTICORN_DIR = join(homedir(), ".multicorn");
|
|
2354
|
-
var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
|
|
2355
|
-
var CACHE_META_PATH = join(MULTICORN_DIR, "cache-meta.json");
|
|
2356
|
-
function cacheKey(agentName, apiKey) {
|
|
2357
|
-
return createHash("sha256").update(`${agentName}:${apiKey}`).digest("hex").slice(0, 16);
|
|
2447
|
+
function hasScope(grantedScopes, requested) {
|
|
2448
|
+
return grantedScopes.some(
|
|
2449
|
+
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
2450
|
+
);
|
|
2358
2451
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
storedHash = meta.apiKeyHash;
|
|
2367
|
-
}
|
|
2368
|
-
} catch {
|
|
2452
|
+
|
|
2453
|
+
// src/logger/action-logger.ts
|
|
2454
|
+
function createActionLogger(config) {
|
|
2455
|
+
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
2456
|
+
throw new Error(
|
|
2457
|
+
"[ActionLogger] API key is required. Provide it via the 'apiKey' config option."
|
|
2458
|
+
);
|
|
2369
2459
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2460
|
+
const baseUrl = config.baseUrl ?? "https://api.multicorn.ai";
|
|
2461
|
+
const timeout = config.timeout ?? 5e3;
|
|
2462
|
+
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
|
|
2463
|
+
throw new Error(
|
|
2464
|
+
`[ActionLogger] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
const endpoint = `${baseUrl}/api/v1/actions`;
|
|
2468
|
+
const batchEnabled = config.batchMode?.enabled ?? false;
|
|
2469
|
+
const maxBatchSize = config.batchMode?.maxSize ?? 10;
|
|
2470
|
+
const flushInterval = config.batchMode?.flushIntervalMs ?? 5e3;
|
|
2471
|
+
const queue = [];
|
|
2472
|
+
let flushTimer;
|
|
2473
|
+
let isShutdown = false;
|
|
2474
|
+
async function sendActions(actions) {
|
|
2475
|
+
if (actions.length === 0) return;
|
|
2476
|
+
const convertAction = (action) => ({
|
|
2477
|
+
agent: action.agent,
|
|
2478
|
+
service: action.service,
|
|
2479
|
+
actionType: action.actionType,
|
|
2480
|
+
status: action.status,
|
|
2481
|
+
...action.cost !== void 0 ? { cost: action.cost } : {},
|
|
2482
|
+
...action.metadata !== void 0 ? { metadata: action.metadata } : {}
|
|
2483
|
+
});
|
|
2484
|
+
const convertedActions = actions.map(convertAction);
|
|
2485
|
+
const payload = batchEnabled ? { actions: convertedActions } : convertedActions[0];
|
|
2486
|
+
let lastError;
|
|
2487
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2488
|
+
try {
|
|
2489
|
+
const controller = new AbortController();
|
|
2490
|
+
const timeoutId = setTimeout(() => {
|
|
2491
|
+
controller.abort();
|
|
2492
|
+
}, timeout);
|
|
2493
|
+
const response = await fetch(endpoint, {
|
|
2494
|
+
method: "POST",
|
|
2495
|
+
headers: {
|
|
2496
|
+
"Content-Type": "application/json",
|
|
2497
|
+
"X-Multicorn-Key": config.apiKey
|
|
2498
|
+
},
|
|
2499
|
+
body: JSON.stringify(payload),
|
|
2500
|
+
signal: controller.signal
|
|
2501
|
+
});
|
|
2502
|
+
clearTimeout(timeoutId);
|
|
2503
|
+
if (response.ok) {
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
if (response.status >= 400 && response.status < 500) {
|
|
2507
|
+
const body = await response.text().catch(() => "");
|
|
2508
|
+
throw new Error(
|
|
2509
|
+
`[ActionLogger] Client error (${String(response.status)}): ${response.statusText}. Response: ${body}. Check your API key and payload format.`
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
if (response.status >= 500 && attempt === 0) {
|
|
2513
|
+
lastError = new Error(
|
|
2514
|
+
`[ActionLogger] Server error (${String(response.status)}): ${response.statusText}. Retrying once...`
|
|
2515
|
+
);
|
|
2516
|
+
await sleep2(100 * Math.pow(2, attempt));
|
|
2517
|
+
continue;
|
|
2518
|
+
}
|
|
2519
|
+
throw new Error(
|
|
2520
|
+
`[ActionLogger] Server error (${String(response.status)}) after retry: ${response.statusText}. Multicorn API may be experiencing issues.`
|
|
2521
|
+
);
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
if (error instanceof Error) {
|
|
2524
|
+
if (error.name === "AbortError") {
|
|
2525
|
+
lastError = new Error(
|
|
2526
|
+
`[ActionLogger] Request timeout after ${String(timeout)}ms. Increase the 'timeout' config option or check your network connection.`
|
|
2527
|
+
);
|
|
2528
|
+
} else if (error.message.includes("Client error") || error.message.includes("Server error")) {
|
|
2529
|
+
lastError = error;
|
|
2530
|
+
} else {
|
|
2531
|
+
lastError = new Error(
|
|
2532
|
+
`[ActionLogger] Network error: ${error.message}. Check your network connection and API endpoint.`
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
} else {
|
|
2536
|
+
lastError = new Error(`[ActionLogger] Unknown error: ${String(error)}`);
|
|
2537
|
+
}
|
|
2538
|
+
if (attempt === 0 && !lastError.message.includes("Client error")) {
|
|
2539
|
+
await sleep2(100 * Math.pow(2, attempt));
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
break;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
if (lastError) {
|
|
2546
|
+
if (config.onError) {
|
|
2547
|
+
config.onError(lastError);
|
|
2548
|
+
}
|
|
2374
2549
|
}
|
|
2375
2550
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
});
|
|
2551
|
+
async function flushQueue() {
|
|
2552
|
+
if (queue.length === 0) return;
|
|
2553
|
+
const actions = queue.map((item) => item.payload);
|
|
2554
|
+
queue.length = 0;
|
|
2555
|
+
await sendActions(actions);
|
|
2382
2556
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
const
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
return entry?.scopes ?? null;
|
|
2394
|
-
} catch {
|
|
2395
|
-
return null;
|
|
2557
|
+
function startFlushTimer() {
|
|
2558
|
+
if (flushTimer !== void 0) return;
|
|
2559
|
+
flushTimer = setInterval(() => {
|
|
2560
|
+
flushQueue().catch(() => {
|
|
2561
|
+
});
|
|
2562
|
+
}, flushInterval);
|
|
2563
|
+
const timer = flushTimer;
|
|
2564
|
+
if (typeof timer.unref === "function") {
|
|
2565
|
+
timer.unref();
|
|
2566
|
+
}
|
|
2396
2567
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
|
|
2403
|
-
let existing = {};
|
|
2404
|
-
try {
|
|
2405
|
-
const raw = await readFile(SCOPES_PATH, "utf8");
|
|
2406
|
-
const parsed = JSON.parse(raw);
|
|
2407
|
-
if (isScopesCacheFile(parsed)) existing = parsed;
|
|
2408
|
-
} catch {
|
|
2568
|
+
function stopFlushTimer() {
|
|
2569
|
+
if (flushTimer) {
|
|
2570
|
+
clearInterval(flushTimer);
|
|
2571
|
+
flushTimer = void 0;
|
|
2572
|
+
}
|
|
2409
2573
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2574
|
+
if (batchEnabled) {
|
|
2575
|
+
startFlushTimer();
|
|
2576
|
+
}
|
|
2577
|
+
return {
|
|
2578
|
+
logAction(action) {
|
|
2579
|
+
if (isShutdown) {
|
|
2580
|
+
throw new Error(
|
|
2581
|
+
"[ActionLogger] Cannot log action after shutdown. Create a new logger instance."
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
if (action.agent.trim().length === 0) {
|
|
2585
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'agent' field.");
|
|
2586
|
+
}
|
|
2587
|
+
if (action.service.trim().length === 0) {
|
|
2588
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'service' field.");
|
|
2589
|
+
}
|
|
2590
|
+
if (action.actionType.trim().length === 0) {
|
|
2591
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'actionType' field.");
|
|
2592
|
+
}
|
|
2593
|
+
if (action.status.trim().length === 0) {
|
|
2594
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'status' field.");
|
|
2595
|
+
}
|
|
2596
|
+
if (batchEnabled) {
|
|
2597
|
+
queue.push({ payload: action, timestamp: Date.now() });
|
|
2598
|
+
if (queue.length >= maxBatchSize) {
|
|
2599
|
+
flushQueue().catch(() => {
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
} else {
|
|
2603
|
+
sendActions([action]).catch(() => {
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
return Promise.resolve();
|
|
2607
|
+
},
|
|
2608
|
+
async flush() {
|
|
2609
|
+
if (!batchEnabled) return;
|
|
2610
|
+
await flushQueue();
|
|
2611
|
+
},
|
|
2612
|
+
async shutdown() {
|
|
2613
|
+
if (isShutdown) return;
|
|
2614
|
+
isShutdown = true;
|
|
2615
|
+
stopFlushTimer();
|
|
2616
|
+
if (batchEnabled) {
|
|
2617
|
+
await flushQueue();
|
|
2618
|
+
}
|
|
2416
2619
|
}
|
|
2417
2620
|
};
|
|
2418
|
-
await writeFile(SCOPES_PATH, JSON.stringify(updated, null, 2) + "\n", {
|
|
2419
|
-
encoding: "utf8",
|
|
2420
|
-
mode: 384
|
|
2421
|
-
});
|
|
2422
2621
|
}
|
|
2423
|
-
function
|
|
2424
|
-
return
|
|
2622
|
+
function sleep2(ms) {
|
|
2623
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2425
2624
|
}
|
|
2426
2625
|
|
|
2427
|
-
// src/
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
url.hostname = "app.multicorn.ai";
|
|
2440
|
-
return url.toString();
|
|
2441
|
-
}
|
|
2442
|
-
if (url.hostname.includes("api")) {
|
|
2443
|
-
url.hostname = url.hostname.replace("api", "app");
|
|
2444
|
-
return url.toString();
|
|
2626
|
+
// src/spending/spending-checker.ts
|
|
2627
|
+
function createSpendingChecker(config) {
|
|
2628
|
+
validateLimits(config.limits);
|
|
2629
|
+
let dailySpendCents = 0;
|
|
2630
|
+
let monthlySpendCents = 0;
|
|
2631
|
+
let lastDailyReset = /* @__PURE__ */ new Date();
|
|
2632
|
+
let lastMonthlyReset = /* @__PURE__ */ new Date();
|
|
2633
|
+
function validateAmount(amountCents, context) {
|
|
2634
|
+
if (!Number.isInteger(amountCents)) {
|
|
2635
|
+
throw new Error(
|
|
2636
|
+
`[SpendingChecker] ${context} must be an integer (cents). Received: ${String(amountCents)}. Convert dollars to cents by multiplying by 100.`
|
|
2637
|
+
);
|
|
2445
2638
|
}
|
|
2446
|
-
if (
|
|
2447
|
-
|
|
2639
|
+
if (amountCents < 0) {
|
|
2640
|
+
throw new Error(
|
|
2641
|
+
`[SpendingChecker] ${context} must be non-negative. Received: ${String(amountCents)} cents.`
|
|
2642
|
+
);
|
|
2448
2643
|
}
|
|
2449
|
-
return "https://app.multicorn.ai";
|
|
2450
|
-
} catch {
|
|
2451
|
-
return "https://app.multicorn.ai";
|
|
2452
2644
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2645
|
+
function formatCents(cents) {
|
|
2646
|
+
const dollars = cents / 100;
|
|
2647
|
+
return `$${dollars.toLocaleString("en-US", {
|
|
2648
|
+
minimumFractionDigits: 2,
|
|
2649
|
+
maximumFractionDigits: 2
|
|
2650
|
+
})}`;
|
|
2459
2651
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
signal: AbortSignal.timeout(8e3)
|
|
2467
|
-
});
|
|
2468
|
-
} catch {
|
|
2469
|
-
return null;
|
|
2652
|
+
function checkAndResetDaily() {
|
|
2653
|
+
const now = /* @__PURE__ */ new Date();
|
|
2654
|
+
if (shouldResetDaily(lastDailyReset, now)) {
|
|
2655
|
+
dailySpendCents = 0;
|
|
2656
|
+
lastDailyReset = now;
|
|
2657
|
+
}
|
|
2470
2658
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2659
|
+
function checkAndResetMonthly() {
|
|
2660
|
+
const now = /* @__PURE__ */ new Date();
|
|
2661
|
+
if (shouldResetMonthly(lastMonthlyReset, now)) {
|
|
2662
|
+
monthlySpendCents = 0;
|
|
2663
|
+
lastMonthlyReset = now;
|
|
2474
2664
|
}
|
|
2475
|
-
return null;
|
|
2476
2665
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
body = await response.json();
|
|
2480
|
-
} catch {
|
|
2481
|
-
return null;
|
|
2666
|
+
function shouldResetDaily(lastReset, now) {
|
|
2667
|
+
return lastReset.getDate() !== now.getDate() || lastReset.getMonth() !== now.getMonth() || lastReset.getFullYear() !== now.getFullYear();
|
|
2482
2668
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2669
|
+
function shouldResetMonthly(lastReset, now) {
|
|
2670
|
+
return lastReset.getMonth() !== now.getMonth() || lastReset.getFullYear() !== now.getFullYear();
|
|
2671
|
+
}
|
|
2672
|
+
function calculateRemainingBudget() {
|
|
2673
|
+
return {
|
|
2674
|
+
transaction: config.limits.perTransaction,
|
|
2675
|
+
daily: Math.max(0, config.limits.perDay - dailySpendCents),
|
|
2676
|
+
monthly: Math.max(0, config.limits.perMonth - monthlySpendCents)
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
return {
|
|
2680
|
+
checkSpend(amountCents) {
|
|
2681
|
+
validateAmount(amountCents, "Spend amount");
|
|
2682
|
+
checkAndResetDaily();
|
|
2683
|
+
checkAndResetMonthly();
|
|
2684
|
+
if (amountCents > config.limits.perTransaction) {
|
|
2685
|
+
return {
|
|
2686
|
+
allowed: false,
|
|
2687
|
+
reason: `Action blocked: ${formatCents(amountCents)} exceeds per-transaction limit of ${formatCents(config.limits.perTransaction)}`,
|
|
2688
|
+
remainingBudget: calculateRemainingBudget()
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
const projectedDaily = dailySpendCents + amountCents;
|
|
2692
|
+
if (projectedDaily > config.limits.perDay) {
|
|
2693
|
+
return {
|
|
2694
|
+
allowed: false,
|
|
2695
|
+
reason: `Action blocked: ${formatCents(amountCents)} would exceed per-day limit. Current spend today: ${formatCents(dailySpendCents)}, limit: ${formatCents(config.limits.perDay)}`,
|
|
2696
|
+
remainingBudget: calculateRemainingBudget()
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
const projectedMonthly = monthlySpendCents + amountCents;
|
|
2700
|
+
if (projectedMonthly > config.limits.perMonth) {
|
|
2701
|
+
return {
|
|
2702
|
+
allowed: false,
|
|
2703
|
+
reason: `Action blocked: ${formatCents(amountCents)} would exceed per-month limit. Current spend this month: ${formatCents(monthlySpendCents)}, limit: ${formatCents(config.limits.perMonth)}`,
|
|
2704
|
+
remainingBudget: calculateRemainingBudget()
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
return {
|
|
2708
|
+
allowed: true,
|
|
2709
|
+
remainingBudget: calculateRemainingBudget()
|
|
2710
|
+
};
|
|
2711
|
+
},
|
|
2712
|
+
recordSpend(amountCents) {
|
|
2713
|
+
validateAmount(amountCents, "Spend amount");
|
|
2714
|
+
checkAndResetDaily();
|
|
2715
|
+
checkAndResetMonthly();
|
|
2716
|
+
dailySpendCents += amountCents;
|
|
2717
|
+
monthlySpendCents += amountCents;
|
|
2498
2718
|
},
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2719
|
+
getCurrentSpend() {
|
|
2720
|
+
checkAndResetDaily();
|
|
2721
|
+
checkAndResetMonthly();
|
|
2722
|
+
return {
|
|
2723
|
+
daily: dailySpendCents,
|
|
2724
|
+
monthly: monthlySpendCents
|
|
2725
|
+
};
|
|
2726
|
+
},
|
|
2727
|
+
reset() {
|
|
2728
|
+
dailySpendCents = 0;
|
|
2729
|
+
monthlySpendCents = 0;
|
|
2730
|
+
lastDailyReset = /* @__PURE__ */ new Date();
|
|
2731
|
+
lastMonthlyReset = /* @__PURE__ */ new Date();
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
function validateLimits(limits) {
|
|
2736
|
+
const checks = [
|
|
2737
|
+
{ value: limits.perTransaction, name: "perTransaction" },
|
|
2738
|
+
{ value: limits.perDay, name: "perDay" },
|
|
2739
|
+
{ value: limits.perMonth, name: "perMonth" }
|
|
2740
|
+
];
|
|
2741
|
+
for (const check of checks) {
|
|
2742
|
+
if (!Number.isInteger(check.value)) {
|
|
2743
|
+
throw new Error(
|
|
2744
|
+
`[SpendingChecker] Limit "${check.name}" must be an integer (cents). Received: ${String(check.value)}. All limits must be specified in integer cents.`
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
if (check.value < 0) {
|
|
2748
|
+
throw new Error(
|
|
2749
|
+
`[SpendingChecker] Limit "${check.name}" must be non-negative. Received: ${String(check.value)} cents.`
|
|
2506
2750
|
);
|
|
2507
2751
|
}
|
|
2508
|
-
throw new Error(
|
|
2509
|
-
`Failed to register agent "${agentName}": service returned ${String(response.status)}.`
|
|
2510
|
-
);
|
|
2511
|
-
}
|
|
2512
|
-
const body = await response.json();
|
|
2513
|
-
if (!isApiSuccessResponse(body)) {
|
|
2514
|
-
throw new Error(`Failed to register agent "${agentName}": unexpected response format.`);
|
|
2515
|
-
}
|
|
2516
|
-
if (!isAgentSummaryShape(body.data)) {
|
|
2517
|
-
throw new Error(`Failed to register agent "${agentName}": response missing agent ID.`);
|
|
2518
2752
|
}
|
|
2519
|
-
return body.data.id;
|
|
2520
2753
|
}
|
|
2521
|
-
|
|
2522
|
-
|
|
2754
|
+
function dollarsToCents(dollars) {
|
|
2755
|
+
return Math.round(dollars * 100);
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
// src/proxy/interceptor.ts
|
|
2759
|
+
var BLOCKED_ERROR_CODE = -32e3;
|
|
2760
|
+
var SPENDING_BLOCKED_ERROR_CODE = -32001;
|
|
2761
|
+
var INTERNAL_ERROR_CODE = -32002;
|
|
2762
|
+
var SERVICE_UNREACHABLE_ERROR_CODE = -32003;
|
|
2763
|
+
var AUTH_ERROR_CODE = -32004;
|
|
2764
|
+
function parseJsonRpcLine(line) {
|
|
2765
|
+
const trimmed = line.trim();
|
|
2766
|
+
if (trimmed.length === 0) return null;
|
|
2767
|
+
let parsed;
|
|
2523
2768
|
try {
|
|
2524
|
-
|
|
2525
|
-
headers: { "X-Multicorn-Key": apiKey },
|
|
2526
|
-
signal: AbortSignal.timeout(8e3)
|
|
2527
|
-
});
|
|
2769
|
+
parsed = JSON.parse(trimmed);
|
|
2528
2770
|
} catch {
|
|
2529
|
-
return
|
|
2530
|
-
}
|
|
2531
|
-
if (!response.ok) return [];
|
|
2532
|
-
const body = await response.json();
|
|
2533
|
-
if (!isApiSuccessResponse(body)) return [];
|
|
2534
|
-
const agentDetail = body.data;
|
|
2535
|
-
if (!isAgentDetailShape(agentDetail)) return [];
|
|
2536
|
-
const scopes = [];
|
|
2537
|
-
for (const perm of agentDetail.permissions) {
|
|
2538
|
-
if (!isPermissionShape(perm)) continue;
|
|
2539
|
-
if (perm.revoked_at !== null) continue;
|
|
2540
|
-
if (perm.read) scopes.push({ service: perm.service, permissionLevel: "read" });
|
|
2541
|
-
if (perm.write) scopes.push({ service: perm.service, permissionLevel: "write" });
|
|
2542
|
-
if (perm.execute) scopes.push({ service: perm.service, permissionLevel: "execute" });
|
|
2771
|
+
return null;
|
|
2543
2772
|
}
|
|
2544
|
-
return
|
|
2773
|
+
return isJsonRpcRequest(parsed) ? parsed : null;
|
|
2545
2774
|
}
|
|
2546
|
-
function
|
|
2547
|
-
if (
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
const
|
|
2551
|
-
const
|
|
2552
|
-
|
|
2775
|
+
function extractToolCallParams(request) {
|
|
2776
|
+
if (request.method !== "tools/call") return null;
|
|
2777
|
+
if (typeof request.params !== "object" || request.params === null) return null;
|
|
2778
|
+
const params = request.params;
|
|
2779
|
+
const name = params["name"];
|
|
2780
|
+
const args = params["arguments"];
|
|
2781
|
+
if (typeof name !== "string") return null;
|
|
2782
|
+
if (typeof args !== "object" || args === null) return null;
|
|
2783
|
+
return { name, arguments: args };
|
|
2553
2784
|
}
|
|
2554
|
-
|
|
2555
|
-
const
|
|
2556
|
-
const
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
Waiting for you to grant access in the Multicorn dashboard...
|
|
2564
|
-
`
|
|
2565
|
-
);
|
|
2566
|
-
openBrowser(consentUrl);
|
|
2567
|
-
const deadline = Date.now() + CONSENT_POLL_TIMEOUT_MS;
|
|
2568
|
-
while (Date.now() < deadline) {
|
|
2569
|
-
await sleep2(CONSENT_POLL_INTERVAL_MS);
|
|
2570
|
-
const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl);
|
|
2571
|
-
if (scopes.length > 0) {
|
|
2572
|
-
logger.info("Permissions granted.", { agent: agentName, scopeCount: scopes.length });
|
|
2573
|
-
return scopes;
|
|
2785
|
+
function buildBlockedResponse(id, service, permissionLevel, dashboardUrl) {
|
|
2786
|
+
const displayService = capitalize(service);
|
|
2787
|
+
const message = `Action blocked by Multicorn Shield: agent does not have ${permissionLevel} access to ${displayService}. Configure permissions at ${dashboardUrl}`;
|
|
2788
|
+
return {
|
|
2789
|
+
jsonrpc: "2.0",
|
|
2790
|
+
id,
|
|
2791
|
+
error: {
|
|
2792
|
+
code: BLOCKED_ERROR_CODE,
|
|
2793
|
+
message
|
|
2574
2794
|
}
|
|
2575
|
-
}
|
|
2576
|
-
throw new Error(
|
|
2577
|
-
`Consent not granted within ${String(CONSENT_POLL_TIMEOUT_MS / 6e4)} minutes. Grant access at ${dashboardUrl} and restart the proxy.`
|
|
2578
|
-
);
|
|
2795
|
+
};
|
|
2579
2796
|
}
|
|
2580
|
-
|
|
2581
|
-
const
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
}
|
|
2589
|
-
if (agent === null) {
|
|
2590
|
-
try {
|
|
2591
|
-
logger.info("Agent not found. Registering.", { agent: agentName });
|
|
2592
|
-
const id = await registerAgent(agentName, apiKey, baseUrl, platform);
|
|
2593
|
-
agent = { id, name: agentName, scopes: [] };
|
|
2594
|
-
logger.info("Agent registered.", { agent: agentName, id });
|
|
2595
|
-
} catch (error) {
|
|
2596
|
-
if (error instanceof ShieldAuthError) {
|
|
2597
|
-
return { id: "", name: agentName, scopes: [], authInvalid: true };
|
|
2598
|
-
}
|
|
2599
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
2600
|
-
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
2601
|
-
logger.warn("Service unreachable. Using cached scopes.", { error: detail });
|
|
2602
|
-
return { id: "", name: agentName, scopes: cachedScopes };
|
|
2603
|
-
}
|
|
2604
|
-
logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
|
|
2605
|
-
error: detail
|
|
2606
|
-
});
|
|
2607
|
-
return { id: "", name: agentName, scopes: [] };
|
|
2797
|
+
function buildSpendingBlockedResponse(id, reason, dashboardUrl) {
|
|
2798
|
+
const message = `Action blocked by Multicorn Shield: ${reason}. Review spending limits at ${dashboardUrl}`;
|
|
2799
|
+
return {
|
|
2800
|
+
jsonrpc: "2.0",
|
|
2801
|
+
id,
|
|
2802
|
+
error: {
|
|
2803
|
+
code: SPENDING_BLOCKED_ERROR_CODE,
|
|
2804
|
+
message
|
|
2608
2805
|
}
|
|
2609
|
-
}
|
|
2610
|
-
const scopes = await fetchGrantedScopes(agent.id, apiKey, baseUrl);
|
|
2611
|
-
if (scopes.length > 0) {
|
|
2612
|
-
await saveCachedScopes(agentName, agent.id, scopes, apiKey);
|
|
2613
|
-
}
|
|
2614
|
-
return { ...agent, scopes };
|
|
2806
|
+
};
|
|
2615
2807
|
}
|
|
2616
|
-
function
|
|
2617
|
-
const
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2808
|
+
function buildInternalErrorResponse(id) {
|
|
2809
|
+
const message = "Action blocked: Shield encountered an internal error and cannot verify permissions. Check proxy logs for details.";
|
|
2810
|
+
return {
|
|
2811
|
+
jsonrpc: "2.0",
|
|
2812
|
+
id,
|
|
2813
|
+
error: {
|
|
2814
|
+
code: INTERNAL_ERROR_CODE,
|
|
2815
|
+
message
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2626
2818
|
}
|
|
2627
|
-
function
|
|
2628
|
-
|
|
2819
|
+
function buildServiceUnreachableResponse(id, dashboardUrl) {
|
|
2820
|
+
const message = `Action blocked: Shield cannot verify permissions (service unreachable). Configure offline behaviour at ${dashboardUrl}`;
|
|
2821
|
+
return {
|
|
2822
|
+
jsonrpc: "2.0",
|
|
2823
|
+
id,
|
|
2824
|
+
error: {
|
|
2825
|
+
code: SERVICE_UNREACHABLE_ERROR_CODE,
|
|
2826
|
+
message
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2629
2829
|
}
|
|
2630
|
-
function
|
|
2631
|
-
|
|
2830
|
+
function buildAuthErrorResponse(id) {
|
|
2831
|
+
const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-shield init to reconfigure.";
|
|
2832
|
+
return {
|
|
2833
|
+
jsonrpc: "2.0",
|
|
2834
|
+
id,
|
|
2835
|
+
error: {
|
|
2836
|
+
code: AUTH_ERROR_CODE,
|
|
2837
|
+
message
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2632
2840
|
}
|
|
2633
|
-
function
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
return obj["success"] === true;
|
|
2841
|
+
function extractServiceFromToolName(toolName) {
|
|
2842
|
+
const idx = toolName.indexOf("_");
|
|
2843
|
+
return idx === -1 ? toolName : toolName.slice(0, idx);
|
|
2637
2844
|
}
|
|
2638
|
-
function
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
return typeof obj["id"] === "string" && typeof obj["name"] === "string";
|
|
2845
|
+
function extractActionFromToolName(toolName) {
|
|
2846
|
+
const idx = toolName.indexOf("_");
|
|
2847
|
+
return idx === -1 ? "call" : toolName.slice(idx + 1);
|
|
2642
2848
|
}
|
|
2643
|
-
function
|
|
2849
|
+
function isJsonRpcRequest(value) {
|
|
2644
2850
|
if (typeof value !== "object" || value === null) return false;
|
|
2645
2851
|
const obj = value;
|
|
2646
|
-
|
|
2852
|
+
if (obj["jsonrpc"] !== "2.0") return false;
|
|
2853
|
+
if (typeof obj["method"] !== "string") return false;
|
|
2854
|
+
const id = obj["id"];
|
|
2855
|
+
const validId = id === null || id === void 0 || typeof id === "string" || typeof id === "number";
|
|
2856
|
+
return validId;
|
|
2647
2857
|
}
|
|
2648
|
-
function
|
|
2649
|
-
if (
|
|
2650
|
-
const
|
|
2651
|
-
return
|
|
2858
|
+
function capitalize(str) {
|
|
2859
|
+
if (str.length === 0) return str;
|
|
2860
|
+
const first = str[0];
|
|
2861
|
+
return first !== void 0 ? first.toUpperCase() + str.slice(1) : str;
|
|
2652
2862
|
}
|
|
2653
2863
|
|
|
2654
2864
|
// src/proxy/index.ts
|
|
@@ -2954,9 +3164,9 @@ function createProxyServer(config) {
|
|
|
2954
3164
|
timer.unref();
|
|
2955
3165
|
}
|
|
2956
3166
|
config.logger.info("Proxy ready.", { agent: config.agentName });
|
|
2957
|
-
return new Promise((
|
|
3167
|
+
return new Promise((resolve2) => {
|
|
2958
3168
|
childProcess.on("exit", () => {
|
|
2959
|
-
|
|
3169
|
+
resolve2();
|
|
2960
3170
|
});
|
|
2961
3171
|
});
|
|
2962
3172
|
}
|
|
@@ -3355,7 +3565,7 @@ function resolveWrapAgentName(cli, config) {
|
|
|
3355
3565
|
const legacyPlatform = config.platform;
|
|
3356
3566
|
const legacyAgentName = config.agentName;
|
|
3357
3567
|
const platformKey = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "other-mcp";
|
|
3358
|
-
const fromPlatform = getAgentByPlatform(config, platformKey);
|
|
3568
|
+
const fromPlatform = getAgentByPlatform(config, platformKey, process.cwd());
|
|
3359
3569
|
if (fromPlatform !== void 0) {
|
|
3360
3570
|
return fromPlatform.name;
|
|
3361
3571
|
}
|