multicorn-shield 0.2.1 → 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/proxy.js ADDED
@@ -0,0 +1,438 @@
1
+ import 'child_process';
2
+ import 'stream';
3
+
4
+ // src/proxy/interceptor.ts
5
+ var BLOCKED_ERROR_CODE = -32e3;
6
+ var SPENDING_BLOCKED_ERROR_CODE = -32001;
7
+ var INTERNAL_ERROR_CODE = -32002;
8
+ var SERVICE_UNREACHABLE_ERROR_CODE = -32003;
9
+ var AUTH_ERROR_CODE = -32004;
10
+ function parseJsonRpcLine(line) {
11
+ const trimmed = line.trim();
12
+ if (trimmed.length === 0) return null;
13
+ let parsed;
14
+ try {
15
+ parsed = JSON.parse(trimmed);
16
+ } catch {
17
+ return null;
18
+ }
19
+ return isJsonRpcRequest(parsed) ? parsed : null;
20
+ }
21
+ function extractToolCallParams(request) {
22
+ if (request.method !== "tools/call") return null;
23
+ if (typeof request.params !== "object" || request.params === null) return null;
24
+ const params = request.params;
25
+ const name = params["name"];
26
+ const args = params["arguments"];
27
+ if (typeof name !== "string") return null;
28
+ if (typeof args !== "object" || args === null) return null;
29
+ return { name, arguments: args };
30
+ }
31
+ function buildBlockedResponse(id, service, permissionLevel, dashboardUrl) {
32
+ const displayService = capitalize(service);
33
+ const message = `Action blocked by Multicorn Shield: agent does not have ${permissionLevel} access to ${displayService}. Configure permissions at ${dashboardUrl}`;
34
+ return {
35
+ jsonrpc: "2.0",
36
+ id,
37
+ error: {
38
+ code: BLOCKED_ERROR_CODE,
39
+ message
40
+ }
41
+ };
42
+ }
43
+ function buildSpendingBlockedResponse(id, reason, dashboardUrl) {
44
+ const message = `Action blocked by Multicorn Shield: ${reason}. Review spending limits at ${dashboardUrl}`;
45
+ return {
46
+ jsonrpc: "2.0",
47
+ id,
48
+ error: {
49
+ code: SPENDING_BLOCKED_ERROR_CODE,
50
+ message
51
+ }
52
+ };
53
+ }
54
+ function buildInternalErrorResponse(id) {
55
+ const message = "Action blocked: Shield encountered an internal error and cannot verify permissions. Check proxy logs for details.";
56
+ return {
57
+ jsonrpc: "2.0",
58
+ id,
59
+ error: {
60
+ code: INTERNAL_ERROR_CODE,
61
+ message
62
+ }
63
+ };
64
+ }
65
+ function buildServiceUnreachableResponse(id, dashboardUrl) {
66
+ const message = `Action blocked: Shield cannot verify permissions (service unreachable). Configure offline behaviour at ${dashboardUrl}`;
67
+ return {
68
+ jsonrpc: "2.0",
69
+ id,
70
+ error: {
71
+ code: SERVICE_UNREACHABLE_ERROR_CODE,
72
+ message
73
+ }
74
+ };
75
+ }
76
+ function buildAuthErrorResponse(id) {
77
+ const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-proxy init to reconfigure.";
78
+ return {
79
+ jsonrpc: "2.0",
80
+ id,
81
+ error: {
82
+ code: AUTH_ERROR_CODE,
83
+ message
84
+ }
85
+ };
86
+ }
87
+ function extractServiceFromToolName(toolName) {
88
+ const idx = toolName.indexOf("_");
89
+ return idx === -1 ? toolName : toolName.slice(0, idx);
90
+ }
91
+ function extractActionFromToolName(toolName) {
92
+ const idx = toolName.indexOf("_");
93
+ return idx === -1 ? "call" : toolName.slice(idx + 1);
94
+ }
95
+ function isJsonRpcRequest(value) {
96
+ if (typeof value !== "object" || value === null) return false;
97
+ const obj = value;
98
+ if (obj["jsonrpc"] !== "2.0") return false;
99
+ if (typeof obj["method"] !== "string") return false;
100
+ const id = obj["id"];
101
+ const validId = id === null || id === void 0 || typeof id === "string" || typeof id === "number";
102
+ return validId;
103
+ }
104
+ function capitalize(str) {
105
+ if (str.length === 0) return str;
106
+ const first = str[0];
107
+ return first !== void 0 ? first.toUpperCase() + str.slice(1) : str;
108
+ }
109
+ function deriveDashboardUrl(baseUrl) {
110
+ try {
111
+ const url = new URL(baseUrl);
112
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
113
+ url.port = "5173";
114
+ url.protocol = "http:";
115
+ return url.toString();
116
+ }
117
+ if (url.hostname === "api.multicorn.ai") {
118
+ url.hostname = "app.multicorn.ai";
119
+ return url.toString();
120
+ }
121
+ if (url.hostname.includes("api")) {
122
+ url.hostname = url.hostname.replace("api", "app");
123
+ return url.toString();
124
+ }
125
+ if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
126
+ return "https://app.multicorn.ai";
127
+ }
128
+ return "https://app.multicorn.ai";
129
+ } catch {
130
+ return "https://app.multicorn.ai";
131
+ }
132
+ }
133
+ var ShieldAuthError = class _ShieldAuthError extends Error {
134
+ constructor(message) {
135
+ super(message);
136
+ this.name = "ShieldAuthError";
137
+ Object.setPrototypeOf(this, _ShieldAuthError.prototype);
138
+ }
139
+ };
140
+ async function findAgentByName(agentName, apiKey, baseUrl) {
141
+ let response;
142
+ try {
143
+ response = await fetch(`${baseUrl}/api/v1/agents`, {
144
+ headers: { "X-Multicorn-Key": apiKey },
145
+ signal: AbortSignal.timeout(8e3)
146
+ });
147
+ } catch {
148
+ return null;
149
+ }
150
+ if (!response.ok) {
151
+ if (response.status === 401 || response.status === 403) {
152
+ return { id: "", name: agentName, scopes: [], authInvalid: true };
153
+ }
154
+ return null;
155
+ }
156
+ let body;
157
+ try {
158
+ body = await response.json();
159
+ } catch {
160
+ return null;
161
+ }
162
+ if (!isApiSuccessResponse(body)) return null;
163
+ const agents = body.data;
164
+ if (!Array.isArray(agents)) return null;
165
+ const match = agents.find(
166
+ (a) => isAgentSummaryShape(a) && a.name === agentName
167
+ );
168
+ if (match === void 0) return null;
169
+ return { id: match.id, name: match.name, scopes: [] };
170
+ }
171
+ async function registerAgent(agentName, apiKey, baseUrl, platform) {
172
+ const response = await fetch(`${baseUrl}/api/v1/agents`, {
173
+ method: "POST",
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ "X-Multicorn-Key": apiKey
177
+ },
178
+ body: JSON.stringify({ name: agentName, ...platform ? { platform } : {} }),
179
+ signal: AbortSignal.timeout(8e3)
180
+ });
181
+ if (!response.ok) {
182
+ if (response.status === 401 || response.status === 403) {
183
+ throw new ShieldAuthError(
184
+ `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
185
+ );
186
+ }
187
+ throw new Error(
188
+ `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
189
+ );
190
+ }
191
+ const body = await response.json();
192
+ if (!isApiSuccessResponse(body)) {
193
+ throw new Error(`Failed to register agent "${agentName}": unexpected response format.`);
194
+ }
195
+ if (!isAgentSummaryShape(body.data)) {
196
+ throw new Error(`Failed to register agent "${agentName}": response missing agent ID.`);
197
+ }
198
+ return body.data.id;
199
+ }
200
+ async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
201
+ let response;
202
+ try {
203
+ response = await fetch(`${baseUrl}/api/v1/agents/${agentId}`, {
204
+ headers: { "X-Multicorn-Key": apiKey },
205
+ signal: AbortSignal.timeout(8e3)
206
+ });
207
+ } catch {
208
+ return [];
209
+ }
210
+ if (!response.ok) return [];
211
+ const body = await response.json();
212
+ if (!isApiSuccessResponse(body)) return [];
213
+ const agentDetail = body.data;
214
+ if (!isAgentDetailShape(agentDetail)) return [];
215
+ const scopes = [];
216
+ for (const perm of agentDetail.permissions) {
217
+ if (!isPermissionShape(perm)) continue;
218
+ if (perm.revoked_at !== null) continue;
219
+ if (perm.read) scopes.push({ service: perm.service, permissionLevel: "read" });
220
+ if (perm.write) scopes.push({ service: perm.service, permissionLevel: "write" });
221
+ if (perm.execute) scopes.push({ service: perm.service, permissionLevel: "execute" });
222
+ }
223
+ return scopes;
224
+ }
225
+ function isApiSuccessResponse(value) {
226
+ if (typeof value !== "object" || value === null) return false;
227
+ const obj = value;
228
+ return obj["success"] === true;
229
+ }
230
+ function isAgentSummaryShape(value) {
231
+ if (typeof value !== "object" || value === null) return false;
232
+ const obj = value;
233
+ return typeof obj["id"] === "string" && typeof obj["name"] === "string";
234
+ }
235
+ function isAgentDetailShape(value) {
236
+ if (typeof value !== "object" || value === null) return false;
237
+ const obj = value;
238
+ return Array.isArray(obj["permissions"]);
239
+ }
240
+ function isPermissionShape(value) {
241
+ if (typeof value !== "object" || value === null) return false;
242
+ const obj = value;
243
+ 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");
244
+ }
245
+ var LOG_LEVELS = {
246
+ debug: 0,
247
+ info: 1,
248
+ warn: 2,
249
+ error: 3
250
+ };
251
+ function createLogger(level, output = process.stderr) {
252
+ const minLevel = LOG_LEVELS[level];
253
+ function write(logLevel, msg, data) {
254
+ if (LOG_LEVELS[logLevel] < minLevel) return;
255
+ const entry = {
256
+ level: logLevel,
257
+ time: (/* @__PURE__ */ new Date()).toISOString(),
258
+ msg,
259
+ ...data
260
+ };
261
+ output.write(JSON.stringify(entry) + "\n");
262
+ }
263
+ return {
264
+ debug: (msg, data) => {
265
+ write("debug", msg, data);
266
+ },
267
+ info: (msg, data) => {
268
+ write("info", msg, data);
269
+ },
270
+ warn: (msg, data) => {
271
+ write("warn", msg, data);
272
+ },
273
+ error: (msg, data) => {
274
+ write("error", msg, data);
275
+ }
276
+ };
277
+ }
278
+ function isValidLogLevel(value) {
279
+ return typeof value === "string" && Object.hasOwn(LOG_LEVELS, value);
280
+ }
281
+
282
+ // src/types/index.ts
283
+ var PERMISSION_LEVELS = {
284
+ Read: "read",
285
+ Write: "write",
286
+ Execute: "execute",
287
+ Publish: "publish",
288
+ Create: "create"
289
+ };
290
+
291
+ // src/scopes/scope-parser.ts
292
+ var VALID_PERMISSION_LEVELS = new Set(Object.values(PERMISSION_LEVELS));
293
+ [...VALID_PERMISSION_LEVELS].join(", ");
294
+ function formatScope(scope) {
295
+ return `${scope.permissionLevel}:${scope.service}`;
296
+ }
297
+
298
+ // src/scopes/scope-validator.ts
299
+ function validateScopeAccess(grantedScopes, requested) {
300
+ const isGranted = grantedScopes.some(
301
+ (granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
302
+ );
303
+ if (isGranted) {
304
+ return { allowed: true };
305
+ }
306
+ const serviceScopes = grantedScopes.filter((g) => g.service === requested.service);
307
+ if (serviceScopes.length > 0) {
308
+ const grantedLevels = serviceScopes.map((g) => `"${g.permissionLevel}"`).join(", ");
309
+ return {
310
+ allowed: false,
311
+ reason: `Permission "${requested.permissionLevel}" is not granted for service "${requested.service}". Currently granted permission level(s): ${grantedLevels}. Requested scope "${formatScope(requested)}" requires explicit consent.`
312
+ };
313
+ }
314
+ return {
315
+ allowed: false,
316
+ 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.`
317
+ };
318
+ }
319
+
320
+ // src/mcp-tool-mapper.ts
321
+ var FILESYSTEM_READ_TOOLS = /* @__PURE__ */ new Set([
322
+ "read_file",
323
+ "read_text_file",
324
+ "read_media_file",
325
+ "read_multiple_files",
326
+ "list_directory",
327
+ "list_dir",
328
+ "directory_tree",
329
+ "tree",
330
+ "get_file_info",
331
+ "stat",
332
+ "search_files",
333
+ "glob_file_search",
334
+ "list_allowed_directories",
335
+ "file_search"
336
+ ]);
337
+ var FILESYSTEM_WRITE_TOOLS = /* @__PURE__ */ new Set([
338
+ "write_file",
339
+ "edit_file",
340
+ "create_directory",
341
+ "mkdir",
342
+ "move_file",
343
+ "rename",
344
+ "delete_file",
345
+ "remove_file",
346
+ "copy_file"
347
+ ]);
348
+ var TERMINAL_EXECUTE_TOOLS = /* @__PURE__ */ new Set([
349
+ "run_terminal_cmd",
350
+ "execute_command",
351
+ "terminal_run",
352
+ "run_command"
353
+ ]);
354
+ var BROWSER_EXECUTE_TOOLS = /* @__PURE__ */ new Set([
355
+ "web_fetch",
356
+ "fetch_url",
357
+ "browser_navigate",
358
+ "navigate",
359
+ "mcp_web_fetch"
360
+ ]);
361
+ var INTEGRATION_SERVICE_BY_PREFIX = {
362
+ gmail: "gmail",
363
+ google_calendar: "google_calendar",
364
+ calendar: "google_calendar",
365
+ google_drive: "google_drive",
366
+ drive: "google_drive",
367
+ slack: "slack",
368
+ payments: "payments",
369
+ payment: "payments",
370
+ stripe: "payments",
371
+ github: "github",
372
+ gitlab: "gitlab",
373
+ notion: "notion",
374
+ linear: "linear",
375
+ jira: "jira"
376
+ };
377
+ function inferPermissionFromToolName(normalized) {
378
+ if (normalized.includes("_read") || normalized.includes("_get") || normalized.includes("_list") || normalized.endsWith("_fetch") || normalized.includes("_search")) {
379
+ return "read";
380
+ }
381
+ 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")) {
382
+ return "write";
383
+ }
384
+ return "execute";
385
+ }
386
+ function mapMcpToolToScope(toolName) {
387
+ const actionType = toolName.trim();
388
+ const normalized = actionType.toLowerCase();
389
+ if (normalized.length === 0) {
390
+ return { service: "unknown", permissionLevel: "execute", actionType };
391
+ }
392
+ if (FILESYSTEM_READ_TOOLS.has(normalized)) {
393
+ return { service: "filesystem", permissionLevel: "read", actionType };
394
+ }
395
+ if (FILESYSTEM_WRITE_TOOLS.has(normalized)) {
396
+ return { service: "filesystem", permissionLevel: "write", actionType };
397
+ }
398
+ if (TERMINAL_EXECUTE_TOOLS.has(normalized)) {
399
+ return { service: "terminal", permissionLevel: "execute", actionType };
400
+ }
401
+ if (BROWSER_EXECUTE_TOOLS.has(normalized)) {
402
+ return { service: "browser", permissionLevel: "execute", actionType };
403
+ }
404
+ if (normalized === "read") {
405
+ return { service: "filesystem", permissionLevel: "read", actionType };
406
+ }
407
+ if (normalized === "write" || normalized === "edit") {
408
+ return { service: "filesystem", permissionLevel: "write", actionType };
409
+ }
410
+ if (normalized === "exec") {
411
+ return { service: "terminal", permissionLevel: "execute", actionType };
412
+ }
413
+ if (normalized.startsWith("git_")) {
414
+ const permissionLevel2 = inferPermissionFromToolName(normalized);
415
+ return { service: "git", permissionLevel: permissionLevel2, actionType };
416
+ }
417
+ for (const [prefix, service] of Object.entries(INTEGRATION_SERVICE_BY_PREFIX)) {
418
+ if (normalized.startsWith(`${prefix}_`) || normalized === prefix) {
419
+ const permissionLevel2 = inferPermissionFromToolName(normalized);
420
+ return { service, permissionLevel: permissionLevel2, actionType };
421
+ }
422
+ }
423
+ const idx = normalized.indexOf("_");
424
+ if (idx === -1) {
425
+ return { service: normalized, permissionLevel: "execute", actionType };
426
+ }
427
+ const head = normalized.slice(0, idx);
428
+ const tail = normalized.slice(idx + 1);
429
+ let permissionLevel = "execute";
430
+ if (tail.includes("read") || tail.includes("list") || tail.includes("get") || tail.includes("search") || tail.includes("fetch")) {
431
+ permissionLevel = "read";
432
+ } else if (tail.includes("write") || tail.includes("send") || tail.includes("create") || tail.includes("update") || tail.includes("delete") || tail.includes("remove")) {
433
+ permissionLevel = "write";
434
+ }
435
+ return { service: head, permissionLevel, actionType };
436
+ }
437
+
438
+ export { ShieldAuthError, buildAuthErrorResponse, buildBlockedResponse, buildInternalErrorResponse, buildServiceUnreachableResponse, buildSpendingBlockedResponse, createLogger, deriveDashboardUrl, extractActionFromToolName, extractServiceFromToolName, extractToolCallParams, fetchGrantedScopes, findAgentByName, isValidLogLevel, mapMcpToolToScope, parseJsonRpcLine, registerAgent, validateScopeAccess };