@papicandela/mcx-core 0.2.2 → 0.2.6
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 +388 -113
- 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/index.ts +1 -0
- package/src/sandbox/analyzer/analyzer.test.ts +3 -3
- package/src/sandbox/analyzer/analyzer.ts +2 -2
- 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 +107 -13
- package/src/sandbox/network-policy.ts +110 -54
- package/src/type-generator.ts +86 -15
- package/src/types.ts +4 -0
package/dist/index.js
CHANGED
|
@@ -32,80 +32,132 @@ function generateNetworkIsolationCode(policy) {
|
|
|
32
32
|
if (policy.mode === "blocked") {
|
|
33
33
|
return `
|
|
34
34
|
// Network isolation: BLOCKED
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
};
|
|
35
|
+
(function() {
|
|
36
|
+
const blockedFetch = async function() {
|
|
37
|
+
throw new Error('Network access is blocked in sandbox. Use adapters instead.');
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
40
|
+
value: blockedFetch,
|
|
41
|
+
writable: false,
|
|
42
|
+
configurable: false
|
|
43
|
+
});
|
|
44
|
+
})();
|
|
39
45
|
|
|
40
46
|
// Block XMLHttpRequest
|
|
41
|
-
globalThis
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
47
|
+
Object.defineProperty(globalThis, 'XMLHttpRequest', {
|
|
48
|
+
value: class {
|
|
49
|
+
constructor() {
|
|
50
|
+
throw new Error('XMLHttpRequest is blocked in sandbox.');
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
writable: false,
|
|
54
|
+
configurable: false
|
|
55
|
+
});
|
|
46
56
|
|
|
47
57
|
// Block WebSocket
|
|
48
|
-
globalThis
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
58
|
+
Object.defineProperty(globalThis, 'WebSocket', {
|
|
59
|
+
value: class {
|
|
60
|
+
constructor() {
|
|
61
|
+
throw new Error('WebSocket is blocked in sandbox.');
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
writable: false,
|
|
65
|
+
configurable: false
|
|
66
|
+
});
|
|
53
67
|
|
|
54
68
|
// Block EventSource (SSE)
|
|
55
|
-
globalThis
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
69
|
+
Object.defineProperty(globalThis, 'EventSource', {
|
|
70
|
+
value: class {
|
|
71
|
+
constructor() {
|
|
72
|
+
throw new Error('EventSource is blocked in sandbox.');
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
writable: false,
|
|
76
|
+
configurable: false
|
|
77
|
+
});
|
|
60
78
|
`;
|
|
61
79
|
}
|
|
62
80
|
const domainsJson = JSON.stringify(policy.domains);
|
|
63
81
|
return `
|
|
64
82
|
// Network isolation: ALLOWED (whitelist)
|
|
65
|
-
|
|
66
|
-
const
|
|
83
|
+
(function() {
|
|
84
|
+
const _domains = ${domainsJson};
|
|
85
|
+
const _real_fetch = globalThis.fetch;
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return __allowed_domains.some(d => hostname === d || hostname.endsWith('.' + d));
|
|
72
|
-
} catch {
|
|
73
|
-
return false;
|
|
87
|
+
// Block private/link-local IPs to prevent DNS rebinding attacks
|
|
88
|
+
function _isPrivateIp(hostname) {
|
|
89
|
+
return /^(localhost|127\\.|10\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.|169\\.254\\.|\\[::1\\]|\\[fc|\\[fd)/.test(hostname);
|
|
74
90
|
}
|
|
75
|
-
}
|
|
76
91
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
function _isUrlAllowed(url) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = new URL(url);
|
|
95
|
+
// Only allow http/https protocols
|
|
96
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return false;
|
|
97
|
+
const hostname = parsed.hostname;
|
|
98
|
+
if (!hostname || _isPrivateIp(hostname)) return false;
|
|
99
|
+
return _domains.some(d => d && (hostname === d || hostname.endsWith('.' + d)));
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
82
103
|
}
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
|
|
105
|
+
const whitelistedFetch = async function(url, options) {
|
|
106
|
+
// Safely extract URL string from various input types
|
|
107
|
+
let urlStr;
|
|
108
|
+
try {
|
|
109
|
+
if (typeof url === 'string') urlStr = url;
|
|
110
|
+
else if (url instanceof URL) urlStr = url.toString();
|
|
111
|
+
else if (url && typeof url.url === 'string') urlStr = url.url;
|
|
112
|
+
else throw new Error('Invalid URL type');
|
|
113
|
+
} catch {
|
|
114
|
+
throw new Error('Network access blocked: could not determine request URL.');
|
|
115
|
+
}
|
|
116
|
+
if (!_isUrlAllowed(urlStr)) {
|
|
117
|
+
throw new Error('Network access blocked: domain not in allowed list.');
|
|
118
|
+
}
|
|
119
|
+
return _real_fetch(url, options);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
123
|
+
value: whitelistedFetch,
|
|
124
|
+
writable: false,
|
|
125
|
+
configurable: false
|
|
126
|
+
});
|
|
127
|
+
})();
|
|
85
128
|
|
|
86
129
|
// Block XMLHttpRequest (not easily whitelistable)
|
|
87
|
-
globalThis
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
130
|
+
Object.defineProperty(globalThis, 'XMLHttpRequest', {
|
|
131
|
+
value: class {
|
|
132
|
+
constructor() {
|
|
133
|
+
throw new Error('XMLHttpRequest is blocked. Use fetch() with allowed domains.');
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
writable: false,
|
|
137
|
+
configurable: false
|
|
138
|
+
});
|
|
92
139
|
|
|
93
|
-
// Block WebSocket
|
|
94
|
-
globalThis
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
throw new Error('WebSocket
|
|
140
|
+
// Block WebSocket - opaque error to prevent allowlist enumeration
|
|
141
|
+
Object.defineProperty(globalThis, 'WebSocket', {
|
|
142
|
+
value: class {
|
|
143
|
+
constructor() {
|
|
144
|
+
throw new Error('WebSocket is not supported in sandbox.');
|
|
98
145
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
},
|
|
147
|
+
writable: false,
|
|
148
|
+
configurable: false
|
|
149
|
+
});
|
|
102
150
|
|
|
103
151
|
// Block EventSource
|
|
104
|
-
globalThis
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
152
|
+
Object.defineProperty(globalThis, 'EventSource', {
|
|
153
|
+
value: class {
|
|
154
|
+
constructor() {
|
|
155
|
+
throw new Error('EventSource is blocked in sandbox.');
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
writable: false,
|
|
159
|
+
configurable: false
|
|
160
|
+
});
|
|
109
161
|
`;
|
|
110
162
|
}
|
|
111
163
|
|
|
@@ -5587,24 +5639,31 @@ function isLiteralTrue(node) {
|
|
|
5587
5639
|
return false;
|
|
5588
5640
|
return node.type === "Literal" && node.value === true;
|
|
5589
5641
|
}
|
|
5590
|
-
function
|
|
5642
|
+
function hasExitStatement(node) {
|
|
5591
5643
|
if (node.type === "BreakStatement")
|
|
5592
5644
|
return true;
|
|
5645
|
+
if (node.type === "ReturnStatement")
|
|
5646
|
+
return true;
|
|
5647
|
+
if (node.type === "ThrowStatement")
|
|
5648
|
+
return true;
|
|
5593
5649
|
if (node.type === "WhileStatement" || node.type === "ForStatement" || node.type === "ForInStatement" || node.type === "ForOfStatement" || node.type === "DoWhileStatement" || node.type === "SwitchStatement") {
|
|
5594
5650
|
return false;
|
|
5595
5651
|
}
|
|
5652
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
5653
|
+
return false;
|
|
5654
|
+
}
|
|
5596
5655
|
for (const key of Object.keys(node)) {
|
|
5597
5656
|
const child = node[key];
|
|
5598
5657
|
if (child && typeof child === "object") {
|
|
5599
5658
|
if (Array.isArray(child)) {
|
|
5600
5659
|
for (const item of child) {
|
|
5601
5660
|
if (item && typeof item === "object" && "type" in item) {
|
|
5602
|
-
if (
|
|
5661
|
+
if (hasExitStatement(item))
|
|
5603
5662
|
return true;
|
|
5604
5663
|
}
|
|
5605
5664
|
}
|
|
5606
5665
|
} else if ("type" in child) {
|
|
5607
|
-
if (
|
|
5666
|
+
if (hasExitStatement(child))
|
|
5608
5667
|
return true;
|
|
5609
5668
|
}
|
|
5610
5669
|
}
|
|
@@ -5614,25 +5673,35 @@ function hasBreak(node) {
|
|
|
5614
5673
|
var rule = {
|
|
5615
5674
|
name: "no-infinite-loop",
|
|
5616
5675
|
severity: "error",
|
|
5617
|
-
description: "Disallow infinite loops without
|
|
5618
|
-
visits: ["WhileStatement", "ForStatement"],
|
|
5676
|
+
description: "Disallow infinite loops without exit statements",
|
|
5677
|
+
visits: ["WhileStatement", "ForStatement", "DoWhileStatement"],
|
|
5619
5678
|
visitors: {
|
|
5620
5679
|
WhileStatement(node, context) {
|
|
5621
5680
|
const whileNode = node;
|
|
5622
|
-
if (isLiteralTrue(whileNode.test) && !
|
|
5681
|
+
if (isLiteralTrue(whileNode.test) && !hasExitStatement(whileNode.body)) {
|
|
5623
5682
|
context.report({
|
|
5624
5683
|
severity: "error",
|
|
5625
|
-
message: "Infinite loop: while(true) without break",
|
|
5684
|
+
message: "Infinite loop: while(true) without break/return/throw",
|
|
5626
5685
|
line: context.getLine(node)
|
|
5627
5686
|
});
|
|
5628
5687
|
}
|
|
5629
5688
|
},
|
|
5630
5689
|
ForStatement(node, context) {
|
|
5631
5690
|
const forNode = node;
|
|
5632
|
-
if (!forNode.test && !
|
|
5691
|
+
if (!forNode.test && !hasExitStatement(forNode.body)) {
|
|
5692
|
+
context.report({
|
|
5693
|
+
severity: "error",
|
|
5694
|
+
message: "Infinite loop: for(;;) without break/return/throw",
|
|
5695
|
+
line: context.getLine(node)
|
|
5696
|
+
});
|
|
5697
|
+
}
|
|
5698
|
+
},
|
|
5699
|
+
DoWhileStatement(node, context) {
|
|
5700
|
+
const doWhileNode = node;
|
|
5701
|
+
if (isLiteralTrue(doWhileNode.test) && !hasExitStatement(doWhileNode.body)) {
|
|
5633
5702
|
context.report({
|
|
5634
5703
|
severity: "error",
|
|
5635
|
-
message: "Infinite loop:
|
|
5704
|
+
message: "Infinite loop: do...while(true) without break/return/throw",
|
|
5636
5705
|
line: context.getLine(node)
|
|
5637
5706
|
});
|
|
5638
5707
|
}
|
|
@@ -5938,49 +6007,100 @@ var EVAL_NAME = "ev" + "al";
|
|
|
5938
6007
|
var FUNC_CONSTRUCTOR = "Func" + "tion";
|
|
5939
6008
|
var REQUIRE_NAME = "req" + "uire";
|
|
5940
6009
|
var PROCESS_NAME = "pro" + "cess";
|
|
6010
|
+
var DANGEROUS_GLOBALS = ["globalThis", "self", "window"];
|
|
6011
|
+
function isConstructorOnFunction(node) {
|
|
6012
|
+
const prop = node.property;
|
|
6013
|
+
const isConstructorAccess = !node.computed && prop.type === "Identifier" && prop.name === "constructor" || node.computed && prop.type === "Literal" && prop.value === "constructor";
|
|
6014
|
+
if (!isConstructorAccess)
|
|
6015
|
+
return false;
|
|
6016
|
+
const obj = node.object;
|
|
6017
|
+
if (obj.type === "FunctionExpression" || obj.type === "ArrowFunctionExpression") {
|
|
6018
|
+
return true;
|
|
6019
|
+
}
|
|
6020
|
+
if (obj.type === "CallExpression") {
|
|
6021
|
+
const call = obj;
|
|
6022
|
+
if (call.callee.type === "MemberExpression") {
|
|
6023
|
+
const callee = call.callee;
|
|
6024
|
+
if (callee.object.type === "Identifier" && callee.object.name === "Object" && callee.property.type === "Identifier" && callee.property.name === "getPrototypeOf") {
|
|
6025
|
+
return true;
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
return false;
|
|
6030
|
+
}
|
|
5941
6031
|
var rule5 = {
|
|
5942
6032
|
name: "no-dangerous-globals",
|
|
5943
6033
|
severity: "warn",
|
|
5944
|
-
description: "
|
|
5945
|
-
visits: ["CallExpression", "NewExpression", "
|
|
6034
|
+
description: "Block dangerous globals that could escape sandbox",
|
|
6035
|
+
visits: ["CallExpression", "NewExpression", "MemberExpression"],
|
|
5946
6036
|
visitors: {
|
|
5947
6037
|
CallExpression(node, context) {
|
|
5948
6038
|
const callExpr = node;
|
|
5949
|
-
|
|
6039
|
+
const calleeName = callExpr.callee.type === "Identifier" ? callExpr.callee.name : null;
|
|
6040
|
+
if (calleeName === EVAL_NAME) {
|
|
5950
6041
|
context.report({
|
|
5951
|
-
severity: "
|
|
5952
|
-
message: `${EVAL_NAME}() is
|
|
6042
|
+
severity: "error",
|
|
6043
|
+
message: `${EVAL_NAME}() is blocked in sandbox - potential code injection`,
|
|
5953
6044
|
line: context.getLine(node)
|
|
5954
6045
|
});
|
|
5955
6046
|
return;
|
|
5956
6047
|
}
|
|
5957
|
-
if (
|
|
6048
|
+
if (calleeName === FUNC_CONSTRUCTOR) {
|
|
5958
6049
|
context.report({
|
|
5959
|
-
severity: "
|
|
5960
|
-
message: `${
|
|
6050
|
+
severity: "error",
|
|
6051
|
+
message: `${FUNC_CONSTRUCTOR}() is blocked in sandbox - potential code injection`,
|
|
5961
6052
|
line: context.getLine(node)
|
|
5962
6053
|
});
|
|
5963
6054
|
return;
|
|
5964
6055
|
}
|
|
5965
|
-
|
|
5966
|
-
NewExpression(node, context) {
|
|
5967
|
-
const newExpr = node;
|
|
5968
|
-
if (newExpr.callee.type === "Identifier" && newExpr.callee.name === FUNC_CONSTRUCTOR) {
|
|
6056
|
+
if (calleeName === REQUIRE_NAME) {
|
|
5969
6057
|
context.report({
|
|
5970
|
-
severity: "
|
|
5971
|
-
message: `${
|
|
6058
|
+
severity: "error",
|
|
6059
|
+
message: `${REQUIRE_NAME}() is blocked in sandbox - could access dangerous modules`,
|
|
5972
6060
|
line: context.getLine(node)
|
|
5973
6061
|
});
|
|
6062
|
+
return;
|
|
6063
|
+
}
|
|
6064
|
+
if (callExpr.callee.type === "MemberExpression") {
|
|
6065
|
+
if (isConstructorOnFunction(callExpr.callee)) {
|
|
6066
|
+
context.report({
|
|
6067
|
+
severity: "error",
|
|
6068
|
+
message: `Accessing .constructor on functions is blocked - potential sandbox escape`,
|
|
6069
|
+
line: context.getLine(node)
|
|
6070
|
+
});
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
5974
6073
|
}
|
|
5975
6074
|
},
|
|
5976
|
-
|
|
5977
|
-
const
|
|
5978
|
-
if (
|
|
6075
|
+
NewExpression(node, context) {
|
|
6076
|
+
const newExpr = node;
|
|
6077
|
+
if (newExpr.callee.type === "Identifier" && newExpr.callee.name === FUNC_CONSTRUCTOR) {
|
|
5979
6078
|
context.report({
|
|
5980
|
-
severity: "
|
|
5981
|
-
message:
|
|
6079
|
+
severity: "error",
|
|
6080
|
+
message: `${FUNC_CONSTRUCTOR} constructor is blocked in sandbox - potential code injection`,
|
|
5982
6081
|
line: context.getLine(node)
|
|
5983
6082
|
});
|
|
6083
|
+
return;
|
|
6084
|
+
}
|
|
6085
|
+
if (newExpr.callee.type === "MemberExpression") {
|
|
6086
|
+
if (isConstructorOnFunction(newExpr.callee)) {
|
|
6087
|
+
context.report({
|
|
6088
|
+
severity: "error",
|
|
6089
|
+
message: `Accessing .constructor on functions is blocked - potential sandbox escape`,
|
|
6090
|
+
line: context.getLine(node)
|
|
6091
|
+
});
|
|
6092
|
+
return;
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
if (newExpr.callee.type === "MemberExpression") {
|
|
6096
|
+
const member = newExpr.callee;
|
|
6097
|
+
if (member.object.type === "Identifier" && DANGEROUS_GLOBALS.includes(member.object.name) && member.property.type === "Identifier" && member.property.name === FUNC_CONSTRUCTOR) {
|
|
6098
|
+
context.report({
|
|
6099
|
+
severity: "error",
|
|
6100
|
+
message: `${FUNC_CONSTRUCTOR} constructor is blocked in sandbox - potential code injection`,
|
|
6101
|
+
line: context.getLine(node)
|
|
6102
|
+
});
|
|
6103
|
+
}
|
|
5984
6104
|
}
|
|
5985
6105
|
},
|
|
5986
6106
|
MemberExpression(node, context) {
|
|
@@ -5991,6 +6111,14 @@ var rule5 = {
|
|
|
5991
6111
|
message: `'${PROCESS_NAME}' is not available in sandbox`,
|
|
5992
6112
|
line: context.getLine(node)
|
|
5993
6113
|
});
|
|
6114
|
+
return;
|
|
6115
|
+
}
|
|
6116
|
+
if (memberExpr.object.type === "Identifier" && DANGEROUS_GLOBALS.includes(memberExpr.object.name) && memberExpr.property.type === "Identifier" && memberExpr.property.name === FUNC_CONSTRUCTOR) {
|
|
6117
|
+
context.report({
|
|
6118
|
+
severity: "error",
|
|
6119
|
+
message: `Accessing ${FUNC_CONSTRUCTOR} via globals is blocked - potential sandbox escape`,
|
|
6120
|
+
line: context.getLine(node)
|
|
6121
|
+
});
|
|
5994
6122
|
}
|
|
5995
6123
|
}
|
|
5996
6124
|
}
|
|
@@ -6130,7 +6258,7 @@ function analyze(code, config = {}) {
|
|
|
6130
6258
|
const errors = findings.filter((f) => f.severity === "error");
|
|
6131
6259
|
const elapsed = performance.now() - start;
|
|
6132
6260
|
if (elapsed > 50) {
|
|
6133
|
-
console.
|
|
6261
|
+
console.error(`[mcx-analyzer] Exceeded 50ms budget: ${elapsed.toFixed(1)}ms`);
|
|
6134
6262
|
}
|
|
6135
6263
|
return { warnings, errors, elapsed };
|
|
6136
6264
|
}
|
|
@@ -6210,14 +6338,17 @@ class BunWorkerSandbox {
|
|
|
6210
6338
|
const url = URL.createObjectURL(blob);
|
|
6211
6339
|
const worker = new Worker(url);
|
|
6212
6340
|
let resolved = false;
|
|
6341
|
+
let timeoutId;
|
|
6213
6342
|
const cleanup = () => {
|
|
6214
6343
|
if (!resolved) {
|
|
6215
6344
|
resolved = true;
|
|
6345
|
+
if (timeoutId)
|
|
6346
|
+
clearTimeout(timeoutId);
|
|
6216
6347
|
worker.terminate();
|
|
6217
6348
|
URL.revokeObjectURL(url);
|
|
6218
6349
|
}
|
|
6219
6350
|
};
|
|
6220
|
-
|
|
6351
|
+
timeoutId = setTimeout(() => {
|
|
6221
6352
|
if (!resolved) {
|
|
6222
6353
|
cleanup();
|
|
6223
6354
|
resolve({
|
|
@@ -6229,6 +6360,8 @@ class BunWorkerSandbox {
|
|
|
6229
6360
|
}
|
|
6230
6361
|
}, this.config.timeout);
|
|
6231
6362
|
worker.onmessage = async (event) => {
|
|
6363
|
+
if (resolved)
|
|
6364
|
+
return;
|
|
6232
6365
|
const { type, ...data2 } = event.data;
|
|
6233
6366
|
if (type === "ready") {
|
|
6234
6367
|
worker.postMessage({ type: "execute", data: { code: normalizedCode } });
|
|
@@ -6246,7 +6379,6 @@ class BunWorkerSandbox {
|
|
|
6246
6379
|
worker.postMessage({ type: "adapter_result", data: { id, error } });
|
|
6247
6380
|
}
|
|
6248
6381
|
} else if (type === "result") {
|
|
6249
|
-
clearTimeout(timeoutId);
|
|
6250
6382
|
cleanup();
|
|
6251
6383
|
resolve({
|
|
6252
6384
|
success: data2.success,
|
|
@@ -6258,7 +6390,6 @@ class BunWorkerSandbox {
|
|
|
6258
6390
|
}
|
|
6259
6391
|
};
|
|
6260
6392
|
worker.onerror = (error) => {
|
|
6261
|
-
clearTimeout(timeoutId);
|
|
6262
6393
|
cleanup();
|
|
6263
6394
|
resolve({
|
|
6264
6395
|
success: false,
|
|
@@ -6287,11 +6418,27 @@ class BunWorkerSandbox {
|
|
|
6287
6418
|
const pendingCalls = new Map();
|
|
6288
6419
|
let callId = 0;
|
|
6289
6420
|
|
|
6421
|
+
// Safe stringify that handles BigInt and circular refs
|
|
6422
|
+
const safeStr = (val) => {
|
|
6423
|
+
if (typeof val !== 'object' || val === null) return String(val);
|
|
6424
|
+
try {
|
|
6425
|
+
const seen = new WeakSet();
|
|
6426
|
+
return JSON.stringify(val, (k, v) => {
|
|
6427
|
+
if (typeof v === 'bigint') return v.toString() + 'n';
|
|
6428
|
+
if (typeof v === 'object' && v !== null) {
|
|
6429
|
+
if (seen.has(v)) return '[Circular]';
|
|
6430
|
+
seen.add(v);
|
|
6431
|
+
}
|
|
6432
|
+
return v;
|
|
6433
|
+
});
|
|
6434
|
+
} catch { return String(val); }
|
|
6435
|
+
};
|
|
6436
|
+
|
|
6290
6437
|
const console = {
|
|
6291
|
-
log: (...args) => logs.push(args.map(
|
|
6292
|
-
warn: (...args) => logs.push('[WARN] ' + args.map(
|
|
6293
|
-
error: (...args) => logs.push('[ERROR] ' + args.map(
|
|
6294
|
-
info: (...args) => logs.push('[INFO] ' + args.map(
|
|
6438
|
+
log: (...args) => logs.push(args.map(safeStr).join(' ')),
|
|
6439
|
+
warn: (...args) => logs.push('[WARN] ' + args.map(safeStr).join(' ')),
|
|
6440
|
+
error: (...args) => logs.push('[ERROR] ' + args.map(safeStr).join(' ')),
|
|
6441
|
+
info: (...args) => logs.push('[INFO] ' + args.map(safeStr).join(' ')),
|
|
6295
6442
|
};
|
|
6296
6443
|
globalThis.console = console;
|
|
6297
6444
|
|
|
@@ -6341,25 +6488,73 @@ class BunWorkerSandbox {
|
|
|
6341
6488
|
return arr.slice(0, n);
|
|
6342
6489
|
};
|
|
6343
6490
|
|
|
6491
|
+
// SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
|
|
6492
|
+
const RESERVED_KEYS = new Set([
|
|
6493
|
+
'onmessage', 'postMessage', 'close', 'terminate', 'self',
|
|
6494
|
+
'constructor', 'prototype', '__proto__',
|
|
6495
|
+
'pendingCalls', 'callId', 'logs', 'console', 'adapters',
|
|
6496
|
+
'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
|
|
6497
|
+
'pick', 'table', 'count', 'sum', 'first', 'safeStr'
|
|
6498
|
+
]);
|
|
6499
|
+
|
|
6344
6500
|
self.onmessage = async (event) => {
|
|
6345
6501
|
const { type, data } = event.data;
|
|
6346
6502
|
|
|
6347
6503
|
if (type === 'init') {
|
|
6348
6504
|
const { variables, adapterMethods, globals } = data;
|
|
6349
6505
|
|
|
6506
|
+
// Inject user variables (skip reserved keys to prevent internal state corruption)
|
|
6350
6507
|
for (const [key, value] of Object.entries(variables || {})) {
|
|
6508
|
+
if (RESERVED_KEYS.has(key)) {
|
|
6509
|
+
logs.push('[WARN] Skipped reserved variable key: ' + key);
|
|
6510
|
+
continue;
|
|
6511
|
+
}
|
|
6351
6512
|
globalThis[key] = value;
|
|
6352
6513
|
}
|
|
6353
6514
|
|
|
6515
|
+
// Inject sandbox globals (skip reserved keys)
|
|
6354
6516
|
for (const [key, value] of Object.entries(globals || {})) {
|
|
6517
|
+
if (RESERVED_KEYS.has(key)) {
|
|
6518
|
+
logs.push('[WARN] Skipped reserved globals key: ' + key);
|
|
6519
|
+
continue;
|
|
6520
|
+
}
|
|
6355
6521
|
globalThis[key] = value;
|
|
6356
6522
|
}
|
|
6357
6523
|
|
|
6358
|
-
|
|
6524
|
+
// Levenshtein distance for fuzzy matching
|
|
6525
|
+
const levenshtein = (a, b) => {
|
|
6526
|
+
if (a.length === 0) return b.length;
|
|
6527
|
+
if (b.length === 0) return a.length;
|
|
6528
|
+
const matrix = [];
|
|
6529
|
+
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
|
6530
|
+
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
6531
|
+
for (let i = 1; i <= b.length; i++) {
|
|
6532
|
+
for (let j = 1; j <= a.length; j++) {
|
|
6533
|
+
matrix[i][j] = b[i-1] === a[j-1]
|
|
6534
|
+
? matrix[i-1][j-1]
|
|
6535
|
+
: Math.min(matrix[i-1][j-1] + 1, matrix[i][j-1] + 1, matrix[i-1][j] + 1);
|
|
6536
|
+
}
|
|
6537
|
+
}
|
|
6538
|
+
return matrix[b.length][a.length];
|
|
6539
|
+
};
|
|
6540
|
+
|
|
6541
|
+
// Find similar method names
|
|
6542
|
+
const findSimilar = (name, methods, maxDist = 3) => {
|
|
6543
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
6544
|
+
return methods
|
|
6545
|
+
.map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
|
|
6546
|
+
.filter(x => x.dist <= maxDist)
|
|
6547
|
+
.sort((a, b) => a.dist - b.dist)
|
|
6548
|
+
.slice(0, 3)
|
|
6549
|
+
.map(x => x.method);
|
|
6550
|
+
};
|
|
6551
|
+
|
|
6552
|
+
// Create adapter proxies with helpful error messages
|
|
6553
|
+
const adaptersObj = {};
|
|
6359
6554
|
for (const [adapterName, methods] of Object.entries(adapterMethods)) {
|
|
6360
|
-
const
|
|
6555
|
+
const methodsImpl = {};
|
|
6361
6556
|
for (const methodName of methods) {
|
|
6362
|
-
|
|
6557
|
+
methodsImpl[methodName] = async (...args) => {
|
|
6363
6558
|
const id = ++callId;
|
|
6364
6559
|
return new Promise((resolve, reject) => {
|
|
6365
6560
|
pendingCalls.set(id, { resolve, reject });
|
|
@@ -6373,9 +6568,33 @@ class BunWorkerSandbox {
|
|
|
6373
6568
|
});
|
|
6374
6569
|
};
|
|
6375
6570
|
}
|
|
6376
|
-
|
|
6377
|
-
|
|
6571
|
+
|
|
6572
|
+
// Use Proxy to intercept undefined method calls
|
|
6573
|
+
const adapterProxy = new Proxy(methodsImpl, {
|
|
6574
|
+
get(target, prop) {
|
|
6575
|
+
if (prop in target) return target[prop];
|
|
6576
|
+
if (typeof prop === 'symbol') return undefined;
|
|
6577
|
+
const similar = findSimilar(String(prop), methods);
|
|
6578
|
+
const suggestion = similar.length > 0
|
|
6579
|
+
? '. Did you mean: ' + similar.join(', ') + '?'
|
|
6580
|
+
: '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
|
|
6581
|
+
throw new Error(adapterName + '.' + String(prop) + ' is not a function' + suggestion);
|
|
6582
|
+
}
|
|
6583
|
+
});
|
|
6584
|
+
|
|
6585
|
+
adaptersObj[adapterName] = adapterProxy;
|
|
6586
|
+
// Also expose at top level but as non-writable
|
|
6587
|
+
Object.defineProperty(globalThis, adapterName, {
|
|
6588
|
+
value: adapterProxy,
|
|
6589
|
+
writable: false,
|
|
6590
|
+
configurable: false
|
|
6591
|
+
});
|
|
6378
6592
|
}
|
|
6593
|
+
Object.defineProperty(globalThis, 'adapters', {
|
|
6594
|
+
value: adaptersObj,
|
|
6595
|
+
writable: false,
|
|
6596
|
+
configurable: false
|
|
6597
|
+
});
|
|
6379
6598
|
|
|
6380
6599
|
self.postMessage({ type: 'ready' });
|
|
6381
6600
|
}
|
|
@@ -6398,10 +6617,12 @@ class BunWorkerSandbox {
|
|
|
6398
6617
|
const result = await fn();
|
|
6399
6618
|
self.postMessage({ type: 'result', success: true, value: result, logs });
|
|
6400
6619
|
} catch (err) {
|
|
6620
|
+
// Truncate stack to 5 lines to prevent context bloat
|
|
6621
|
+
const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;
|
|
6401
6622
|
self.postMessage({
|
|
6402
6623
|
type: 'result',
|
|
6403
6624
|
success: false,
|
|
6404
|
-
error: { name: err.name, message: err.message, stack
|
|
6625
|
+
error: { name: err.name, message: err.message, stack },
|
|
6405
6626
|
logs
|
|
6406
6627
|
});
|
|
6407
6628
|
}
|
|
@@ -10411,12 +10632,12 @@ function createParameterValidator(params) {
|
|
|
10411
10632
|
default:
|
|
10412
10633
|
schema = exports_external.unknown();
|
|
10413
10634
|
}
|
|
10414
|
-
if (param.default !== undefined) {
|
|
10415
|
-
schema = schema.default(param.default);
|
|
10416
|
-
}
|
|
10417
10635
|
if (!param.required) {
|
|
10418
10636
|
schema = schema.optional();
|
|
10419
10637
|
}
|
|
10638
|
+
if (param.default !== undefined) {
|
|
10639
|
+
schema = schema.default(param.default);
|
|
10640
|
+
}
|
|
10420
10641
|
shape[param.name] = schema;
|
|
10421
10642
|
}
|
|
10422
10643
|
return exports_external.object(shape);
|
|
@@ -10559,7 +10780,10 @@ var DEFAULT_CONFIG2 = {
|
|
|
10559
10780
|
timeout: 5000,
|
|
10560
10781
|
memoryLimit: 128,
|
|
10561
10782
|
allowAsync: true,
|
|
10562
|
-
globals: {}
|
|
10783
|
+
globals: {},
|
|
10784
|
+
networkPolicy: DEFAULT_NETWORK_POLICY,
|
|
10785
|
+
normalizeCode: true,
|
|
10786
|
+
analysis: DEFAULT_ANALYSIS_CONFIG
|
|
10563
10787
|
},
|
|
10564
10788
|
adaptersDir: "./adapters",
|
|
10565
10789
|
skillsDir: "./skills"
|
|
@@ -10653,21 +10877,22 @@ function generateTypes(adapters, options = {}) {
|
|
|
10653
10877
|
const safeName = sanitizeIdentifier(adapter.name);
|
|
10654
10878
|
for (const [toolName, tool] of Object.entries(adapter.tools)) {
|
|
10655
10879
|
if (tool.parameters && Object.keys(tool.parameters).length > 0) {
|
|
10656
|
-
const
|
|
10880
|
+
const safeToolNameForType = sanitizeIdentifier(toolName);
|
|
10881
|
+
const inputTypeName = `${capitalize(safeName)}_${capitalize(safeToolNameForType)}_Input`;
|
|
10657
10882
|
lines.push(generateInputInterface(inputTypeName, tool.parameters, includeDescriptions));
|
|
10658
10883
|
lines.push("");
|
|
10659
10884
|
}
|
|
10660
10885
|
}
|
|
10661
10886
|
if (includeDescriptions && adapter.description) {
|
|
10662
|
-
lines.push(`/** ${adapter.description} */`);
|
|
10887
|
+
lines.push(`/** ${sanitizeJSDoc(adapter.description)} */`);
|
|
10663
10888
|
}
|
|
10664
10889
|
lines.push(`declare const ${safeName}: {`);
|
|
10665
10890
|
for (const [toolName, tool] of Object.entries(adapter.tools)) {
|
|
10666
10891
|
const safeToolName = sanitizeIdentifier(toolName);
|
|
10667
10892
|
const hasParams = tool.parameters && Object.keys(tool.parameters).length > 0;
|
|
10668
|
-
const inputTypeName = `${capitalize(safeName)}_${capitalize(
|
|
10893
|
+
const inputTypeName = `${capitalize(safeName)}_${capitalize(safeToolName)}_Input`;
|
|
10669
10894
|
if (includeDescriptions && tool.description) {
|
|
10670
|
-
lines.push(` /** ${tool.description} */`);
|
|
10895
|
+
lines.push(` /** ${sanitizeJSDoc(tool.description)} */`);
|
|
10671
10896
|
}
|
|
10672
10897
|
const paramStr = hasParams ? `params: ${inputTypeName}` : "";
|
|
10673
10898
|
const returnType = asyncResults ? "Promise<unknown>" : "unknown";
|
|
@@ -10680,21 +10905,64 @@ function generateTypes(adapters, options = {}) {
|
|
|
10680
10905
|
`).trim();
|
|
10681
10906
|
}
|
|
10682
10907
|
function generateTypesSummary(adapters) {
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10908
|
+
const byDomain = new Map;
|
|
10909
|
+
for (const adapter of adapters) {
|
|
10910
|
+
const domain = inferDomain(adapter);
|
|
10911
|
+
if (!byDomain.has(domain)) {
|
|
10912
|
+
byDomain.set(domain, []);
|
|
10913
|
+
}
|
|
10914
|
+
byDomain.get(domain).push(adapter);
|
|
10915
|
+
}
|
|
10916
|
+
if (byDomain.size <= 1 || adapters.length < 4) {
|
|
10917
|
+
return adapters.map((adapter) => {
|
|
10918
|
+
const count = Object.keys(adapter.tools).length;
|
|
10919
|
+
return `- ${adapter.name} (${count} methods)`;
|
|
10920
|
+
}).join(`
|
|
10921
|
+
`);
|
|
10922
|
+
}
|
|
10923
|
+
const lines = [];
|
|
10924
|
+
for (const [domain, domainAdapters] of byDomain) {
|
|
10925
|
+
const adapterList = domainAdapters.map((a) => `${a.name}(${Object.keys(a.tools).length})`).join(", ");
|
|
10926
|
+
lines.push(`[${domain}] ${adapterList}`);
|
|
10927
|
+
}
|
|
10928
|
+
return lines.join(`
|
|
10687
10929
|
`);
|
|
10688
10930
|
}
|
|
10931
|
+
function inferDomain(adapter) {
|
|
10932
|
+
if (adapter.domain)
|
|
10933
|
+
return adapter.domain;
|
|
10934
|
+
const name = adapter.name.toLowerCase();
|
|
10935
|
+
const desc = (adapter.description || "").toLowerCase();
|
|
10936
|
+
const combined = `${name} ${desc}`;
|
|
10937
|
+
const domains = {
|
|
10938
|
+
payments: ["stripe", "paypal", "square", "payment", "checkout", "billing", "invoice"],
|
|
10939
|
+
database: ["supabase", "postgres", "mysql", "mongodb", "redis", "database", "sql", "query"],
|
|
10940
|
+
email: ["sendgrid", "mailgun", "postmark", "email", "smtp", "mail"],
|
|
10941
|
+
storage: ["s3", "cloudflare", "storage", "blob", "file", "upload"],
|
|
10942
|
+
auth: ["auth", "oauth", "login", "jwt", "clerk", "auth0"],
|
|
10943
|
+
ai: ["openai", "anthropic", "claude", "gpt", "llm", "ai", "ml"],
|
|
10944
|
+
messaging: ["slack", "discord", "telegram", "twilio", "sms", "chat"],
|
|
10945
|
+
crm: ["hubspot", "salesforce", "crm", "customer"],
|
|
10946
|
+
analytics: ["analytics", "metrics", "tracking", "mixpanel", "amplitude"],
|
|
10947
|
+
devtools: ["github", "gitlab", "jira", "linear", "chrome", "devtools", "ci", "cd"]
|
|
10948
|
+
};
|
|
10949
|
+
for (const [domain, keywords2] of Object.entries(domains)) {
|
|
10950
|
+
if (keywords2.some((k) => combined.includes(k))) {
|
|
10951
|
+
return domain;
|
|
10952
|
+
}
|
|
10953
|
+
}
|
|
10954
|
+
return "general";
|
|
10955
|
+
}
|
|
10689
10956
|
function generateInputInterface(typeName, parameters, includeDescriptions) {
|
|
10690
10957
|
const lines = [`interface ${typeName} {`];
|
|
10691
10958
|
for (const [paramName, param] of Object.entries(parameters)) {
|
|
10959
|
+
const safeParamName = sanitizeIdentifier(paramName);
|
|
10692
10960
|
if (includeDescriptions && param.description) {
|
|
10693
|
-
lines.push(` /** ${param.description} */`);
|
|
10961
|
+
lines.push(` /** ${sanitizeJSDoc(param.description)} */`);
|
|
10694
10962
|
}
|
|
10695
10963
|
const tsType = paramTypeToTS(param.type);
|
|
10696
|
-
const optional = param.required ===
|
|
10697
|
-
lines.push(` ${
|
|
10964
|
+
const optional = param.required === true ? "" : "?";
|
|
10965
|
+
lines.push(` ${safeParamName}${optional}: ${tsType};`);
|
|
10698
10966
|
}
|
|
10699
10967
|
lines.push("}");
|
|
10700
10968
|
return lines.join(`
|
|
@@ -10767,6 +11035,9 @@ function sanitizeIdentifier(name) {
|
|
|
10767
11035
|
function capitalize(str) {
|
|
10768
11036
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
10769
11037
|
}
|
|
11038
|
+
function sanitizeJSDoc(text) {
|
|
11039
|
+
return text.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
|
|
11040
|
+
}
|
|
10770
11041
|
// src/executor.ts
|
|
10771
11042
|
import { pathToFileURL } from "node:url";
|
|
10772
11043
|
class MCXExecutor {
|
|
@@ -10823,7 +11094,7 @@ class MCXExecutor {
|
|
|
10823
11094
|
}
|
|
10824
11095
|
registerAdapter(adapter) {
|
|
10825
11096
|
if (this.adapters.has(adapter.name)) {
|
|
10826
|
-
console.
|
|
11097
|
+
console.error(`[MCX] Adapter "${adapter.name}" is being overwritten`);
|
|
10827
11098
|
}
|
|
10828
11099
|
this.adapters.set(adapter.name, adapter);
|
|
10829
11100
|
}
|
|
@@ -10838,7 +11109,7 @@ class MCXExecutor {
|
|
|
10838
11109
|
}
|
|
10839
11110
|
registerSkill(skill) {
|
|
10840
11111
|
if (this.skills.has(skill.name)) {
|
|
10841
|
-
console.
|
|
11112
|
+
console.error(`[MCX] Skill "${skill.name}" is being overwritten`);
|
|
10842
11113
|
}
|
|
10843
11114
|
this.skills.set(skill.name, skill);
|
|
10844
11115
|
}
|
|
@@ -10905,12 +11176,15 @@ class MCXExecutor {
|
|
|
10905
11176
|
};
|
|
10906
11177
|
} catch (error) {
|
|
10907
11178
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
11179
|
+
const stack = err.stack ? err.stack.split(`
|
|
11180
|
+
`).slice(0, 5).join(`
|
|
11181
|
+
`) : undefined;
|
|
10908
11182
|
return {
|
|
10909
11183
|
success: false,
|
|
10910
11184
|
error: {
|
|
10911
11185
|
name: err.name,
|
|
10912
11186
|
message: err.message,
|
|
10913
|
-
stack
|
|
11187
|
+
stack
|
|
10914
11188
|
},
|
|
10915
11189
|
logs: [],
|
|
10916
11190
|
executionTime: performance.now() - startTime
|
|
@@ -10976,6 +11250,7 @@ export {
|
|
|
10976
11250
|
normalizeCode,
|
|
10977
11251
|
mergeConfigs,
|
|
10978
11252
|
isUrlAllowed,
|
|
11253
|
+
inferDomain,
|
|
10979
11254
|
generateTypesSummary,
|
|
10980
11255
|
generateTypes,
|
|
10981
11256
|
formatFindings,
|