multicorn-shield 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/multicorn-proxy.js +353 -455
- package/dist/shield-extension.js +771 -30
- package/package.json +22 -33
package/dist/shield-extension.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { mkdirSync, appendFileSync } from 'fs';
|
|
3
|
+
import { readFile, mkdir, writeFile, unlink } from 'fs/promises';
|
|
3
4
|
import { homedir } from 'os';
|
|
4
5
|
import { join } from 'path';
|
|
5
6
|
import process3 from 'process';
|
|
6
7
|
import 'stream';
|
|
7
8
|
import { spawn } from 'child_process';
|
|
8
9
|
import { createHash } from 'crypto';
|
|
9
|
-
import { readFile, mkdir, writeFile, unlink } from 'fs/promises';
|
|
10
10
|
import 'readline';
|
|
11
11
|
|
|
12
12
|
// Multicorn Shield Claude Desktop Extension - https://multicorn.ai
|
|
@@ -22008,6 +22008,10 @@ async function saveCachedScopes(agentName, agentId, scopes, apiKey) {
|
|
|
22008
22008
|
function isScopesCacheFile(value) {
|
|
22009
22009
|
return typeof value === "object" && value !== null;
|
|
22010
22010
|
}
|
|
22011
|
+
|
|
22012
|
+
// src/proxy/consent.ts
|
|
22013
|
+
var CONSENT_POLL_INTERVAL_MS = 3e3;
|
|
22014
|
+
var CONSENT_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
22011
22015
|
function deriveDashboardUrl(baseUrl) {
|
|
22012
22016
|
try {
|
|
22013
22017
|
const url2 = new URL(baseUrl);
|
|
@@ -22132,11 +22136,36 @@ function openBrowser(url2) {
|
|
|
22132
22136
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
22133
22137
|
spawn(cmd, [url2], { detached: true, stdio: "ignore" }).unref();
|
|
22134
22138
|
}
|
|
22139
|
+
async function waitForConsent(agentId, agentName, apiKey, baseUrl, dashboardUrl, logger, scope, platform) {
|
|
22140
|
+
const scopeStrings = scope ? [`${scope.service}:${scope.permissionLevel}`] : detectScopeHints();
|
|
22141
|
+
const consentUrl = buildConsentUrl(agentName, scopeStrings, dashboardUrl, platform);
|
|
22142
|
+
logger.info("Opening consent page in your browser.", { url: consentUrl });
|
|
22143
|
+
process.stderr.write(
|
|
22144
|
+
`
|
|
22145
|
+
Action requires permission. Opening consent page...
|
|
22146
|
+
${consentUrl}
|
|
22147
|
+
|
|
22148
|
+
Waiting for you to grant access in the Multicorn dashboard...
|
|
22149
|
+
`
|
|
22150
|
+
);
|
|
22151
|
+
openBrowser(consentUrl);
|
|
22152
|
+
const deadline = Date.now() + CONSENT_POLL_TIMEOUT_MS;
|
|
22153
|
+
while (Date.now() < deadline) {
|
|
22154
|
+
await sleep(CONSENT_POLL_INTERVAL_MS);
|
|
22155
|
+
const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl);
|
|
22156
|
+
if (scopes.length > 0) {
|
|
22157
|
+
logger.info("Permissions granted.", { agent: agentName, scopeCount: scopes.length });
|
|
22158
|
+
return scopes;
|
|
22159
|
+
}
|
|
22160
|
+
}
|
|
22161
|
+
throw new Error(
|
|
22162
|
+
`Consent not granted within ${String(CONSENT_POLL_TIMEOUT_MS / 6e4)} minutes. Grant access at ${dashboardUrl} and restart the proxy.`
|
|
22163
|
+
);
|
|
22164
|
+
}
|
|
22135
22165
|
async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform) {
|
|
22136
22166
|
const cachedScopes = await loadCachedScopes(agentName, apiKey);
|
|
22137
22167
|
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
22138
22168
|
logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
|
|
22139
|
-
return { id: "", name: agentName, scopes: cachedScopes };
|
|
22140
22169
|
}
|
|
22141
22170
|
let agent = await findAgentByName(agentName, apiKey, baseUrl);
|
|
22142
22171
|
if (agent?.authInvalid) {
|
|
@@ -22153,6 +22182,10 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform)
|
|
|
22153
22182
|
return { id: "", name: agentName, scopes: [], authInvalid: true };
|
|
22154
22183
|
}
|
|
22155
22184
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
22185
|
+
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
22186
|
+
logger.warn("Service unreachable. Using cached scopes.", { error: detail });
|
|
22187
|
+
return { id: "", name: agentName, scopes: cachedScopes };
|
|
22188
|
+
}
|
|
22156
22189
|
logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
|
|
22157
22190
|
error: detail
|
|
22158
22191
|
});
|
|
@@ -22176,6 +22209,12 @@ function buildConsentUrl(agentName, scopes, dashboardUrl, platform) {
|
|
|
22176
22209
|
}
|
|
22177
22210
|
return `${base}/consent?${params.toString()}`;
|
|
22178
22211
|
}
|
|
22212
|
+
function detectScopeHints() {
|
|
22213
|
+
return [];
|
|
22214
|
+
}
|
|
22215
|
+
function sleep(ms) {
|
|
22216
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22217
|
+
}
|
|
22179
22218
|
function isApiSuccessResponse(value) {
|
|
22180
22219
|
if (typeof value !== "object" || value === null) return false;
|
|
22181
22220
|
const obj = value;
|
|
@@ -22246,6 +22285,23 @@ function isMcpServerEntry(value) {
|
|
|
22246
22285
|
}
|
|
22247
22286
|
return true;
|
|
22248
22287
|
}
|
|
22288
|
+
function isShieldExtensionEntry(serverKey, entry) {
|
|
22289
|
+
const key = serverKey.trim().toLowerCase();
|
|
22290
|
+
if (key === "multicorn-shield") {
|
|
22291
|
+
return true;
|
|
22292
|
+
}
|
|
22293
|
+
const argBlob = entry.args.join(" ").toLowerCase();
|
|
22294
|
+
if (argBlob.includes("shield-extension")) {
|
|
22295
|
+
return true;
|
|
22296
|
+
}
|
|
22297
|
+
if (entry.command.toLowerCase().includes("shield-extension")) {
|
|
22298
|
+
return true;
|
|
22299
|
+
}
|
|
22300
|
+
if (entry.env?.["MULTICORN_SHIELD_EXTENSION"] === "1") {
|
|
22301
|
+
return true;
|
|
22302
|
+
}
|
|
22303
|
+
return false;
|
|
22304
|
+
}
|
|
22249
22305
|
async function readClaudeDesktopMcpConfig() {
|
|
22250
22306
|
const configPath = getClaudeDesktopConfigPath();
|
|
22251
22307
|
let raw;
|
|
@@ -22302,7 +22358,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22302
22358
|
|
|
22303
22359
|
// package.json
|
|
22304
22360
|
var package_default = {
|
|
22305
|
-
version: "0.
|
|
22361
|
+
version: "0.4.0"};
|
|
22306
22362
|
|
|
22307
22363
|
// src/package-meta.ts
|
|
22308
22364
|
var PACKAGE_VERSION = package_default.version;
|
|
@@ -22506,7 +22562,7 @@ var ProxySession = class {
|
|
|
22506
22562
|
this.nextId = 1;
|
|
22507
22563
|
this.sessionId = null;
|
|
22508
22564
|
this.closed = false;
|
|
22509
|
-
this.proxyUrl = proxyUrl;
|
|
22565
|
+
this.proxyUrl = proxyUrl.replace(/\/+$/, "") + "/mcp";
|
|
22510
22566
|
this.apiKey = apiKey;
|
|
22511
22567
|
this.requestTimeoutMs = options?.requestTimeoutMs ?? 6e4;
|
|
22512
22568
|
}
|
|
@@ -22753,6 +22809,386 @@ function resultSuggestsConsentNeeded(result) {
|
|
|
22753
22809
|
const t = first.text;
|
|
22754
22810
|
return t.includes("Action blocked by Multicorn Shield") || t.includes("does not have") && t.includes("access to") || t.includes("Configure permissions at");
|
|
22755
22811
|
}
|
|
22812
|
+
|
|
22813
|
+
// src/types/index.ts
|
|
22814
|
+
var PERMISSION_LEVELS = {
|
|
22815
|
+
Read: "read",
|
|
22816
|
+
Write: "write",
|
|
22817
|
+
Execute: "execute",
|
|
22818
|
+
Publish: "publish",
|
|
22819
|
+
Create: "create"
|
|
22820
|
+
};
|
|
22821
|
+
|
|
22822
|
+
// src/scopes/scope-parser.ts
|
|
22823
|
+
var VALID_PERMISSION_LEVELS = new Set(Object.values(PERMISSION_LEVELS));
|
|
22824
|
+
[...VALID_PERMISSION_LEVELS].join(", ");
|
|
22825
|
+
function formatScope(scope) {
|
|
22826
|
+
return `${scope.permissionLevel}:${scope.service}`;
|
|
22827
|
+
}
|
|
22828
|
+
|
|
22829
|
+
// src/scopes/scope-validator.ts
|
|
22830
|
+
function validateScopeAccess(grantedScopes, requested) {
|
|
22831
|
+
const isGranted = grantedScopes.some(
|
|
22832
|
+
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
22833
|
+
);
|
|
22834
|
+
if (isGranted) {
|
|
22835
|
+
return { allowed: true };
|
|
22836
|
+
}
|
|
22837
|
+
const serviceScopes = grantedScopes.filter((g) => g.service === requested.service);
|
|
22838
|
+
if (serviceScopes.length > 0) {
|
|
22839
|
+
const grantedLevels = serviceScopes.map((g) => `"${g.permissionLevel}"`).join(", ");
|
|
22840
|
+
return {
|
|
22841
|
+
allowed: false,
|
|
22842
|
+
reason: `Permission "${requested.permissionLevel}" is not granted for service "${requested.service}". Currently granted permission level(s): ${grantedLevels}. Requested scope "${formatScope(requested)}" requires explicit consent.`
|
|
22843
|
+
};
|
|
22844
|
+
}
|
|
22845
|
+
return {
|
|
22846
|
+
allowed: false,
|
|
22847
|
+
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.`
|
|
22848
|
+
};
|
|
22849
|
+
}
|
|
22850
|
+
function hasScope(grantedScopes, requested) {
|
|
22851
|
+
return grantedScopes.some(
|
|
22852
|
+
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
22853
|
+
);
|
|
22854
|
+
}
|
|
22855
|
+
|
|
22856
|
+
// src/logger/action-logger.ts
|
|
22857
|
+
function createActionLogger(config2) {
|
|
22858
|
+
if (!config2.apiKey || config2.apiKey.trim().length === 0) {
|
|
22859
|
+
throw new Error(
|
|
22860
|
+
"[ActionLogger] API key is required. Provide it via the 'apiKey' config option."
|
|
22861
|
+
);
|
|
22862
|
+
}
|
|
22863
|
+
const baseUrl = config2.baseUrl ?? "https://api.multicorn.ai";
|
|
22864
|
+
const timeout = config2.timeout ?? 5e3;
|
|
22865
|
+
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
|
|
22866
|
+
throw new Error(
|
|
22867
|
+
`[ActionLogger] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
|
|
22868
|
+
);
|
|
22869
|
+
}
|
|
22870
|
+
const endpoint = `${baseUrl}/api/v1/actions`;
|
|
22871
|
+
const batchEnabled = config2.batchMode?.enabled ?? false;
|
|
22872
|
+
const maxBatchSize = config2.batchMode?.maxSize ?? 10;
|
|
22873
|
+
const flushInterval = config2.batchMode?.flushIntervalMs ?? 5e3;
|
|
22874
|
+
const queue = [];
|
|
22875
|
+
let flushTimer;
|
|
22876
|
+
let isShutdown = false;
|
|
22877
|
+
async function sendActions(actions) {
|
|
22878
|
+
if (actions.length === 0) return;
|
|
22879
|
+
const convertAction = (action) => ({
|
|
22880
|
+
agent: action.agent,
|
|
22881
|
+
service: action.service,
|
|
22882
|
+
actionType: action.actionType,
|
|
22883
|
+
status: action.status,
|
|
22884
|
+
...action.cost !== void 0 ? { cost: action.cost } : {},
|
|
22885
|
+
...action.metadata !== void 0 ? { metadata: action.metadata } : {}
|
|
22886
|
+
});
|
|
22887
|
+
const convertedActions = actions.map(convertAction);
|
|
22888
|
+
const payload = batchEnabled ? { actions: convertedActions } : convertedActions[0];
|
|
22889
|
+
let lastError;
|
|
22890
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
22891
|
+
try {
|
|
22892
|
+
const controller = new AbortController();
|
|
22893
|
+
const timeoutId = setTimeout(() => {
|
|
22894
|
+
controller.abort();
|
|
22895
|
+
}, timeout);
|
|
22896
|
+
const response = await fetch(endpoint, {
|
|
22897
|
+
method: "POST",
|
|
22898
|
+
headers: {
|
|
22899
|
+
"Content-Type": "application/json",
|
|
22900
|
+
"X-Multicorn-Key": config2.apiKey
|
|
22901
|
+
},
|
|
22902
|
+
body: JSON.stringify(payload),
|
|
22903
|
+
signal: controller.signal
|
|
22904
|
+
});
|
|
22905
|
+
clearTimeout(timeoutId);
|
|
22906
|
+
if (response.ok) {
|
|
22907
|
+
return;
|
|
22908
|
+
}
|
|
22909
|
+
if (response.status >= 400 && response.status < 500) {
|
|
22910
|
+
const body = await response.text().catch(() => "");
|
|
22911
|
+
throw new Error(
|
|
22912
|
+
`[ActionLogger] Client error (${String(response.status)}): ${response.statusText}. Response: ${body}. Check your API key and payload format.`
|
|
22913
|
+
);
|
|
22914
|
+
}
|
|
22915
|
+
if (response.status >= 500 && attempt === 0) {
|
|
22916
|
+
lastError = new Error(
|
|
22917
|
+
`[ActionLogger] Server error (${String(response.status)}): ${response.statusText}. Retrying once...`
|
|
22918
|
+
);
|
|
22919
|
+
await sleep2(100 * Math.pow(2, attempt));
|
|
22920
|
+
continue;
|
|
22921
|
+
}
|
|
22922
|
+
throw new Error(
|
|
22923
|
+
`[ActionLogger] Server error (${String(response.status)}) after retry: ${response.statusText}. Multicorn API may be experiencing issues.`
|
|
22924
|
+
);
|
|
22925
|
+
} catch (error2) {
|
|
22926
|
+
if (error2 instanceof Error) {
|
|
22927
|
+
if (error2.name === "AbortError") {
|
|
22928
|
+
lastError = new Error(
|
|
22929
|
+
`[ActionLogger] Request timeout after ${String(timeout)}ms. Increase the 'timeout' config option or check your network connection.`
|
|
22930
|
+
);
|
|
22931
|
+
} else if (error2.message.includes("Client error") || error2.message.includes("Server error")) {
|
|
22932
|
+
lastError = error2;
|
|
22933
|
+
} else {
|
|
22934
|
+
lastError = new Error(
|
|
22935
|
+
`[ActionLogger] Network error: ${error2.message}. Check your network connection and API endpoint.`
|
|
22936
|
+
);
|
|
22937
|
+
}
|
|
22938
|
+
} else {
|
|
22939
|
+
lastError = new Error(`[ActionLogger] Unknown error: ${String(error2)}`);
|
|
22940
|
+
}
|
|
22941
|
+
if (attempt === 0 && !lastError.message.includes("Client error")) {
|
|
22942
|
+
await sleep2(100 * Math.pow(2, attempt));
|
|
22943
|
+
continue;
|
|
22944
|
+
}
|
|
22945
|
+
break;
|
|
22946
|
+
}
|
|
22947
|
+
}
|
|
22948
|
+
if (lastError) {
|
|
22949
|
+
if (config2.onError) {
|
|
22950
|
+
config2.onError(lastError);
|
|
22951
|
+
}
|
|
22952
|
+
}
|
|
22953
|
+
}
|
|
22954
|
+
async function flushQueue() {
|
|
22955
|
+
if (queue.length === 0) return;
|
|
22956
|
+
const actions = queue.map((item) => item.payload);
|
|
22957
|
+
queue.length = 0;
|
|
22958
|
+
await sendActions(actions);
|
|
22959
|
+
}
|
|
22960
|
+
function startFlushTimer() {
|
|
22961
|
+
if (flushTimer !== void 0) return;
|
|
22962
|
+
flushTimer = setInterval(() => {
|
|
22963
|
+
flushQueue().catch(() => {
|
|
22964
|
+
});
|
|
22965
|
+
}, flushInterval);
|
|
22966
|
+
const timer = flushTimer;
|
|
22967
|
+
if (typeof timer.unref === "function") {
|
|
22968
|
+
timer.unref();
|
|
22969
|
+
}
|
|
22970
|
+
}
|
|
22971
|
+
function stopFlushTimer() {
|
|
22972
|
+
if (flushTimer) {
|
|
22973
|
+
clearInterval(flushTimer);
|
|
22974
|
+
flushTimer = void 0;
|
|
22975
|
+
}
|
|
22976
|
+
}
|
|
22977
|
+
if (batchEnabled) {
|
|
22978
|
+
startFlushTimer();
|
|
22979
|
+
}
|
|
22980
|
+
return {
|
|
22981
|
+
logAction(action) {
|
|
22982
|
+
if (isShutdown) {
|
|
22983
|
+
throw new Error(
|
|
22984
|
+
"[ActionLogger] Cannot log action after shutdown. Create a new logger instance."
|
|
22985
|
+
);
|
|
22986
|
+
}
|
|
22987
|
+
if (action.agent.trim().length === 0) {
|
|
22988
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'agent' field.");
|
|
22989
|
+
}
|
|
22990
|
+
if (action.service.trim().length === 0) {
|
|
22991
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'service' field.");
|
|
22992
|
+
}
|
|
22993
|
+
if (action.actionType.trim().length === 0) {
|
|
22994
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'actionType' field.");
|
|
22995
|
+
}
|
|
22996
|
+
if (action.status.trim().length === 0) {
|
|
22997
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'status' field.");
|
|
22998
|
+
}
|
|
22999
|
+
if (batchEnabled) {
|
|
23000
|
+
queue.push({ payload: action, timestamp: Date.now() });
|
|
23001
|
+
if (queue.length >= maxBatchSize) {
|
|
23002
|
+
flushQueue().catch(() => {
|
|
23003
|
+
});
|
|
23004
|
+
}
|
|
23005
|
+
} else {
|
|
23006
|
+
sendActions([action]).catch(() => {
|
|
23007
|
+
});
|
|
23008
|
+
}
|
|
23009
|
+
return Promise.resolve();
|
|
23010
|
+
},
|
|
23011
|
+
async flush() {
|
|
23012
|
+
if (!batchEnabled) return;
|
|
23013
|
+
await flushQueue();
|
|
23014
|
+
},
|
|
23015
|
+
async shutdown() {
|
|
23016
|
+
if (isShutdown) return;
|
|
23017
|
+
isShutdown = true;
|
|
23018
|
+
stopFlushTimer();
|
|
23019
|
+
if (batchEnabled) {
|
|
23020
|
+
await flushQueue();
|
|
23021
|
+
}
|
|
23022
|
+
}
|
|
23023
|
+
};
|
|
23024
|
+
}
|
|
23025
|
+
function sleep2(ms) {
|
|
23026
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
23027
|
+
}
|
|
23028
|
+
|
|
23029
|
+
// src/proxy/interceptor.ts
|
|
23030
|
+
var BLOCKED_ERROR_CODE = -32e3;
|
|
23031
|
+
var INTERNAL_ERROR_CODE = -32002;
|
|
23032
|
+
var SERVICE_UNREACHABLE_ERROR_CODE = -32003;
|
|
23033
|
+
function buildBlockedResponse(id, service, permissionLevel, dashboardUrl) {
|
|
23034
|
+
const displayService = capitalize(service);
|
|
23035
|
+
const message = `Action blocked by Multicorn Shield: agent does not have ${permissionLevel} access to ${displayService}. Configure permissions at ${dashboardUrl}`;
|
|
23036
|
+
return {
|
|
23037
|
+
jsonrpc: "2.0",
|
|
23038
|
+
id,
|
|
23039
|
+
error: {
|
|
23040
|
+
code: BLOCKED_ERROR_CODE,
|
|
23041
|
+
message
|
|
23042
|
+
}
|
|
23043
|
+
};
|
|
23044
|
+
}
|
|
23045
|
+
function buildInternalErrorResponse(id) {
|
|
23046
|
+
const message = "Action blocked: Shield encountered an internal error and cannot verify permissions. Check proxy logs for details.";
|
|
23047
|
+
return {
|
|
23048
|
+
jsonrpc: "2.0",
|
|
23049
|
+
id,
|
|
23050
|
+
error: {
|
|
23051
|
+
code: INTERNAL_ERROR_CODE,
|
|
23052
|
+
message
|
|
23053
|
+
}
|
|
23054
|
+
};
|
|
23055
|
+
}
|
|
23056
|
+
function buildServiceUnreachableResponse(id, dashboardUrl) {
|
|
23057
|
+
const message = `Action blocked: Shield cannot verify permissions (service unreachable). Configure offline behaviour at ${dashboardUrl}`;
|
|
23058
|
+
return {
|
|
23059
|
+
jsonrpc: "2.0",
|
|
23060
|
+
id,
|
|
23061
|
+
error: {
|
|
23062
|
+
code: SERVICE_UNREACHABLE_ERROR_CODE,
|
|
23063
|
+
message
|
|
23064
|
+
}
|
|
23065
|
+
};
|
|
23066
|
+
}
|
|
23067
|
+
function capitalize(str) {
|
|
23068
|
+
if (str.length === 0) return str;
|
|
23069
|
+
const first = str[0];
|
|
23070
|
+
return first !== void 0 ? first.toUpperCase() + str.slice(1) : str;
|
|
23071
|
+
}
|
|
23072
|
+
|
|
23073
|
+
// src/mcp-tool-mapper.ts
|
|
23074
|
+
var FILESYSTEM_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
23075
|
+
"read_file",
|
|
23076
|
+
"read_text_file",
|
|
23077
|
+
"read_media_file",
|
|
23078
|
+
"read_multiple_files",
|
|
23079
|
+
"list_directory",
|
|
23080
|
+
"list_dir",
|
|
23081
|
+
"directory_tree",
|
|
23082
|
+
"tree",
|
|
23083
|
+
"get_file_info",
|
|
23084
|
+
"stat",
|
|
23085
|
+
"search_files",
|
|
23086
|
+
"glob_file_search",
|
|
23087
|
+
"list_allowed_directories",
|
|
23088
|
+
"file_search"
|
|
23089
|
+
]);
|
|
23090
|
+
var FILESYSTEM_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
23091
|
+
"write_file",
|
|
23092
|
+
"edit_file",
|
|
23093
|
+
"create_directory",
|
|
23094
|
+
"mkdir",
|
|
23095
|
+
"move_file",
|
|
23096
|
+
"rename",
|
|
23097
|
+
"delete_file",
|
|
23098
|
+
"remove_file",
|
|
23099
|
+
"copy_file"
|
|
23100
|
+
]);
|
|
23101
|
+
var TERMINAL_EXECUTE_TOOLS = /* @__PURE__ */ new Set([
|
|
23102
|
+
"run_terminal_cmd",
|
|
23103
|
+
"execute_command",
|
|
23104
|
+
"terminal_run",
|
|
23105
|
+
"run_command"
|
|
23106
|
+
]);
|
|
23107
|
+
var BROWSER_EXECUTE_TOOLS = /* @__PURE__ */ new Set([
|
|
23108
|
+
"web_fetch",
|
|
23109
|
+
"fetch_url",
|
|
23110
|
+
"browser_navigate",
|
|
23111
|
+
"navigate",
|
|
23112
|
+
"mcp_web_fetch"
|
|
23113
|
+
]);
|
|
23114
|
+
var INTEGRATION_SERVICE_BY_PREFIX = {
|
|
23115
|
+
gmail: "gmail",
|
|
23116
|
+
google_calendar: "google_calendar",
|
|
23117
|
+
calendar: "google_calendar",
|
|
23118
|
+
google_drive: "google_drive",
|
|
23119
|
+
drive: "google_drive",
|
|
23120
|
+
slack: "slack",
|
|
23121
|
+
payments: "payments",
|
|
23122
|
+
payment: "payments",
|
|
23123
|
+
stripe: "payments",
|
|
23124
|
+
github: "github",
|
|
23125
|
+
gitlab: "gitlab",
|
|
23126
|
+
notion: "notion",
|
|
23127
|
+
linear: "linear",
|
|
23128
|
+
jira: "jira"
|
|
23129
|
+
};
|
|
23130
|
+
function inferPermissionFromToolName(normalized) {
|
|
23131
|
+
if (normalized.includes("_read") || normalized.includes("_get") || normalized.includes("_list") || normalized.endsWith("_fetch") || normalized.includes("_search")) {
|
|
23132
|
+
return "read";
|
|
23133
|
+
}
|
|
23134
|
+
if (normalized.includes("_write") || normalized.includes("_send") || normalized.includes("_create") || normalized.includes("_update") || normalized.includes("_delete") || normalized.includes("_push") || normalized.includes("_commit") || normalized.includes("_post") || normalized.includes("_patch")) {
|
|
23135
|
+
return "write";
|
|
23136
|
+
}
|
|
23137
|
+
return "execute";
|
|
23138
|
+
}
|
|
23139
|
+
function mapMcpToolToScope(toolName) {
|
|
23140
|
+
const actionType = toolName.trim();
|
|
23141
|
+
const normalized = actionType.toLowerCase();
|
|
23142
|
+
if (normalized.length === 0) {
|
|
23143
|
+
return { service: "unknown", permissionLevel: "execute", actionType };
|
|
23144
|
+
}
|
|
23145
|
+
if (FILESYSTEM_READ_TOOLS.has(normalized)) {
|
|
23146
|
+
return { service: "filesystem", permissionLevel: "read", actionType };
|
|
23147
|
+
}
|
|
23148
|
+
if (FILESYSTEM_WRITE_TOOLS.has(normalized)) {
|
|
23149
|
+
return { service: "filesystem", permissionLevel: "write", actionType };
|
|
23150
|
+
}
|
|
23151
|
+
if (TERMINAL_EXECUTE_TOOLS.has(normalized)) {
|
|
23152
|
+
return { service: "terminal", permissionLevel: "execute", actionType };
|
|
23153
|
+
}
|
|
23154
|
+
if (BROWSER_EXECUTE_TOOLS.has(normalized)) {
|
|
23155
|
+
return { service: "browser", permissionLevel: "execute", actionType };
|
|
23156
|
+
}
|
|
23157
|
+
if (normalized === "read") {
|
|
23158
|
+
return { service: "filesystem", permissionLevel: "read", actionType };
|
|
23159
|
+
}
|
|
23160
|
+
if (normalized === "write" || normalized === "edit") {
|
|
23161
|
+
return { service: "filesystem", permissionLevel: "write", actionType };
|
|
23162
|
+
}
|
|
23163
|
+
if (normalized === "exec") {
|
|
23164
|
+
return { service: "terminal", permissionLevel: "execute", actionType };
|
|
23165
|
+
}
|
|
23166
|
+
if (normalized.startsWith("git_")) {
|
|
23167
|
+
const permissionLevel2 = inferPermissionFromToolName(normalized);
|
|
23168
|
+
return { service: "git", permissionLevel: permissionLevel2, actionType };
|
|
23169
|
+
}
|
|
23170
|
+
for (const [prefix, service] of Object.entries(INTEGRATION_SERVICE_BY_PREFIX)) {
|
|
23171
|
+
if (normalized.startsWith(`${prefix}_`) || normalized === prefix) {
|
|
23172
|
+
const permissionLevel2 = inferPermissionFromToolName(normalized);
|
|
23173
|
+
return { service, permissionLevel: permissionLevel2, actionType };
|
|
23174
|
+
}
|
|
23175
|
+
}
|
|
23176
|
+
const idx = normalized.indexOf("_");
|
|
23177
|
+
if (idx === -1) {
|
|
23178
|
+
return { service: normalized, permissionLevel: "execute", actionType };
|
|
23179
|
+
}
|
|
23180
|
+
const head = normalized.slice(0, idx);
|
|
23181
|
+
const tail = normalized.slice(idx + 1);
|
|
23182
|
+
let permissionLevel = "execute";
|
|
23183
|
+
if (tail.includes("read") || tail.includes("list") || tail.includes("get") || tail.includes("search") || tail.includes("fetch")) {
|
|
23184
|
+
permissionLevel = "read";
|
|
23185
|
+
} else if (tail.includes("write") || tail.includes("send") || tail.includes("create") || tail.includes("update") || tail.includes("delete") || tail.includes("remove")) {
|
|
23186
|
+
permissionLevel = "write";
|
|
23187
|
+
}
|
|
23188
|
+
return { service: head, permissionLevel, actionType };
|
|
23189
|
+
}
|
|
23190
|
+
|
|
23191
|
+
// src/extension/runtime.ts
|
|
22756
23192
|
function debugLog(msg) {
|
|
22757
23193
|
try {
|
|
22758
23194
|
const dir = join(homedir(), ".multicorn");
|
|
@@ -22762,10 +23198,37 @@ function debugLog(msg) {
|
|
|
22762
23198
|
} catch {
|
|
22763
23199
|
}
|
|
22764
23200
|
}
|
|
23201
|
+
var JSON_RPC_ID = 0;
|
|
23202
|
+
function toolError(text) {
|
|
23203
|
+
return {
|
|
23204
|
+
isError: true,
|
|
23205
|
+
content: [{ type: "text", text }]
|
|
23206
|
+
};
|
|
23207
|
+
}
|
|
23208
|
+
function messageFromJsonRpcResponse(json2) {
|
|
23209
|
+
try {
|
|
23210
|
+
const parsed = JSON.parse(json2);
|
|
23211
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
23212
|
+
const err = parsed["error"];
|
|
23213
|
+
if (typeof err === "object" && err !== null) {
|
|
23214
|
+
const msg = err["message"];
|
|
23215
|
+
if (typeof msg === "string") return msg;
|
|
23216
|
+
}
|
|
23217
|
+
}
|
|
23218
|
+
} catch {
|
|
23219
|
+
return "Action blocked by Multicorn Shield.";
|
|
23220
|
+
}
|
|
23221
|
+
return "Action blocked by Multicorn Shield.";
|
|
23222
|
+
}
|
|
23223
|
+
var DEFAULT_SCOPE_REFRESH_INTERVAL_MS = 6e4;
|
|
22765
23224
|
var ShieldExtensionRuntime = class {
|
|
22766
23225
|
constructor(config2) {
|
|
23226
|
+
this.actionLogger = null;
|
|
23227
|
+
this.grantedScopes = [];
|
|
22767
23228
|
this.agentId = "";
|
|
22768
23229
|
this.authInvalid = false;
|
|
23230
|
+
this.refreshTimer = null;
|
|
23231
|
+
this.consentInProgress = false;
|
|
22769
23232
|
this.consentBrowserOpened = false;
|
|
22770
23233
|
this.config = config2;
|
|
22771
23234
|
}
|
|
@@ -22792,15 +23255,90 @@ var ShieldExtensionRuntime = class {
|
|
|
22792
23255
|
"claude-desktop"
|
|
22793
23256
|
);
|
|
22794
23257
|
debugLog(
|
|
22795
|
-
`[SHIELD] Agent record resolved: id=${agentRecord.id.length > 0 ? agentRecord.id : "(empty)"} authInvalid=${String(agentRecord.authInvalid === true)}`
|
|
23258
|
+
`[SHIELD] Agent record resolved: id=${agentRecord.id.length > 0 ? agentRecord.id : "(empty)"} scopeCount=${String(agentRecord.scopes.length)} authInvalid=${String(agentRecord.authInvalid === true)}`
|
|
22796
23259
|
);
|
|
22797
23260
|
this.agentId = agentRecord.id;
|
|
23261
|
+
this.grantedScopes = agentRecord.scopes;
|
|
22798
23262
|
this.authInvalid = agentRecord.authInvalid === true;
|
|
23263
|
+
this.actionLogger = createActionLogger({
|
|
23264
|
+
apiKey: cfg.apiKey,
|
|
23265
|
+
baseUrl: cfg.baseUrl,
|
|
23266
|
+
batchMode: { enabled: false },
|
|
23267
|
+
onError: (err) => {
|
|
23268
|
+
cfg.logger.warn("Action log failed.", { error: err.message });
|
|
23269
|
+
}
|
|
23270
|
+
});
|
|
23271
|
+
const refreshIntervalMs = cfg.scopeRefreshIntervalMs ?? DEFAULT_SCOPE_REFRESH_INTERVAL_MS;
|
|
23272
|
+
this.refreshTimer = setInterval(() => {
|
|
23273
|
+
void this.refreshScopes();
|
|
23274
|
+
}, refreshIntervalMs);
|
|
23275
|
+
const timer = this.refreshTimer;
|
|
23276
|
+
if (typeof timer.unref === "function") {
|
|
23277
|
+
timer.unref();
|
|
23278
|
+
}
|
|
22799
23279
|
}
|
|
22800
23280
|
async stop() {
|
|
23281
|
+
if (this.refreshTimer !== null) {
|
|
23282
|
+
clearInterval(this.refreshTimer);
|
|
23283
|
+
this.refreshTimer = null;
|
|
23284
|
+
}
|
|
23285
|
+
if (this.actionLogger !== null) {
|
|
23286
|
+
await this.actionLogger.shutdown();
|
|
23287
|
+
this.actionLogger = null;
|
|
23288
|
+
}
|
|
23289
|
+
}
|
|
23290
|
+
async refreshScopes() {
|
|
23291
|
+
if (this.agentId.length === 0) return;
|
|
23292
|
+
try {
|
|
23293
|
+
const scopes = await fetchGrantedScopes(
|
|
23294
|
+
this.agentId,
|
|
23295
|
+
this.config.apiKey,
|
|
23296
|
+
this.config.baseUrl
|
|
23297
|
+
);
|
|
23298
|
+
this.grantedScopes = scopes;
|
|
23299
|
+
if (scopes.length > 0) {
|
|
23300
|
+
await saveCachedScopes(this.config.agentName, this.agentId, scopes, this.config.apiKey);
|
|
23301
|
+
}
|
|
23302
|
+
} catch (error2) {
|
|
23303
|
+
this.config.logger.warn("Scope refresh failed.", {
|
|
23304
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
23305
|
+
});
|
|
23306
|
+
}
|
|
23307
|
+
}
|
|
23308
|
+
async ensureConsent(requestedScope) {
|
|
23309
|
+
if (this.agentId.length === 0) return;
|
|
23310
|
+
if (requestedScope !== void 0) {
|
|
23311
|
+
if (hasScope(this.grantedScopes, requestedScope) || this.consentInProgress) return;
|
|
23312
|
+
} else {
|
|
23313
|
+
if (this.grantedScopes.length > 0 || this.consentInProgress) return;
|
|
23314
|
+
}
|
|
23315
|
+
this.consentInProgress = true;
|
|
23316
|
+
try {
|
|
23317
|
+
const scopeParam = requestedScope !== void 0 ? { service: requestedScope.service, permissionLevel: requestedScope.permissionLevel } : void 0;
|
|
23318
|
+
debugLog(
|
|
23319
|
+
`[SHIELD] ensureConsent: calling waitForConsent agentId=${this.agentId} scope=${scopeParam !== void 0 ? `${scopeParam.service}:${scopeParam.permissionLevel}` : "default"}`
|
|
23320
|
+
);
|
|
23321
|
+
const scopes = await waitForConsent(
|
|
23322
|
+
this.agentId,
|
|
23323
|
+
this.config.agentName,
|
|
23324
|
+
this.config.apiKey,
|
|
23325
|
+
this.config.baseUrl,
|
|
23326
|
+
this.config.dashboardUrl,
|
|
23327
|
+
this.config.logger,
|
|
23328
|
+
scopeParam,
|
|
23329
|
+
"claude-desktop"
|
|
23330
|
+
);
|
|
23331
|
+
debugLog(
|
|
23332
|
+
`[SHIELD] ensureConsent: waitForConsent returned scopeCount=${String(scopes.length)}`
|
|
23333
|
+
);
|
|
23334
|
+
this.grantedScopes = scopes;
|
|
23335
|
+
await saveCachedScopes(this.config.agentName, this.agentId, scopes, this.config.apiKey);
|
|
23336
|
+
} finally {
|
|
23337
|
+
this.consentInProgress = false;
|
|
23338
|
+
}
|
|
22801
23339
|
}
|
|
22802
23340
|
/**
|
|
22803
|
-
* Opens the consent URL once (
|
|
23341
|
+
* Opens the consent URL once (hosted-proxy path when a tool result suggests consent).
|
|
22804
23342
|
*/
|
|
22805
23343
|
openConsentBrowserOnce() {
|
|
22806
23344
|
if (this.consentBrowserOpened || this.authInvalid) {
|
|
@@ -22822,6 +23360,87 @@ ${consentUrl}
|
|
|
22822
23360
|
);
|
|
22823
23361
|
openBrowser(consentUrl);
|
|
22824
23362
|
}
|
|
23363
|
+
/**
|
|
23364
|
+
* Returns whether the tool call may proceed to the child MCP server.
|
|
23365
|
+
*/
|
|
23366
|
+
async evaluateToolCall(toolName) {
|
|
23367
|
+
const cfg = this.config;
|
|
23368
|
+
try {
|
|
23369
|
+
if (this.authInvalid) {
|
|
23370
|
+
return {
|
|
23371
|
+
allow: false,
|
|
23372
|
+
result: toolError(
|
|
23373
|
+
"Action blocked: Shield API key is invalid or has been revoked. Open Claude Desktop, open the Multicorn Shield extension settings, and enter a valid API key."
|
|
23374
|
+
)
|
|
23375
|
+
};
|
|
23376
|
+
}
|
|
23377
|
+
if (this.agentId.length === 0) {
|
|
23378
|
+
const blocked = buildServiceUnreachableResponse(JSON_RPC_ID, cfg.dashboardUrl);
|
|
23379
|
+
return {
|
|
23380
|
+
allow: false,
|
|
23381
|
+
result: toolError(messageFromJsonRpcResponse(JSON.stringify(blocked)))
|
|
23382
|
+
};
|
|
23383
|
+
}
|
|
23384
|
+
const mapped = mapMcpToolToScope(toolName);
|
|
23385
|
+
const { service, permissionLevel, actionType } = mapped;
|
|
23386
|
+
const requestedScope = { service, permissionLevel };
|
|
23387
|
+
let validation = validateScopeAccess(this.grantedScopes, requestedScope);
|
|
23388
|
+
cfg.logger.debug("Tool call intercepted.", {
|
|
23389
|
+
tool: toolName,
|
|
23390
|
+
service,
|
|
23391
|
+
permissionLevel,
|
|
23392
|
+
allowed: validation.allowed
|
|
23393
|
+
});
|
|
23394
|
+
if (!validation.allowed) {
|
|
23395
|
+
debugLog(`[SHIELD] Before ensureConsent() for tool=${toolName}`);
|
|
23396
|
+
await this.ensureConsent(requestedScope);
|
|
23397
|
+
debugLog(`[SHIELD] After ensureConsent() for tool=${toolName}`);
|
|
23398
|
+
validation = validateScopeAccess(this.grantedScopes, requestedScope);
|
|
23399
|
+
if (!validation.allowed) {
|
|
23400
|
+
if (this.actionLogger !== null && cfg.agentName.trim().length > 0) {
|
|
23401
|
+
await this.actionLogger.logAction({
|
|
23402
|
+
agent: cfg.agentName,
|
|
23403
|
+
service,
|
|
23404
|
+
actionType,
|
|
23405
|
+
status: "blocked"
|
|
23406
|
+
});
|
|
23407
|
+
}
|
|
23408
|
+
const blocked = buildBlockedResponse(
|
|
23409
|
+
JSON_RPC_ID,
|
|
23410
|
+
service,
|
|
23411
|
+
permissionLevel,
|
|
23412
|
+
cfg.dashboardUrl
|
|
23413
|
+
);
|
|
23414
|
+
return {
|
|
23415
|
+
allow: false,
|
|
23416
|
+
result: toolError(messageFromJsonRpcResponse(JSON.stringify(blocked)))
|
|
23417
|
+
};
|
|
23418
|
+
}
|
|
23419
|
+
}
|
|
23420
|
+
if (this.actionLogger !== null) {
|
|
23421
|
+
if (cfg.agentName.trim().length === 0) {
|
|
23422
|
+
cfg.logger.warn("Cannot log action: agent name not resolved.");
|
|
23423
|
+
} else {
|
|
23424
|
+
await this.actionLogger.logAction({
|
|
23425
|
+
agent: cfg.agentName,
|
|
23426
|
+
service,
|
|
23427
|
+
actionType,
|
|
23428
|
+
status: "approved"
|
|
23429
|
+
});
|
|
23430
|
+
}
|
|
23431
|
+
}
|
|
23432
|
+
return { allow: true };
|
|
23433
|
+
} catch (error2) {
|
|
23434
|
+
cfg.logger.error("Tool call handler error.", {
|
|
23435
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
23436
|
+
});
|
|
23437
|
+
const blocked = buildInternalErrorResponse(JSON_RPC_ID);
|
|
23438
|
+
return {
|
|
23439
|
+
allow: false,
|
|
23440
|
+
result: toolError(messageFromJsonRpcResponse(JSON.stringify(blocked)))
|
|
23441
|
+
};
|
|
23442
|
+
}
|
|
23443
|
+
}
|
|
22825
23444
|
};
|
|
22826
23445
|
|
|
22827
23446
|
// src/extension/server.ts
|
|
@@ -22835,6 +23454,45 @@ function debugLog2(msg) {
|
|
|
22835
23454
|
}
|
|
22836
23455
|
}
|
|
22837
23456
|
var SETUP_TIMEOUT_MS = 15e3;
|
|
23457
|
+
function getMulticornConfigPath() {
|
|
23458
|
+
return join(homedir(), ".multicorn", "config.json");
|
|
23459
|
+
}
|
|
23460
|
+
function isLocalProxyConfigRow(value) {
|
|
23461
|
+
if (typeof value !== "object" || value === null) return false;
|
|
23462
|
+
const o = value;
|
|
23463
|
+
return typeof o["serverName"] === "string" && o["serverName"].length > 0 && typeof o["proxyUrl"] === "string" && o["proxyUrl"].length > 0 && typeof o["targetUrl"] === "string" && o["targetUrl"].length > 0;
|
|
23464
|
+
}
|
|
23465
|
+
function localRowToProxyConfigItem(row) {
|
|
23466
|
+
return {
|
|
23467
|
+
proxy_url: row.proxyUrl,
|
|
23468
|
+
server_name: row.serverName,
|
|
23469
|
+
target_url: row.targetUrl
|
|
23470
|
+
};
|
|
23471
|
+
}
|
|
23472
|
+
async function readProxyConfigsFromLocalMulticornConfig() {
|
|
23473
|
+
let raw;
|
|
23474
|
+
try {
|
|
23475
|
+
raw = await readFile(getMulticornConfigPath(), "utf8");
|
|
23476
|
+
} catch {
|
|
23477
|
+
return [];
|
|
23478
|
+
}
|
|
23479
|
+
let parsed;
|
|
23480
|
+
try {
|
|
23481
|
+
parsed = JSON.parse(raw);
|
|
23482
|
+
} catch {
|
|
23483
|
+
return [];
|
|
23484
|
+
}
|
|
23485
|
+
if (typeof parsed !== "object" || parsed === null) return [];
|
|
23486
|
+
const list = parsed["proxyConfigs"];
|
|
23487
|
+
if (!Array.isArray(list)) return [];
|
|
23488
|
+
const out = [];
|
|
23489
|
+
for (const row of list) {
|
|
23490
|
+
if (isLocalProxyConfigRow(row)) {
|
|
23491
|
+
out.push(localRowToProxyConfigItem(row));
|
|
23492
|
+
}
|
|
23493
|
+
}
|
|
23494
|
+
return out;
|
|
23495
|
+
}
|
|
22838
23496
|
function noProxyConfigStatusMessage(dashboardUrl) {
|
|
22839
23497
|
const base = dashboardUrl.replace(/\/+$/, "");
|
|
22840
23498
|
return `Multicorn Shield is active but no hosted proxy configurations were found for your account.
|
|
@@ -22854,11 +23512,14 @@ var ARGS_INPUT_JSON_SCHEMA = ARGS_OBJECT_SCHEMA !== void 0 ? toJsonSchemaCompat(
|
|
|
22854
23512
|
function readApiKey() {
|
|
22855
23513
|
const key = process.env["MULTICORN_API_KEY"]?.trim();
|
|
22856
23514
|
if (key === void 0 || key.length === 0) return null;
|
|
23515
|
+
if (key.startsWith("${")) return null;
|
|
22857
23516
|
return key;
|
|
22858
23517
|
}
|
|
22859
23518
|
function readBaseUrl() {
|
|
22860
23519
|
const raw = process.env["MULTICORN_BASE_URL"]?.trim();
|
|
22861
|
-
|
|
23520
|
+
if (raw === void 0 || raw.length === 0) return "https://api.multicorn.ai";
|
|
23521
|
+
if (raw.startsWith("${")) return "https://api.multicorn.ai";
|
|
23522
|
+
return raw;
|
|
22862
23523
|
}
|
|
22863
23524
|
function readAgentName() {
|
|
22864
23525
|
const raw = process.env["MULTICORN_AGENT_NAME"]?.trim();
|
|
@@ -22869,6 +23530,44 @@ function readLogLevel() {
|
|
|
22869
23530
|
if (raw !== void 0 && isValidLogLevel(raw)) return raw;
|
|
22870
23531
|
return "info";
|
|
22871
23532
|
}
|
|
23533
|
+
async function autoCreateProxyConfig(baseUrl, apiKey, serverName, entry, agentName) {
|
|
23534
|
+
const targetUrl = `stdio://${entry.command}/${entry.args.join("/")}`;
|
|
23535
|
+
const url2 = `${baseUrl.replace(/\/+$/, "")}/api/v1/proxy/config`;
|
|
23536
|
+
debugLog2(`[SHIELD] Auto-creating proxy config for "${serverName}".`);
|
|
23537
|
+
let response;
|
|
23538
|
+
try {
|
|
23539
|
+
response = await fetch(url2, {
|
|
23540
|
+
method: "POST",
|
|
23541
|
+
headers: {
|
|
23542
|
+
"Content-Type": "application/json",
|
|
23543
|
+
"X-Multicorn-Key": apiKey
|
|
23544
|
+
},
|
|
23545
|
+
body: JSON.stringify({
|
|
23546
|
+
server_name: serverName,
|
|
23547
|
+
target_url: targetUrl,
|
|
23548
|
+
agent_name: agentName
|
|
23549
|
+
}),
|
|
23550
|
+
signal: AbortSignal.timeout(SETUP_TIMEOUT_MS)
|
|
23551
|
+
});
|
|
23552
|
+
} catch (error2) {
|
|
23553
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
23554
|
+
debugLog2(`[SHIELD] Failed to create proxy config for "${serverName}": ${message}`);
|
|
23555
|
+
return false;
|
|
23556
|
+
}
|
|
23557
|
+
if (response.status === 409) {
|
|
23558
|
+
debugLog2(`[SHIELD] Proxy config for "${serverName}" already exists (409), skipping.`);
|
|
23559
|
+
return false;
|
|
23560
|
+
}
|
|
23561
|
+
if (!response.ok) {
|
|
23562
|
+
const body = await response.text().catch(() => "");
|
|
23563
|
+
debugLog2(
|
|
23564
|
+
`[SHIELD] Failed to create proxy config for "${serverName}": HTTP ${String(response.status)} ${body.slice(0, 200)}`
|
|
23565
|
+
);
|
|
23566
|
+
return false;
|
|
23567
|
+
}
|
|
23568
|
+
debugLog2(`[SHIELD] Proxy config created for "${serverName}".`);
|
|
23569
|
+
return true;
|
|
23570
|
+
}
|
|
22872
23571
|
async function runShieldExtension() {
|
|
22873
23572
|
const debugBaseUrl = process.env["MULTICORN_BASE_URL"] ?? "";
|
|
22874
23573
|
const debugApiKeyPrefix = process.env["MULTICORN_API_KEY"]?.slice(0, 8) ?? "";
|
|
@@ -22893,7 +23592,8 @@ async function runShieldExtension() {
|
|
|
22893
23592
|
{ name: "multicorn-shield", version: PACKAGE_VERSION },
|
|
22894
23593
|
{ capabilities: { tools: { listChanged: true } } }
|
|
22895
23594
|
);
|
|
22896
|
-
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
23595
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
23596
|
+
await readyPromise;
|
|
22897
23597
|
const tools = Array.from(toolRegistry.entries()).map(([name, entry]) => ({
|
|
22898
23598
|
name,
|
|
22899
23599
|
description: entry.description,
|
|
@@ -22941,7 +23641,7 @@ async function runShieldExtension() {
|
|
|
22941
23641
|
const setupTimeout = setTimeout(() => {
|
|
22942
23642
|
rejectReady(
|
|
22943
23643
|
new Error(
|
|
22944
|
-
"[SHIELD] Background setup timed out after 15 seconds. Steps include
|
|
23644
|
+
"[SHIELD] Background setup timed out after 15 seconds. Steps include proxy config resolution, hosted proxy sessions, and Shield runtime. See ~/.multicorn/extension-debug.log for earlier [SHIELD] lines."
|
|
22945
23645
|
)
|
|
22946
23646
|
);
|
|
22947
23647
|
}, SETUP_TIMEOUT_MS);
|
|
@@ -22953,27 +23653,68 @@ async function runShieldExtension() {
|
|
|
22953
23653
|
} else {
|
|
22954
23654
|
logger.warn("Could not read Claude Desktop config. No MCP backup was written.", {});
|
|
22955
23655
|
}
|
|
23656
|
+
const discoveredServers = {};
|
|
23657
|
+
if (desktop !== null) {
|
|
23658
|
+
for (const [name, entry] of Object.entries(desktop.mcpServers)) {
|
|
23659
|
+
if (!isShieldExtensionEntry(name, entry)) {
|
|
23660
|
+
discoveredServers[name] = entry;
|
|
23661
|
+
}
|
|
23662
|
+
}
|
|
23663
|
+
}
|
|
23664
|
+
const serverCount = Object.keys(discoveredServers).length;
|
|
23665
|
+
debugLog2(
|
|
23666
|
+
`[SHIELD] Config read; ${String(serverCount)} MCP server(s) discovered (excluding Shield).`
|
|
23667
|
+
);
|
|
23668
|
+
debugLog2("[SHIELD] Resolving proxy configs (local config or API).");
|
|
22956
23669
|
let configs;
|
|
22957
|
-
|
|
22958
|
-
|
|
22959
|
-
|
|
22960
|
-
|
|
22961
|
-
|
|
22962
|
-
|
|
22963
|
-
|
|
22964
|
-
|
|
22965
|
-
|
|
22966
|
-
|
|
22967
|
-
|
|
22968
|
-
|
|
22969
|
-
|
|
22970
|
-
|
|
22971
|
-
|
|
22972
|
-
|
|
22973
|
-
|
|
22974
|
-
|
|
23670
|
+
const localConfigs = await readProxyConfigsFromLocalMulticornConfig();
|
|
23671
|
+
if (localConfigs.length > 0) {
|
|
23672
|
+
debugLog2(`[SHIELD] Loaded ${String(localConfigs.length)} proxy configs from local config.`);
|
|
23673
|
+
configs = localConfigs;
|
|
23674
|
+
} else {
|
|
23675
|
+
debugLog2("[SHIELD] No local proxy configs; fetching from API.");
|
|
23676
|
+
try {
|
|
23677
|
+
configs = await fetchProxyConfigs(baseUrl, apiKey, SETUP_TIMEOUT_MS);
|
|
23678
|
+
} catch (e) {
|
|
23679
|
+
clearTimeout(setupTimeout);
|
|
23680
|
+
if (e instanceof ProxyConfigFetchError) {
|
|
23681
|
+
const msg = e.kind === "auth" ? e.message : `${e.message} (${dashboardUrl.replace(/\/+$/, "")}/proxy)`;
|
|
23682
|
+
toolRegistry.set("multicorn_shield_status", {
|
|
23683
|
+
description: "Reports Shield API or proxy config errors during extension setup.",
|
|
23684
|
+
call: () => Promise.resolve({
|
|
23685
|
+
isError: true,
|
|
23686
|
+
content: [{ type: "text", text: msg }]
|
|
23687
|
+
})
|
|
23688
|
+
});
|
|
23689
|
+
await server.sendToolListChanged();
|
|
23690
|
+
debugLog2(`[SHIELD] Proxy config fetch failed (${e.kind}); status tool only.`);
|
|
23691
|
+
resolveReady();
|
|
23692
|
+
return;
|
|
23693
|
+
}
|
|
23694
|
+
throw e;
|
|
23695
|
+
}
|
|
23696
|
+
debugLog2(`[SHIELD] Fetched ${String(configs.length)} proxy config(s) from API.`);
|
|
23697
|
+
if (serverCount > 0) {
|
|
23698
|
+
const existingNames = new Set(configs.map((c) => c.server_name));
|
|
23699
|
+
let createdCount = 0;
|
|
23700
|
+
for (const [name, entry] of Object.entries(discoveredServers)) {
|
|
23701
|
+
if (!existingNames.has(name)) {
|
|
23702
|
+
const created = await autoCreateProxyConfig(baseUrl, apiKey, name, entry, agentName);
|
|
23703
|
+
if (created) createdCount += 1;
|
|
23704
|
+
}
|
|
23705
|
+
}
|
|
23706
|
+
if (createdCount > 0) {
|
|
23707
|
+
debugLog2(
|
|
23708
|
+
`[SHIELD] Auto-created ${String(createdCount)} proxy config(s); re-fetching from API.`
|
|
23709
|
+
);
|
|
23710
|
+
try {
|
|
23711
|
+
configs = await fetchProxyConfigs(baseUrl, apiKey, SETUP_TIMEOUT_MS);
|
|
23712
|
+
} catch (e) {
|
|
23713
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
23714
|
+
debugLog2(`[SHIELD] Re-fetch after auto-creation failed: ${message}`);
|
|
23715
|
+
}
|
|
23716
|
+
}
|
|
22975
23717
|
}
|
|
22976
|
-
throw e;
|
|
22977
23718
|
}
|
|
22978
23719
|
debugLog2(`[SHIELD] Proxy config count: ${String(configs.length)}.`);
|
|
22979
23720
|
if (configs.length === 0) {
|
|
@@ -22997,7 +23738,7 @@ async function runShieldExtension() {
|
|
|
22997
23738
|
dashboardUrl,
|
|
22998
23739
|
logger
|
|
22999
23740
|
});
|
|
23000
|
-
debugLog2("[SHIELD] Starting extension runtime (
|
|
23741
|
+
debugLog2("[SHIELD] Starting extension runtime (hosted proxy path).");
|
|
23001
23742
|
await runtime.start();
|
|
23002
23743
|
debugLog2(
|
|
23003
23744
|
`[SHIELD] Runtime ready agentId=${runtime.getAgentId().length > 0 ? "(set)" : "(empty)"} authInvalid=${String(runtime.isAuthInvalid())}`
|
|
@@ -23070,7 +23811,7 @@ async function runShieldExtension() {
|
|
|
23070
23811
|
});
|
|
23071
23812
|
}
|
|
23072
23813
|
await server.sendToolListChanged();
|
|
23073
|
-
debugLog2("[SHIELD] Setup complete; signaling ready.");
|
|
23814
|
+
debugLog2("[SHIELD] Setup complete (hosted proxy path); signaling ready.");
|
|
23074
23815
|
clearTimeout(setupTimeout);
|
|
23075
23816
|
resolveReady();
|
|
23076
23817
|
} catch (error2) {
|