@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papicandela/mcx-core",
3
- "version": "0.2.2",
3
+ "version": "0.2.6",
4
4
  "description": "MCX Core - MCP Code Execution Framework",
5
5
  "author": "papicandela",
6
6
  "license": "MIT",
@@ -8,7 +8,13 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/schizoidcock/mcx"
10
10
  },
11
- "keywords": ["mcp", "claude", "ai", "code-execution", "sandbox"],
11
+ "keywords": [
12
+ "mcp",
13
+ "claude",
14
+ "ai",
15
+ "code-execution",
16
+ "sandbox"
17
+ ],
12
18
  "main": "dist/index.js",
13
19
  "types": "dist/index.d.ts",
14
20
  "type": "module",
package/src/adapter.ts CHANGED
@@ -38,14 +38,18 @@ function createParameterValidator(
38
38
  schema = z.unknown();
39
39
  }
40
40
 
41
- if (param.default !== undefined) {
42
- schema = schema.default(param.default);
43
- }
44
-
41
+ // IMPORTANT: Order matters in Zod!
42
+ // .optional() must come BEFORE .default() so that:
43
+ // - absent input resolves to the default (not undefined)
44
+ // - z.string().optional().default("x") = ZodDefault<ZodOptional<...>>
45
45
  if (!param.required) {
46
46
  schema = schema.optional();
47
47
  }
48
48
 
49
+ if (param.default !== undefined) {
50
+ schema = schema.default(param.default);
51
+ }
52
+
49
53
  shape[param.name] = schema;
50
54
  }
51
55
 
package/src/config.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { Adapter, MCXConfig, SandboxConfig, Skill } from "./types.js";
2
+ import { DEFAULT_NETWORK_POLICY } from "./sandbox/network-policy.js";
3
+ import { DEFAULT_ANALYSIS_CONFIG } from "./sandbox/analyzer/index.js";
2
4
 
3
5
  /**
4
6
  * Default MCX configuration values.
7
+ * Must include all sandbox fields to ensure network isolation is always enabled.
5
8
  */
