multicorn-shield 0.2.0 → 0.2.2
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/README.md +22 -0
- package/dist/multicorn-proxy.js +34 -17
- package/dist/multicorn-shield.js +118 -0
- package/dist/openclaw-hook/handler.js +2 -2
- package/dist/openclaw-plugin/multicorn-shield.js +2 -2
- package/dist/proxy.cjs +457 -0
- package/dist/proxy.d.cts +235 -0
- package/dist/proxy.d.ts +235 -0
- package/dist/proxy.js +438 -0
- package/dist/shield-extension.js +23117 -0
- package/package.json +51 -21
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JSON-RPC message parsing and tool call interception for the MCP proxy.
|
|
5
|
+
*
|
|
6
|
+
* Handles the MCP stdio transport (newline-delimited JSON-RPC 2.0). Provides
|
|
7
|
+
* types, parsers, and response builders used by the proxy to intercept
|
|
8
|
+
* `tools/call` requests before they reach the wrapped MCP server.
|
|
9
|
+
*
|
|
10
|
+
* @module proxy/interceptor
|
|
11
|
+
*/
|
|
12
|
+
interface JsonRpcRequest {
|
|
13
|
+
readonly jsonrpc: "2.0";
|
|
14
|
+
readonly id: string | number | null;
|
|
15
|
+
readonly method: string;
|
|
16
|
+
readonly params?: unknown;
|
|
17
|
+
}
|
|
18
|
+
interface JsonRpcResponse {
|
|
19
|
+
readonly jsonrpc: "2.0";
|
|
20
|
+
readonly id: string | number | null;
|
|
21
|
+
readonly result?: unknown;
|
|
22
|
+
readonly error?: JsonRpcError;
|
|
23
|
+
}
|
|
24
|
+
interface JsonRpcError {
|
|
25
|
+
readonly code: number;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}
|
|
28
|
+
interface ToolCallParams {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly arguments: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
declare function parseJsonRpcLine(line: string): JsonRpcRequest | null;
|
|
33
|
+
declare function extractToolCallParams(request: JsonRpcRequest): ToolCallParams | null;
|
|
34
|
+
declare function buildBlockedResponse(id: string | number | null, service: string, permissionLevel: string, dashboardUrl: string): JsonRpcResponse;
|
|
35
|
+
declare function buildSpendingBlockedResponse(id: string | number | null, reason: string, dashboardUrl: string): JsonRpcResponse;
|
|
36
|
+
/**
|
|
37
|
+
* Internal error: Shield could not verify permissions due to an exception.
|
|
38
|
+
* Use when the proxy hits an unhandled error (fail-closed).
|
|
39
|
+
*/
|
|
40
|
+
declare function buildInternalErrorResponse(id: string | number | null): JsonRpcResponse;
|
|
41
|
+
/**
|
|
42
|
+
* Service unreachable: Shield could not verify permissions (DNS, network, timeout).
|
|
43
|
+
* Distinct code (-32003) so callers can tell apart from permission-denied (-32000).
|
|
44
|
+
*/
|
|
45
|
+
declare function buildServiceUnreachableResponse(id: string | number | null, dashboardUrl: string): JsonRpcResponse;
|
|
46
|
+
/**
|
|
47
|
+
* API key invalid or revoked (401/403 from service).
|
|
48
|
+
* Distinct code (-32004) so callers can tell apart from permission-denied (-32000).
|
|
49
|
+
*/
|
|
50
|
+
declare function buildAuthErrorResponse(id: string | number | null): JsonRpcResponse;
|
|
51
|
+
declare function extractServiceFromToolName(toolName: string): string;
|
|
52
|
+
declare function extractActionFromToolName(toolName: string): string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Permission levels that can be granted on a service scope.
|
|
56
|
+
*
|
|
57
|
+
* - `read`: observe data without modification
|
|
58
|
+
* - `write`: create or modify data
|
|
59
|
+
* - `execute`: trigger side-effects (e.g. send an email, make a payment)
|
|
60
|
+
* - `publish`: make existing content publicly accessible (e.g. deploy, publish, make live)
|
|
61
|
+
* - `create`: create new content that is immediately public (e.g. tweet, public commit, forum post)
|
|
62
|
+
*/
|
|
63
|
+
declare const PERMISSION_LEVELS: {
|
|
64
|
+
readonly Read: "read";
|
|
65
|
+
readonly Write: "write";
|
|
66
|
+
readonly Execute: "execute";
|
|
67
|
+
readonly Publish: "publish";
|
|
68
|
+
readonly Create: "create";
|
|
69
|
+
};
|
|
70
|
+
type PermissionLevel = (typeof PERMISSION_LEVELS)[keyof typeof PERMISSION_LEVELS];
|
|
71
|
+
/**
|
|
72
|
+
* A single permission scope binding a service to an access level.
|
|
73
|
+
*
|
|
74
|
+
* For example, `{ service: "gmail", permissionLevel: "write" }` grants
|
|
75
|
+
* write access to the Gmail integration.
|
|
76
|
+
*/
|
|
77
|
+
interface Scope {
|
|
78
|
+
readonly service: string;
|
|
79
|
+
readonly permissionLevel: PermissionLevel;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Structured JSON logger for the MCP proxy.
|
|
84
|
+
*
|
|
85
|
+
* Writes to stderr only. Stdout is reserved for the MCP JSON-RPC transport.
|
|
86
|
+
* Log output is newline-delimited JSON so it can be parsed by log aggregators.
|
|
87
|
+
*
|
|
88
|
+
* @module proxy/logger
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
declare const LOG_LEVELS: {
|
|
92
|
+
readonly debug: 0;
|
|
93
|
+
readonly info: 1;
|
|
94
|
+
readonly warn: 2;
|
|
95
|
+
readonly error: 3;
|
|
96
|
+
};
|
|
97
|
+
type LogLevel = keyof typeof LOG_LEVELS;
|
|
98
|
+
interface ProxyLogger {
|
|
99
|
+
debug(msg: string, data?: Record<string, unknown>): void;
|
|
100
|
+
info(msg: string, data?: Record<string, unknown>): void;
|
|
101
|
+
warn(msg: string, data?: Record<string, unknown>): void;
|
|
102
|
+
error(msg: string, data?: Record<string, unknown>): void;
|
|
103
|
+
}
|
|
104
|
+
declare function createLogger(level: LogLevel, output?: Writable): ProxyLogger;
|
|
105
|
+
declare function isValidLogLevel(value: unknown): value is LogLevel;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Agent registration, scope fetching, consent flow, and scope caching.
|
|
109
|
+
*
|
|
110
|
+
* Handles the full lifecycle of connecting a proxy to the Multicorn service:
|
|
111
|
+
* finding or creating an agent record, fetching its granted scopes, and
|
|
112
|
+
* triggering the browser consent flow when the agent has no scopes yet.
|
|
113
|
+
*
|
|
114
|
+
* Scopes are cached in `~/.multicorn/scopes.json` for offline resilience
|
|
115
|
+
* and refreshed from the service every 60 seconds while the proxy runs.
|
|
116
|
+
* Cache is account-aware (keyed by agent name + API key).
|
|
117
|
+
*
|
|
118
|
+
* @module proxy/consent
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Derives the dashboard URL from the API base URL.
|
|
123
|
+
* - http://localhost:8080 -> http://localhost:5173
|
|
124
|
+
* - https://api.multicorn.ai -> https://app.multicorn.ai
|
|
125
|
+
* - Other URLs: replace "api" with "app" in the hostname, or use default
|
|
126
|
+
*/
|
|
127
|
+
declare function deriveDashboardUrl(baseUrl: string): string;
|
|
128
|
+
/**
|
|
129
|
+
* Thrown when the Shield API returns 401 or 403 (API key invalid or revoked).
|
|
130
|
+
* Used so resolveAgentRecord can detect auth failure without ad-hoc property casts.
|
|
131
|
+
*/
|
|
132
|
+
declare class ShieldAuthError extends Error {
|
|
133
|
+
constructor(message: string);
|
|
134
|
+
}
|
|
135
|
+
interface AgentRecord {
|
|
136
|
+
readonly id: string;
|
|
137
|
+
readonly name: string;
|
|
138
|
+
readonly scopes: readonly Scope[];
|
|
139
|
+
/** Set when the service returned 401/403; proxy must block with auth error message. */
|
|
140
|
+
readonly authInvalid?: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
declare function findAgentByName(agentName: string, apiKey: string, baseUrl: string): Promise<AgentRecord | null>;
|
|
144
|
+
declare function registerAgent(agentName: string, apiKey: string, baseUrl: string, platform?: string): Promise<string>;
|
|
145
|
+
declare function fetchGrantedScopes(agentId: string, apiKey: string, baseUrl: string): Promise<readonly Scope[]>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Scope validation engine.
|
|
149
|
+
*
|
|
150
|
+
* Given a set of granted scopes and a requested action, determines whether
|
|
151
|
+
* the action is permitted. Returns a structured result with a human-readable
|
|
152
|
+
* reason when access is denied. No silent failures, no implicit grants.
|
|
153
|
+
*
|
|
154
|
+
* **Design principle (Jordan persona):** Every permission must be explicitly
|
|
155
|
+
* granted. `read` does **not** imply `write`; `write` does **not** imply
|
|
156
|
+
* `execute`. There is no wildcard or superuser bypass.
|
|
157
|
+
*
|
|
158
|
+
* @module scopes/scope-validator
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The outcome of a scope validation check.
|
|
163
|
+
*
|
|
164
|
+
* - When `allowed` is `true`, no `reason` is present.
|
|
165
|
+
* - When `allowed` is `false`, `reason` contains a descriptive explanation
|
|
166
|
+
* of why the action was denied.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* const result = validateScopeAccess(granted, requested);
|
|
171
|
+
* if (!result.allowed) {
|
|
172
|
+
* console.warn(`Denied: ${result.reason}`);
|
|
173
|
+
* }
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
interface ValidationResult {
|
|
177
|
+
/** Whether the requested action is permitted by the granted scopes. */
|
|
178
|
+
readonly allowed: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Human-readable explanation of why the action was denied.
|
|
181
|
+
* Only present when `allowed` is `false`.
|
|
182
|
+
*/
|
|
183
|
+
readonly reason?: string;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Check whether a single requested scope is covered by the granted set.
|
|
187
|
+
*
|
|
188
|
+
* Matching is **exact**: the granted set must contain an entry with the
|
|
189
|
+
* same `service` **and** `permissionLevel`. No implicit escalation is
|
|
190
|
+
* performed (e.g. `write` does not subsume `read`).
|
|
191
|
+
*
|
|
192
|
+
* @param grantedScopes - The scopes the agent has been granted via consent.
|
|
193
|
+
* @param requested - The scope required by the action the agent wants to perform.
|
|
194
|
+
* @returns A {@link ValidationResult} indicating whether access is allowed.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* const granted = [
|
|
199
|
+
* { service: "gmail", permissionLevel: "read" },
|
|
200
|
+
* { service: "gmail", permissionLevel: "write" },
|
|
201
|
+
* ];
|
|
202
|
+
*
|
|
203
|
+
* validateScopeAccess(granted, { service: "gmail", permissionLevel: "read" });
|
|
204
|
+
* // → { allowed: true }
|
|
205
|
+
*
|
|
206
|
+
* validateScopeAccess(granted, { service: "gmail", permissionLevel: "execute" });
|
|
207
|
+
* // → { allowed: false, reason: "Permission \"execute\" is not granted …" }
|
|
208
|
+
*
|
|
209
|
+
* validateScopeAccess(granted, { service: "slack", permissionLevel: "read" });
|
|
210
|
+
* // → { allowed: false, reason: "No permissions granted for service \"slack\" …" }
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
declare function validateScopeAccess(grantedScopes: readonly Scope[], requested: Scope): ValidationResult;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Maps MCP tool names (stdio servers, Claude Desktop) to Shield service and permission level.
|
|
217
|
+
*
|
|
218
|
+
* Uses explicit tables for common MCP servers (filesystem, terminal, browser) and the same
|
|
219
|
+
* integration-style prefix rules as OpenClaw's tool-mapper for names like `gmail_send_email`.
|
|
220
|
+
*
|
|
221
|
+
* @module mcp-tool-mapper
|
|
222
|
+
*/
|
|
223
|
+
|
|
224
|
+
interface McpToolScopeMapping {
|
|
225
|
+
readonly service: string;
|
|
226
|
+
readonly permissionLevel: PermissionLevel;
|
|
227
|
+
/** Original tool name for audit logs. */
|
|
228
|
+
readonly actionType: string;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Maps an MCP `tools/call` tool name to Shield `service` + `permissionLevel` for scope checks.
|
|
232
|
+
*/
|
|
233
|
+
declare function mapMcpToolToScope(toolName: string): McpToolScopeMapping;
|
|
234
|
+
|
|
235
|
+
export { type AgentRecord, type JsonRpcError, type JsonRpcRequest, type JsonRpcResponse, type LogLevel, type ProxyLogger, ShieldAuthError, type ToolCallParams, buildAuthErrorResponse, buildBlockedResponse, buildInternalErrorResponse, buildServiceUnreachableResponse, buildSpendingBlockedResponse, createLogger, deriveDashboardUrl, extractActionFromToolName, extractServiceFromToolName, extractToolCallParams, fetchGrantedScopes, findAgentByName, isValidLogLevel, mapMcpToolToScope, parseJsonRpcLine, registerAgent, validateScopeAccess };
|
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 };
|