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.
@@ -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.2.2"};
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 (first permission-style block). Skipped if API key is invalid.
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
- return raw !== void 0 && raw.length > 0 ? raw : "https://api.multicorn.ai";
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 backup of Claude config, fetching proxy configs from Shield, initializing hosted proxy sessions and listing tools. See ~/.multicorn/extension-debug.log for earlier [SHIELD] lines."
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
- try {
22958
- debugLog2("[SHIELD] Fetching proxy configs from Shield API.");
22959
- configs = await fetchProxyConfigs(baseUrl, apiKey, SETUP_TIMEOUT_MS);
22960
- } catch (e) {
22961
- clearTimeout(setupTimeout);
22962
- if (e instanceof ProxyConfigFetchError) {
22963
- const msg = e.kind === "auth" ? e.message : `${e.message} (${dashboardUrl.replace(/\/+$/, "")}/proxy)`;
22964
- toolRegistry.set("multicorn_shield_status", {
22965
- description: "Reports Shield API or proxy config errors during extension setup.",
22966
- call: () => Promise.resolve({
22967
- isError: true,
22968
- content: [{ type: "text", text: msg }]
22969
- })
22970
- });
22971
- await server.sendToolListChanged();
22972
- debugLog2(`[SHIELD] Proxy config fetch failed (${e.kind}); status tool only.`);
22973
- resolveReady();
22974
- return;
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 (agent resolution).");
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) {