@papicandela/mcx-core 0.2.1 → 0.2.5
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/.turbo/turbo-build.log +2 -2
- package/dist/index.js +313 -110
- package/package.json +8 -2
- package/src/adapter.ts +8 -4
- package/src/config.ts +6 -0
- package/src/executor.ts +5 -3
- package/src/sandbox/analyzer/analyzer.test.ts +3 -3
- package/src/sandbox/analyzer/analyzer.ts +20 -6
- package/src/sandbox/analyzer/rules/no-dangerous-globals.ts +135 -33
- package/src/sandbox/analyzer/rules/no-infinite-loop.ts +40 -13
- package/src/sandbox/bun-worker.ts +67 -11
- package/src/sandbox/network-policy.ts +110 -54
- package/src/type-generator.ts +23 -9
|
@@ -75,83 +75,139 @@ export function generateNetworkIsolationCode(policy: NetworkPolicy): string {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
if (policy.mode === "blocked") {
|
|
78
|
+
// SECURITY: Wrap in IIFE to prevent user code from accessing __original_fetch
|
|
79
|
+
// Use Object.defineProperty with writable:false to prevent user code from overwriting
|
|
78
80
|
return `
|
|
79
81
|
// Network isolation: BLOCKED
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
82
|
+
(function() {
|
|
83
|
+
const blockedFetch = async function() {
|
|
84
|
+
throw new Error('Network access is blocked in sandbox. Use adapters instead.');
|
|
85
|
+
};
|
|
86
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
87
|
+
value: blockedFetch,
|
|
88
|
+
writable: false,
|
|
89
|
+
configurable: false
|
|
90
|
+
});
|
|
91
|
+
})();
|
|
84
92
|
|
|
85
93
|
// Block XMLHttpRequest
|
|
86
|
-
globalThis
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
94
|
+
Object.defineProperty(globalThis, 'XMLHttpRequest', {
|
|
95
|
+
value: class {
|
|
96
|
+
constructor() {
|
|
97
|
+
throw new Error('XMLHttpRequest is blocked in sandbox.');
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
writable: false,
|
|
101
|
+
configurable: false
|
|
102
|
+
});
|
|
91
103
|
|
|
92
104
|
// Block WebSocket
|
|
93
|
-
globalThis
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
105
|
+
Object.defineProperty(globalThis, 'WebSocket', {
|
|
106
|
+
value: class {
|
|
107
|
+
constructor() {
|
|
108
|
+
throw new Error('WebSocket is blocked in sandbox.');
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
writable: false,
|
|
112
|
+
configurable: false
|
|
113
|
+
});
|
|
98
114
|
|
|
99
115
|
// Block EventSource (SSE)
|
|
100
|
-
globalThis
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
116
|
+
Object.defineProperty(globalThis, 'EventSource', {
|
|
117
|
+
value: class {
|
|
118
|
+
constructor() {
|
|
119
|
+
throw new Error('EventSource is blocked in sandbox.');
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
writable: false,
|
|
123
|
+
configurable: false
|
|
124
|
+
});
|
|
105
125
|
`;
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
// mode === 'allowed' - whitelist specific domains
|
|
129
|
+
// SECURITY: Wrap in IIFE to prevent user code from accessing internals
|
|
130
|
+
// Use Object.defineProperty with writable:false to prevent user code from overwriting
|
|
109
131
|
const domainsJson = JSON.stringify(policy.domains);
|
|
110
132
|
return `
|
|
111
133
|
// Network isolation: ALLOWED (whitelist)
|
|
112
|
-
|
|
113
|
-
const
|
|
134
|
+
(function() {
|
|
135
|
+
const _domains = ${domainsJson};
|
|
136
|
+
const _real_fetch = globalThis.fetch;
|
|
114
137
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return __allowed_domains.some(d => hostname === d || hostname.endsWith('.' + d));
|
|
119
|
-
} catch {
|
|
120
|
-
return false;
|
|
138
|
+
// Block private/link-local IPs to prevent DNS rebinding attacks
|
|
139
|
+
function _isPrivateIp(hostname) {
|
|
140
|
+
return /^(localhost|127\\.|10\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.|169\\.254\\.|\\[::1\\]|\\[fc|\\[fd)/.test(hostname);
|
|
121
141
|
}
|
|
122
|
-
}
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
function _isUrlAllowed(url) {
|
|
144
|
+
try {
|
|
145
|
+
const parsed = new URL(url);
|
|
146
|
+
// Only allow http/https protocols
|
|
147
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return false;
|
|
148
|
+
const hostname = parsed.hostname;
|
|
149
|
+
if (!hostname || _isPrivateIp(hostname)) return false;
|
|
150
|
+
return _domains.some(d => d && (hostname === d || hostname.endsWith('.' + d)));
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
129
154
|
}
|
|
130
|
-
return __original_fetch(url, options);
|
|
131
|
-
};
|
|
132
155
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
156
|
+
const whitelistedFetch = async function(url, options) {
|
|
157
|
+
// Safely extract URL string from various input types
|
|
158
|
+
let urlStr;
|
|
159
|
+
try {
|
|
160
|
+
if (typeof url === 'string') urlStr = url;
|
|
161
|
+
else if (url instanceof URL) urlStr = url.toString();
|
|
162
|
+
else if (url && typeof url.url === 'string') urlStr = url.url;
|
|
163
|
+
else throw new Error('Invalid URL type');
|
|
164
|
+
} catch {
|
|
165
|
+
throw new Error('Network access blocked: could not determine request URL.');
|
|
166
|
+
}
|
|
167
|
+
if (!_isUrlAllowed(urlStr)) {
|
|
168
|
+
throw new Error('Network access blocked: domain not in allowed list.');
|
|
169
|
+
}
|
|
170
|
+
return _real_fetch(url, options);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
174
|
+
value: whitelistedFetch,
|
|
175
|
+
writable: false,
|
|
176
|
+
configurable: false
|
|
177
|
+
});
|
|
178
|
+
})();
|
|
139
179
|
|
|
140
|
-
// Block
|
|
141
|
-
globalThis
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
throw new Error('
|
|
180
|
+
// Block XMLHttpRequest (not easily whitelistable)
|
|
181
|
+
Object.defineProperty(globalThis, 'XMLHttpRequest', {
|
|
182
|
+
value: class {
|
|
183
|
+
constructor() {
|
|
184
|
+
throw new Error('XMLHttpRequest is blocked. Use fetch() with allowed domains.');
|
|
145
185
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
186
|
+
},
|
|
187
|
+
writable: false,
|
|
188
|
+
configurable: false
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Block WebSocket - opaque error to prevent allowlist enumeration
|
|
192
|
+
Object.defineProperty(globalThis, 'WebSocket', {
|
|
193
|
+
value: class {
|
|
194
|
+
constructor() {
|
|
195
|
+
throw new Error('WebSocket is not supported in sandbox.');
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
writable: false,
|
|
199
|
+
configurable: false
|
|
200
|
+
});
|
|
149
201
|
|
|
150
202
|
// Block EventSource
|
|
151
|
-
globalThis
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
203
|
+
Object.defineProperty(globalThis, 'EventSource', {
|
|
204
|
+
value: class {
|
|
205
|
+
constructor() {
|
|
206
|
+
throw new Error('EventSource is blocked in sandbox.');
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
writable: false,
|
|
210
|
+
configurable: false
|
|
211
|
+
});
|
|
156
212
|
`;
|
|
157
213
|
}
|
package/src/type-generator.ts
CHANGED
|
@@ -48,7 +48,8 @@ export function generateTypes(
|
|
|
48
48
|
// Generate input interface for each tool with parameters
|
|
49
49
|
for (const [toolName, tool] of Object.entries(adapter.tools)) {
|
|
50
50
|
if (tool.parameters && Object.keys(tool.parameters).length > 0) {
|
|
51
|
-
const
|
|
51
|
+
const safeToolNameForType = sanitizeIdentifier(toolName);
|
|
52
|
+
const inputTypeName = `${capitalize(safeName)}_${capitalize(safeToolNameForType)}_Input`;
|
|
52
53
|
lines.push(generateInputInterface(inputTypeName, tool.parameters, includeDescriptions));
|
|
53
54
|
lines.push("");
|
|
54
55
|
}
|
|
@@ -56,17 +57,18 @@ export function generateTypes(
|
|
|
56
57
|
|
|
57
58
|
// Generate adapter declaration
|
|
58
59
|
if (includeDescriptions && adapter.description) {
|
|
59
|
-
lines.push(`/** ${adapter.description} */`);
|
|
60
|
+
lines.push(`/** ${sanitizeJSDoc(adapter.description)} */`);
|
|
60
61
|
}
|
|
61
62
|
lines.push(`declare const ${safeName}: {`);
|
|
62
63
|
|
|
63
64
|
for (const [toolName, tool] of Object.entries(adapter.tools)) {
|
|
64
65
|
const safeToolName = sanitizeIdentifier(toolName);
|
|
65
66
|
const hasParams = tool.parameters && Object.keys(tool.parameters).length > 0;
|
|
66
|
-
|
|
67
|
+
// Use sanitized tool name in type name to ensure consistency
|
|
68
|
+
const inputTypeName = `${capitalize(safeName)}_${capitalize(safeToolName)}_Input`;
|
|
67
69
|
|
|
68
70
|
if (includeDescriptions && tool.description) {
|
|
69
|
-
lines.push(` /** ${tool.description} */`);
|
|
71
|
+
lines.push(` /** ${sanitizeJSDoc(tool.description)} */`);
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
const paramStr = hasParams ? `params: ${inputTypeName}` : "";
|
|
@@ -83,7 +85,8 @@ export function generateTypes(
|
|
|
83
85
|
|
|
84
86
|
/**
|
|
85
87
|
* Generate a compact type summary for token-constrained contexts.
|
|
86
|
-
*
|
|
88
|
+
* Only shows adapter names and method count to minimize context usage.
|
|
89
|
+
* Use mcx_search to discover specific methods.
|
|
87
90
|
*
|
|
88
91
|
* @param adapters - Array of adapters
|
|
89
92
|
* @returns Compact summary string
|
|
@@ -91,8 +94,8 @@ export function generateTypes(
|
|
|
91
94
|
export function generateTypesSummary(adapters: Adapter[]): string {
|
|
92
95
|
return adapters
|
|
93
96
|
.map((adapter) => {
|
|
94
|
-
const
|
|
95
|
-
return
|
|
97
|
+
const count = Object.keys(adapter.tools).length;
|
|
98
|
+
return `- ${adapter.name} (${count} methods)`;
|
|
96
99
|
})
|
|
97
100
|
.join("\n");
|
|
98
101
|
}
|
|
@@ -108,13 +111,16 @@ function generateInputInterface(
|
|
|
108
111
|
const lines: string[] = [`interface ${typeName} {`];
|
|
109
112
|
|
|
110
113
|
for (const [paramName, param] of Object.entries(parameters)) {
|
|
114
|
+
// Sanitize parameter name to prevent injection
|
|
115
|
+
const safeParamName = sanitizeIdentifier(paramName);
|
|
116
|
+
|
|
111
117
|
if (includeDescriptions && param.description) {
|
|
112
|
-
lines.push(` /** ${param.description} */`);
|
|
118
|
+
lines.push(` /** ${sanitizeJSDoc(param.description)} */`);
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
const tsType = paramTypeToTS(param.type);
|
|
116
122
|
const optional = param.required === false ? "?" : "";
|
|
117
|
-
lines.push(` ${
|
|
123
|
+
lines.push(` ${safeParamName}${optional}: ${tsType};`);
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
lines.push("}");
|
|
@@ -170,3 +176,11 @@ export function sanitizeIdentifier(name: string): string {
|
|
|
170
176
|
function capitalize(str: string): string {
|
|
171
177
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
172
178
|
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sanitize text for use in JSDoc comments.
|
|
182
|
+
* Prevents comment injection via `*/` sequences.
|
|
183
|
+
*/
|
|
184
|
+
function sanitizeJSDoc(text: string): string {
|
|
185
|
+
return text.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
|
|
186
|
+
}
|