6
9
  const DEFAULT_CONFIG: Required<Omit<MCXConfig, "adapters" | "skills" | "env">> = {
7
10
  sandbox: {
@@ -9,6 +12,9 @@ const DEFAULT_CONFIG: Required<Omit<MCXConfig, "adapters" | "skills" | "env">> =
9
12
  memoryLimit: 128,
10
13
  allowAsync: true,
11
14
  globals: {},
15
+ networkPolicy: DEFAULT_NETWORK_POLICY,
16
+ normalizeCode: true,
17
+ analysis: DEFAULT_ANALYSIS_CONFIG,
12
18
  },
13
19
  adaptersDir: "./adapters",
14
20
  skillsDir: "./skills",
package/src/executor.ts CHANGED
@@ -121,7 +121,7 @@ export class MCXExecutor {
121
121
  */
122
122
  registerAdapter(adapter: Adapter): void {
123
123
  if (this.adapters.has(adapter.name)) {
124
- console.warn(`Adapter "${adapter.name}" is being overwritten`);
124
+ console.error(`[MCX] Adapter "${adapter.name}" is being overwritten`);
125
125
  }
126
126
  this.adapters.set(adapter.name, adapter);
127
127
  }
@@ -159,7 +159,7 @@ export class MCXExecutor {
159
159
  */
160
160
  registerSkill(skill: Skill): void {
161
161
  if (this.skills.has(skill.name)) {
162
- console.warn(`Skill "${skill.name}" is being overwritten`);
162
+ console.error(`[MCX] Skill "${skill.name}" is being overwritten`);
163
163
  }
164
164
  this.skills.set(skill.name, skill);
165
165
  }
@@ -288,12 +288,14 @@ export class MCXExecutor {
288
288
  };
289
289
  } catch (error) {
290
290
  const err = error instanceof Error ? error : new Error(String(error));
291
+ // Truncate stack to 5 lines to prevent context bloat
292
+ const stack = err.stack ? err.stack.split("\n").slice(0, 5).join("\n") : undefined;
291
293
  return {
292
294
  success: false,
293
295
  error: {
294
296
  name: err.name,
295
297
  message: err.message,
296
- stack: err.stack,
298
+ stack,
297
299
  },
298
300
  logs: [],
299
301
  executionTime: performance.now() - startTime,
package/src/index.ts CHANGED
@@ -78,6 +78,7 @@ export { configBuilder, defineConfig, mergeConfigs } from "./config.js";
78
78
  export {
79
79
  generateTypes,
80
80
  generateTypesSummary,
81
+ inferDomain,
81
82
  sanitizeIdentifier,
82
83
  type TypeGeneratorOptions,
83
84
  } from "./type-generator.js";
@@ -179,10 +179,10 @@ describe("analyze", () => {
179
179
  )).toBe(true);
180
180
  });
181
181
 
182
- it("warns about require", () => {
182
+ it("errors on require (security: could access dangerous modules)", () => {
183
183
  const result = analyze("const fs = require('fs');");
184
- expect(result.warnings.some(w =>
185
- w.rule === "no-dangerous-globals" && w.message.includes("require")
184
+ expect(result.errors.some(e =>
185
+ e.rule === "no-dangerous-globals" && e.message.includes("require")
186
186
  )).toBe(true);
187
187
  });
188
188
 
@@ -222,9 +222,9 @@ export function analyze(
222
222
 
223
223
  const elapsed = performance.now() - start;
224
224
 
225
- // Log if we exceed performance budget
225
+ // Log if we exceed performance budget (use stderr to avoid breaking stdio transport)
226
226
  if (elapsed > 50) {
227
- console.warn(`[mcx-analyzer] Exceeded 50ms budget: ${elapsed.toFixed(1)}ms`);
227
+ console.error(`[mcx-analyzer] Exceeded 50ms budget: ${elapsed.toFixed(1)}ms`);
228
228
  }
229
229
 
230
230
  return { warnings, errors, elapsed };
@@ -2,10 +2,11 @@
2
2
  * Rule: no-dangerous-globals
3
3
  *
4
4
  * Detects usage of dangerous globals that are not available or unsafe in sandbox:
5
- * - Dynamic code execution
6
- * - Function constructor
7
- * - process object
8
- * - require function
5
+ * - Dynamic code execution (blocked as error)
6
+ * - Function constructor (blocked as error)
7
+ * - AsyncFunction constructor via prototype chain (blocked as error)
8
+ * - process object (warning)
9
+ * - require function (blocked as error - could access fs/child_process)
9
10
  */
10
11
 
11
12
  import type * as acorn from "acorn";
@@ -17,72 +18,158 @@ const FUNC_CONSTRUCTOR = "Func" + "tion";
17
18
  const REQUIRE_NAME = "req" + "uire";
18
19
  const PROCESS_NAME = "pro" + "cess";
19
20
 
21
+ // Globals that expose Function constructor
22
+ const DANGEROUS_GLOBALS = ["globalThis", "self", "window"];
23
+
24
+ /**
25
+ * Check if a node is accessing .constructor on a function expression
26
+ * Detects: Object.getPrototypeOf(async function(){}).constructor
27
+ * (function(){}).constructor
28
+ * (async ()=>{}).constructor
29
+ */
30
+ function isConstructorOnFunction(node: acorn.MemberExpression): boolean {
31
+ // Check if accessing .constructor
32
+ const prop = node.property;
33
+ const isConstructorAccess =
34
+ (!node.computed && prop.type === "Identifier" && (prop as acorn.Identifier).name === "constructor") ||
35
+ (node.computed && prop.type === "Literal" && (prop as acorn.Literal).value === "constructor");
36
+
37
+ if (!isConstructorAccess) return false;
38
+
39
+ // Check if the object is a function expression or call result that could return Function
40
+ const obj = node.object;
41
+
42
+ // Direct: (function(){}).constructor or (async ()=>{}).constructor
43
+ if (obj.type === "FunctionExpression" || obj.type === "ArrowFunctionExpression") {
44
+ return true;
45
+ }
46
+
47
+ // Via Object.getPrototypeOf: Object.getPrototypeOf(fn).constructor
48
+ if (obj.type === "CallExpression") {
49
+ const call = obj as acorn.CallExpression;
50
+ if (call.callee.type === "MemberExpression") {
51
+ const callee = call.callee as acorn.MemberExpression;
52
+ if (
53
+ callee.object.type === "Identifier" &&
54
+ (callee.object as acorn.Identifier).name === "Object" &&
55
+ callee.property.type === "Identifier" &&
56
+ (callee.property as acorn.Identifier).name === "getPrototypeOf"
57
+ ) {
58
+ return true;
59
+ }
60
+ }
61
+ }
62
+
63
+ return false;
64
+ }
65
+
20
66
  export const rule: Rule = {
21
67
  name: "no-dangerous-globals",
22
68
  severity: "warn",
23
- description: "Warn about dangerous globals not available in sandbox",
24
- visits: ["CallExpression", "NewExpression", "Identifier", "MemberExpression"],
69
+ description: "Block dangerous globals that could escape sandbox",
70
+ visits: ["CallExpression", "NewExpression", "MemberExpression"],
25
71
 
26
72
  visitors: {
27
73
  CallExpression(node, context) {
28
74
  const callExpr = node as acorn.CallExpression;
75
+ const calleeName = callExpr.callee.type === "Identifier"
76
+ ? (callExpr.callee as acorn.Identifier).name
77
+ : null;
29
78
 
30
- // Check for dynamic code execution
31
- if (
32
- callExpr.callee.type === "Identifier" &&
33
- (callExpr.callee as acorn.Identifier).name === EVAL_NAME
34
- ) {
79
+ // SECURITY: Dynamic code execution is a sandbox escape vector - block
80
+ if (calleeName === EVAL_NAME) {
35
81
  context.report({
36
- severity: "warn",
37
- message: `${EVAL_NAME}() is not available in sandbox`,
82
+ severity: "error",
83
+ message: `${EVAL_NAME}() is blocked in sandbox - potential code injection`,
38
84
  line: context.getLine(node),
39
85
  });
40
86
  return;
41
87
  }
42
88
 
43
- // Check for require()
44
- if (
45
- callExpr.callee.type === "Identifier" &&
46
- (callExpr.callee as acorn.Identifier).name === REQUIRE_NAME
47
- ) {
89
+ // SECURITY: Function constructor called without new is equally dangerous
90
+ if (calleeName === FUNC_CONSTRUCTOR) {
48
91
  context.report({
49
- severity: "warn",
50
- message: `${REQUIRE_NAME}() is not available in sandbox - use adapters instead`,
92
+ severity: "error",
93
+ message: `${FUNC_CONSTRUCTOR}() is blocked in sandbox - potential code injection`,
51
94
  line: context.getLine(node),
52
95
  });
53
96
  return;
54
97
  }
98
+
99
+ // SECURITY: require() could access fs, child_process - block as error
100
+ if (calleeName === REQUIRE_NAME) {
101
+ context.report({
102
+ severity: "error",
103
+ message: `${REQUIRE_NAME}() is blocked in sandbox - could access dangerous modules`,
104
+ line: context.getLine(node),
105
+ });
106
+ return;
107
+ }
108
+
109
+ // SECURITY: Detect (fn).constructor() or Object.getPrototypeOf(fn).constructor()
110
+ // This catches: new (Object.getPrototypeOf(async function(){}).constructor)(code)
111
+ if (callExpr.callee.type === "MemberExpression") {
112
+ if (isConstructorOnFunction(callExpr.callee as acorn.MemberExpression)) {
113
+ context.report({
114
+ severity: "error",
115
+ message: `Accessing .constructor on functions is blocked - potential sandbox escape`,
116
+ line: context.getLine(node),
117
+ });
118
+ return;
119
+ }
120
+ }
55
121
  },
56
122
 
57
123
  NewExpression(node, context) {
58
124
  const newExpr = node as acorn.NewExpression;
59
125
 
60
- // Check for Function constructor
126
+ // SECURITY: Function constructor is a sandbox escape vector - block
61
127
  if (
62
128
  newExpr.callee.type === "Identifier" &&
63
129
  (newExpr.callee as acorn.Identifier).name === FUNC_CONSTRUCTOR
64
130
  ) {
65
131
  context.report({
66
- severity: "warn",
67
- message: `${FUNC_CONSTRUCTOR} constructor is not recommended in sandbox`,
132
+ severity: "error",
133
+ message: `${FUNC_CONSTRUCTOR} constructor is blocked in sandbox - potential code injection`,
68
134
  line: context.getLine(node),
69
135
  });
136
+ return;
70
137
  }
71
- },
72
138
 
73
- Identifier(node, context) {
74
- const identifier = node as acorn.Identifier;
139
+ // SECURITY: Detect new (fn).constructor() patterns
140
+ if (newExpr.callee.type === "MemberExpression") {
141
+ if (isConstructorOnFunction(newExpr.callee as acorn.MemberExpression)) {
142
+ context.report({
143
+ severity: "error",
144
+ message: `Accessing .constructor on functions is blocked - potential sandbox escape`,
145
+ line: context.getLine(node),
146
+ });
147
+ return;
148
+ }
149
+ }
75
150
 
76
- // Check for bare `process` reference
77
- if (identifier.name === PROCESS_NAME) {
78
- context.report({
79
- severity: "warn",
80
- message: `'${PROCESS_NAME}' is not available in sandbox`,
81
- line: context.getLine(node),
82
- });
151
+ // SECURITY: Detect new globalThis.Function(), new self.Function(), etc.
152
+ if (newExpr.callee.type === "MemberExpression") {
153
+ const member = newExpr.callee as acorn.MemberExpression;
154
+ if (
155
+ member.object.type === "Identifier" &&
156
+ DANGEROUS_GLOBALS.includes((member.object as acorn.Identifier).name) &&
157
+ member.property.type === "Identifier" &&
158
+ (member.property as acorn.Identifier).name === FUNC_CONSTRUCTOR
159
+ ) {
160
+ context.report({
161
+ severity: "error",
162
+ message: `${FUNC_CONSTRUCTOR} constructor is blocked in sandbox - potential code injection`,
163
+ line: context.getLine(node),
164
+ });
165
+ }
83
166
  }
84
167
  },
85
168
 
169
+ // Note: Removed Identifier visitor for 'process' - MemberExpression catches
170
+ // process.X usage, and bare 'process' references fail at runtime anyway.
171
+ // This prevents duplicate warnings for each process.X access.
172
+
86
173
  MemberExpression(node, context) {
87
174
  const memberExpr = node as acorn.MemberExpression;
88
175
 
@@ -96,6 +183,21 @@ export const rule: Rule = {
96
183
  message: `'${PROCESS_NAME}' is not available in sandbox`,
97
184
  line: context.getLine(node),
98
185
  });
186
+ return;
187
+ }
188
+
189
+ // SECURITY: Detect globalThis.Function, self.Function access
190
+ if (
191
+ memberExpr.object.type === "Identifier" &&
192
+ DANGEROUS_GLOBALS.includes((memberExpr.object as acorn.Identifier).name) &&
193
+ memberExpr.property.type === "Identifier" &&
194
+ (memberExpr.property as acorn.Identifier).name === FUNC_CONSTRUCTOR
195
+ ) {
196
+ context.report({
197
+ severity: "error",
198
+ message: `Accessing ${FUNC_CONSTRUCTOR} via globals is blocked - potential sandbox escape`,
199
+ line: context.getLine(node),
200
+ });
99
201
  }
100
202
  },
101
203
  },
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Rule: no-infinite-loop
3
3
  *
4
- * Detects infinite loops without break statements:
5
- * - while(true) { ... } without break
6
- * - for(;;) { ... } without break
4
+ * Detects infinite loops without exit statements:
5
+ * - while(true) { ... } without break/return/throw
6
+ * - for(;;) { ... } without break/return/throw
7
+ * - do { ... } while(true) without break/return/throw
7
8
  */
8
9
 
9
10
  import type * as acorn from "acorn";
@@ -18,12 +19,17 @@ function isLiteralTrue(node: acorn.Node | null | undefined): boolean {
18
19
  }
19
20
 
20
21
  /**
21
- * Check if a block contains a break statement (at any depth)
22
+ * Check if a block contains an exit statement (break, return, throw)
23
+ * These all terminate or exit the loop, so they prevent infinite loops.
22
24
  */
23
- function hasBreak(node: acorn.Node): boolean {
25
+ function hasExitStatement(node: acorn.Node): boolean {
26
+ // Exit statements
24
27
  if (node.type === "BreakStatement") return true;
28
+ if (node.type === "ReturnStatement") return true;
29
+ if (node.type === "ThrowStatement") return true;
25
30
 
26
31
  // Don't descend into nested loops or switches (break would apply to them)
32
+ // But DO descend for return/throw since they exit the function entirely
27
33
  if (
28
34
  node.type === "WhileStatement" ||
29
35
  node.type === "ForStatement" ||
@@ -35,6 +41,15 @@ function hasBreak(node: acorn.Node): boolean {
35
41
  return false;
36
42
  }
37
43
 
44
+ // Don't descend into nested functions (return/throw would apply to them)
45
+ if (
46
+ node.type === "FunctionDeclaration" ||
47
+ node.type === "FunctionExpression" ||
48
+ node.type === "ArrowFunctionExpression"
49
+ ) {
50
+ return false;
51
+ }
52
+
38
53
  // Check children
39
54
  for (const key of Object.keys(node)) {
40
55
  const child = (node as unknown as Record<string, unknown>)[key];
@@ -42,11 +57,11 @@ function hasBreak(node: acorn.Node): boolean {
42
57
  if (Array.isArray(child)) {
43
58
  for (const item of child) {
44
59
  if (item && typeof item === "object" && "type" in item) {
45
- if (hasBreak(item as acorn.Node)) return true;
60
+ if (hasExitStatement(item as acorn.Node)) return true;
46
61
  }
47
62
  }
48
63
  } else if ("type" in child) {
49
- if (hasBreak(child as acorn.Node)) return true;
64
+ if (hasExitStatement(child as acorn.Node)) return true;
50
65
  }
51
66
  }
52
67
  }
@@ -57,17 +72,17 @@ function hasBreak(node: acorn.Node): boolean {
57
72
  export const rule: Rule = {
58
73
  name: "no-infinite-loop",
59
74
  severity: "error",
60
- description: "Disallow infinite loops without break statements",
61
- visits: ["WhileStatement", "ForStatement"],
75
+ description: "Disallow infinite loops without exit statements",
76
+ visits: ["WhileStatement", "ForStatement", "DoWhileStatement"],
62
77
 
63
78
  visitors: {
64
79
  WhileStatement(node, context) {
65
80
  const whileNode = node as acorn.WhileStatement;
66
81
 
67
- if (isLiteralTrue(whileNode.test) && !hasBreak(whileNode.body)) {
82
+ if (isLiteralTrue(whileNode.test) && !hasExitStatement(whileNode.body)) {
68
83
  context.report({
69
84
  severity: "error",
70
- message: "Infinite loop: while(true) without break",
85
+ message: "Infinite loop: while(true) without break/return/throw",
71
86
  line: context.getLine(node),
72
87
  });
73
88
  }
@@ -77,10 +92,22 @@ export const rule: Rule = {
77
92
  const forNode = node as acorn.ForStatement;
78
93
 
79
94
  // for(;;) without test condition
80
- if (!forNode.test && !hasBreak(forNode.body)) {
95
+ if (!forNode.test && !hasExitStatement(forNode.body)) {
96
+ context.report({
97
+ severity: "error",
98
+ message: "Infinite loop: for(;;) without break/return/throw",
99
+ line: context.getLine(node),
100
+ });
101
+ }
102
+ },
103
+
104
+ DoWhileStatement(node, context) {
105
+ const doWhileNode = node as acorn.DoWhileStatement;
106
+
107
+ if (isLiteralTrue(doWhileNode.test) && !hasExitStatement(doWhileNode.body)) {
81
108
  context.report({
82
109
  severity: "error",
83
- message: "Infinite loop: for(;;) without break",
110
+ message: "Infinite loop: do...while(true) without break/return/throw",
84
111
  line: context.getLine(node),
85
112
  });
86
113
  }