@ruifung/codemode-bridge 1.0.3-1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -34,7 +34,7 @@ program
34
34
  await runServer(options.config, servers, options.debug);
35
35
  });
36
36
  // Config command group
37
- const config = program.command("config").description("Manage bridge configuration");
37
+ const config = program.command("config").description("Manage bridge configuration").enablePositionalOptions();
38
38
  config
39
39
  .command("list")
40
40
  .description("List all configured servers")
@@ -51,7 +51,8 @@ config
51
51
  });
52
52
  config
53
53
  .command("add <name> [commandAndArgs...]")
54
- .description("Add a new server configuration")
54
+ .description("Add a new server configuration (use -- before commands with flags, e.g. -- npx -y @some/pkg)")
55
+ .passThroughOptions()
55
56
  .requiredOption("-t, --type <type>", "Server type (stdio or http)")
56
57
  .option("--url <url>", "Server URL (required for http servers)")
57
58
  .option("--env <env...>", 'Environment variables as KEY=VALUE pairs')
@@ -135,6 +135,38 @@ export function createExecutorTestSuite(name, createExecutor, options) {
135
135
  `, { failingTool: mockTool });
136
136
  expect(result.result).toContain('caught');
137
137
  });
138
+ testOrSkip('should handle parallel tool calls with Promise.all', async () => {
139
+ const mockTool = vi.fn(async (id) => ({ id, name: `Item ${id}` }));
140
+ const result = await executor.execute(`
141
+ const results = await Promise.all([
142
+ codemode.fetchItem(1),
143
+ codemode.fetchItem(2),
144
+ codemode.fetchItem(3),
145
+ ]);
146
+ return results;
147
+ `, { fetchItem: mockTool });
148
+ expect(result.error).toBeUndefined();
149
+ expect(Array.isArray(result.result)).toBe(true);
150
+ expect(result.result).toHaveLength(3);
151
+ expect(mockTool).toHaveBeenCalledTimes(3);
152
+ expect(result.result[0]).toEqual({ id: 1, name: 'Item 1' });
153
+ expect(result.result[2]).toEqual({ id: 3, name: 'Item 3' });
154
+ });
155
+ testOrSkip('should protect codemode from reassignment', async () => {
156
+ const mockTool = vi.fn(async () => 'original');
157
+ const result = await executor.execute(`
158
+ try {
159
+ codemode = { hijacked: true };
160
+ } catch (e) {
161
+ // Expected — codemode is non-configurable / non-writable
162
+ }
163
+ // The real codemode should still work
164
+ return await codemode.myTool();
165
+ `, { myTool: mockTool });
166
+ expect(result.error).toBeUndefined();
167
+ expect(result.result).toBe('original');
168
+ expect(mockTool).toHaveBeenCalled();
169
+ });
138
170
  });
139
171
  describe('Complex Code Patterns', () => {
140
172
  testOrSkip('should handle for loops', async () => {
@@ -193,6 +225,113 @@ export function createExecutorTestSuite(name, createExecutor, options) {
193
225
  expect(result.result).toHaveLength(2);
194
226
  expect(mockFetch).toHaveBeenCalledTimes(2);
195
227
  });
228
+ testOrSkip('should handle Promise.allSettled', async () => {
229
+ const result = await executor.execute(`
230
+ const results = await Promise.allSettled([
231
+ Promise.resolve('ok'),
232
+ Promise.reject(new Error('fail')),
233
+ Promise.resolve(42),
234
+ ]);
235
+ return results.map(r => ({
236
+ status: r.status,
237
+ value: r.status === 'fulfilled' ? r.value : undefined,
238
+ reason: r.status === 'rejected' ? r.reason.message : undefined,
239
+ }));
240
+ `, {});
241
+ expect(result.error).toBeUndefined();
242
+ const arr = result.result;
243
+ expect(arr).toHaveLength(3);
244
+ expect(arr[0]).toEqual({ status: 'fulfilled', value: 'ok', reason: undefined });
245
+ expect(arr[1]).toEqual({ status: 'rejected', value: undefined, reason: 'fail' });
246
+ expect(arr[2]).toEqual({ status: 'fulfilled', value: 42, reason: undefined });
247
+ });
248
+ testOrSkip('should handle Promise.withResolvers', async () => {
249
+ // Promise.withResolvers is ES2024 — available in Node 22+
250
+ const result = await executor.execute(`
251
+ if (typeof Promise.withResolvers !== 'function') {
252
+ return 'unsupported';
253
+ }
254
+ const { promise, resolve } = Promise.withResolvers();
255
+ resolve(99);
256
+ return await promise;
257
+ `, {});
258
+ expect(result.error).toBeUndefined();
259
+ if (result.result === 'unsupported') {
260
+ // Engine doesn't support withResolvers, that's fine
261
+ return;
262
+ }
263
+ expect(result.result).toBe(99);
264
+ });
265
+ testOrSkip('should handle async generators', async () => {
266
+ const result = await executor.execute(`
267
+ async function* gen() {
268
+ yield 1;
269
+ yield 2;
270
+ yield 3;
271
+ }
272
+ const values = [];
273
+ for await (const v of gen()) {
274
+ values.push(v);
275
+ }
276
+ return values;
277
+ `, {});
278
+ expect(result.error).toBeUndefined();
279
+ expect(result.result).toEqual([1, 2, 3]);
280
+ });
281
+ testOrSkip('should handle closures and currying', async () => {
282
+ const result = await executor.execute(`
283
+ function multiply(a) {
284
+ return function(b) {
285
+ return a * b;
286
+ };
287
+ }
288
+ const double = multiply(2);
289
+ const triple = multiply(3);
290
+ return [double(5), triple(5)];
291
+ `, {});
292
+ expect(result.error).toBeUndefined();
293
+ expect(result.result).toEqual([10, 15]);
294
+ });
295
+ testOrSkip('should handle sync generators', async () => {
296
+ const result = await executor.execute(`
297
+ function* fibonacci() {
298
+ let a = 0, b = 1;
299
+ while (true) {
300
+ yield a;
301
+ [a, b] = [b, a + b];
302
+ }
303
+ }
304
+ const fib = fibonacci();
305
+ const values = [];
306
+ for (let i = 0; i < 8; i++) {
307
+ values.push(fib.next().value);
308
+ }
309
+ return values;
310
+ `, {});
311
+ expect(result.error).toBeUndefined();
312
+ expect(result.result).toEqual([0, 1, 1, 2, 3, 5, 8, 13]);
313
+ });
314
+ testOrSkip('should handle error cause chaining', async () => {
315
+ const result = await executor.execute(`
316
+ try {
317
+ try {
318
+ throw new Error('root cause');
319
+ } catch (e) {
320
+ throw new Error('wrapper', { cause: e });
321
+ }
322
+ } catch (e) {
323
+ return {
324
+ message: e.message,
325
+ causeMessage: e.cause?.message,
326
+ };
327
+ }
328
+ `, {});
329
+ expect(result.error).toBeUndefined();
330
+ expect(result.result).toEqual({
331
+ message: 'wrapper',
332
+ causeMessage: 'root cause',
333
+ });
334
+ });
196
335
  });
197
336
  describe('Isolation & Safety', () => {
198
337
  testOrSkip('should not allow access to require', async () => {
@@ -312,26 +451,105 @@ export function createExecutorTestSuite(name, createExecutor, options) {
312
451
  expect(result.result).not.toBe('network allowed via dns');
313
452
  }
314
453
  });
315
- testOrSkip('should isolate prototype pollution to the current execution', async () => {
316
- // NOTE: Prototype pollution IS possible within a single execution,
317
- // but it is NOT a security risk because:
318
- // 1. Each execution gets a fresh sandbox with clean prototypes
319
- // 2. Pollution only affects that specific execution's globals
320
- // 3. Host code is protected by serialization boundaries (JSON.stringify)
321
- // 4. Next execution runs in a completely new sandbox
322
- // Test 1: Pollution works within execution (expected)
323
- const result1 = await executor.execute(`
324
- Object.assign(Object.prototype, { polluted: true });
454
+ testOrSkip('should block prototype pollution', async () => {
455
+ // Prototypes are frozen Object.assign(Object.prototype, ...) must
456
+ // either throw or silently fail, and the pollution must not take effect.
457
+ const result = await executor.execute(`
458
+ try {
459
+ Object.assign(Object.prototype, { polluted: true });
460
+ } catch (e) {
461
+ // Throws in strict mode expected
462
+ }
325
463
  const obj = {};
326
464
  return obj.polluted;
327
465
  `, {});
328
- expect(result1.result).toBe(true); // Pollution works within this execution
329
- // Test 2: Next execution has clean prototypes (this proves isolation)
330
- const result2 = await executor.execute(`
466
+ // Pollution must not have taken effect
467
+ expect(result.result).toBeUndefined();
468
+ expect(result.error).toBeUndefined();
469
+ });
470
+ testOrSkip('should freeze Array.prototype', async () => {
471
+ const result = await executor.execute(`
472
+ try {
473
+ Array.prototype.myCustomMethod = () => 'injected';
474
+ } catch (e) {
475
+ return 'blocked';
476
+ }
477
+ return typeof [].myCustomMethod === 'function' ? 'polluted' : 'blocked';
478
+ `, {});
479
+ expect(result.result).toBe('blocked');
480
+ expect(result.error).toBeUndefined();
481
+ });
482
+ testOrSkip('should freeze Function.prototype', async () => {
483
+ const result = await executor.execute(`
484
+ try {
485
+ Function.prototype.myCustomMethod = () => 'injected';
486
+ } catch (e) {
487
+ return 'blocked';
488
+ }
489
+ return typeof (function(){}).myCustomMethod === 'function' ? 'polluted' : 'blocked';
490
+ `, {});
491
+ expect(result.result).toBe('blocked');
492
+ expect(result.error).toBeUndefined();
493
+ });
494
+ testOrSkip('should prevent adding new globals when globalThis is sealed', async () => {
495
+ const result = await executor.execute(`
496
+ try {
497
+ globalThis.sneakyGlobal = 42;
498
+ } catch (e) {
499
+ return 'blocked';
500
+ }
501
+ return typeof globalThis.sneakyGlobal !== 'undefined' ? 'leaked' : 'blocked';
502
+ `, {});
503
+ expect(result.result).toBe('blocked');
504
+ expect(result.error).toBeUndefined();
505
+ });
506
+ testOrSkip('should prevent overwriting codemode namespace', async () => {
507
+ const result = await executor.execute(`
508
+ try {
509
+ codemode = { hijacked: true };
510
+ } catch (e) {
511
+ return 'blocked';
512
+ }
513
+ return typeof codemode.hijacked !== 'undefined' ? 'overwritten' : 'blocked';
514
+ `, {});
515
+ expect(result.result).toBe('blocked');
516
+ expect(result.error).toBeUndefined();
517
+ });
518
+ testOrSkip('should hide internal state from enumeration', async () => {
519
+ // Internal sandbox mechanisms should not be enumerable on globalThis
520
+ const result = await executor.execute(`
521
+ const keys = Object.keys(globalThis);
522
+ // Should not expose internal mechanisms like __resolveResult, __callTool, etc.
523
+ const internalKeys = keys.filter(k => k.startsWith('__'));
524
+ return internalKeys;
525
+ `, {});
526
+ expect(result.error).toBeUndefined();
527
+ expect(result.result).toEqual([]);
528
+ });
529
+ testOrSkip('should block dynamic import', async () => {
530
+ const result = await executor.execute(`
531
+ try {
532
+ const m = await import('fs');
533
+ return 'import allowed';
534
+ } catch (e) {
535
+ return 'blocked';
536
+ }
537
+ `, {});
538
+ expect(result.result).toBe('blocked');
539
+ expect(result.error).toBeUndefined();
540
+ });
541
+ testOrSkip('should not persist prototype pollution across executions', async () => {
542
+ // First execution: attempt to pollute
543
+ await executor.execute(`
544
+ try { Object.prototype.crossExecPollution = 'leaked'; } catch(e) {}
545
+ `, {});
546
+ // Second execution: check if pollution persists
547
+ const result = await executor.execute(`
331
548
  const obj = {};
332
- return obj.polluted;
549
+ return typeof obj.crossExecPollution;
333
550
  `, {});
334
- expect(result2.result).toBeUndefined(); // Fresh sandbox, no pollution
551
+ expect(result.result).toBe('undefined');
552
+ expect(result.error).toBeUndefined();
335
553
  });
336
554
  });
337
555
  describe('Concurrency', () => {
@@ -372,6 +590,272 @@ export function createExecutorTestSuite(name, createExecutor, options) {
372
590
  expect(elapsed).toBeLessThan(500);
373
591
  });
374
592
  });
593
+ describe('Data Structures & Serialization', () => {
594
+ testOrSkip('should handle regex named groups', async () => {
595
+ const result = await executor.execute(`
596
+ const re = /(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})/;
597
+ const match = re.exec('2026-02-25');
598
+ return {
599
+ year: match.groups.year,
600
+ month: match.groups.month,
601
+ day: match.groups.day,
602
+ };
603
+ `, {});
604
+ expect(result.error).toBeUndefined();
605
+ expect(result.result).toEqual({ year: '2026', month: '02', day: '25' });
606
+ });
607
+ testOrSkip('should handle modern array methods', async () => {
608
+ const result = await executor.execute(`
609
+ const arr = [1, [2, [3, [4]]]];
610
+ const flat = arr.flat(Infinity);
611
+ const atResult = flat.at(-1);
612
+ const findLastResult = flat.findLast(x => x % 2 === 0);
613
+ return { flat, atResult, findLastResult };
614
+ `, {});
615
+ expect(result.error).toBeUndefined();
616
+ expect(result.result).toEqual({
617
+ flat: [1, 2, 3, 4],
618
+ atResult: 4,
619
+ findLastResult: 4,
620
+ });
621
+ });
622
+ testOrSkip('should handle Object.groupBy', async () => {
623
+ const result = await executor.execute(`
624
+ if (typeof Object.groupBy !== 'function') {
625
+ return 'unsupported';
626
+ }
627
+ const items = [
628
+ { type: 'fruit', name: 'apple' },
629
+ { type: 'veg', name: 'carrot' },
630
+ { type: 'fruit', name: 'banana' },
631
+ ];
632
+ const grouped = Object.groupBy(items, item => item.type);
633
+ return {
634
+ fruitCount: grouped.fruit.length,
635
+ vegCount: grouped.veg.length,
636
+ };
637
+ `, {});
638
+ expect(result.error).toBeUndefined();
639
+ if (result.result === 'unsupported')
640
+ return;
641
+ expect(result.result).toEqual({ fruitCount: 2, vegCount: 1 });
642
+ });
643
+ testOrSkip('should handle Map and Set', async () => {
644
+ const result = await executor.execute(`
645
+ const map = new Map();
646
+ map.set('a', 1);
647
+ map.set('b', 2);
648
+ map.set('c', 3);
649
+ const set = new Set([1, 2, 2, 3, 3, 3]);
650
+ return {
651
+ mapSize: map.size,
652
+ mapEntries: Array.from(map.entries()),
653
+ setSize: set.size,
654
+ setValues: Array.from(set.values()),
655
+ };
656
+ `, {});
657
+ expect(result.error).toBeUndefined();
658
+ expect(result.result).toEqual({
659
+ mapSize: 3,
660
+ mapEntries: [['a', 1], ['b', 2], ['c', 3]],
661
+ setSize: 3,
662
+ setValues: [1, 2, 3],
663
+ });
664
+ });
665
+ testOrSkip('should handle JSON replacer and reviver', async () => {
666
+ const result = await executor.execute(`
667
+ const data = { name: 'test', secret: 'hidden', count: 5 };
668
+ const filtered = JSON.stringify(data, (key, val) =>
669
+ key === 'secret' ? undefined : val
670
+ );
671
+ const parsed = JSON.parse('{"value":"42"}', (key, val) =>
672
+ key === 'value' ? Number(val) : val
673
+ );
674
+ return { filtered: JSON.parse(filtered), parsed };
675
+ `, {});
676
+ expect(result.error).toBeUndefined();
677
+ expect(result.result).toEqual({
678
+ filtered: { name: 'test', count: 5 },
679
+ parsed: { value: 42 },
680
+ });
681
+ });
682
+ testOrSkip('should handle JSON serialization edge cases', async () => {
683
+ const result = await executor.execute(`
684
+ return {
685
+ nan: Number.isNaN(NaN),
686
+ inf: !Number.isFinite(Infinity),
687
+ negZero: Object.is(-0, -0),
688
+ nullVal: null,
689
+ emptyStr: '',
690
+ emptyArr: [],
691
+ emptyObj: {},
692
+ nested: { a: { b: { c: 1 } } },
693
+ };
694
+ `, {});
695
+ expect(result.error).toBeUndefined();
696
+ const r = result.result;
697
+ expect(r.nan).toBe(true);
698
+ expect(r.inf).toBe(true);
699
+ expect(r.negZero).toBe(true);
700
+ expect(r.nullVal).toBeNull();
701
+ expect(r.emptyStr).toBe('');
702
+ expect(r.emptyArr).toEqual([]);
703
+ expect(r.emptyObj).toEqual({});
704
+ expect(r.nested).toEqual({ a: { b: { c: 1 } } });
705
+ });
706
+ testOrSkip('should handle Date operations', async () => {
707
+ const result = await executor.execute(`
708
+ const d = new Date('2026-02-25T12:00:00Z');
709
+ return {
710
+ iso: d.toISOString(),
711
+ year: d.getUTCFullYear(),
712
+ month: d.getUTCMonth() + 1,
713
+ day: d.getUTCDate(),
714
+ };
715
+ `, {});
716
+ expect(result.error).toBeUndefined();
717
+ expect(result.result).toEqual({
718
+ iso: '2026-02-25T12:00:00.000Z',
719
+ year: 2026,
720
+ month: 2,
721
+ day: 25,
722
+ });
723
+ });
724
+ testOrSkip('should handle BigInt arithmetic', async () => {
725
+ const result = await executor.execute(`
726
+ const a = BigInt('9007199254740993');
727
+ const b = BigInt('9007199254740993');
728
+ const sum = a + b;
729
+ return {
730
+ sumStr: sum.toString(),
731
+ isLarger: sum > BigInt(Number.MAX_SAFE_INTEGER),
732
+ };
733
+ `, {});
734
+ expect(result.error).toBeUndefined();
735
+ const r = result.result;
736
+ expect(r.sumStr).toBe('18014398509481986');
737
+ expect(r.isLarger).toBe(true);
738
+ });
739
+ });
740
+ describe('Stress & Edge Cases', () => {
741
+ testOrSkip('should handle deep nesting', async () => {
742
+ const result = await executor.execute(`
743
+ let obj = { value: 'deep' };
744
+ for (let i = 0; i < 50; i++) {
745
+ obj = { nested: obj };
746
+ }
747
+ let current = obj;
748
+ for (let i = 0; i < 50; i++) {
749
+ current = current.nested;
750
+ }
751
+ return current.value;
752
+ `, {});
753
+ expect(result.error).toBeUndefined();
754
+ expect(result.result).toBe('deep');
755
+ });
756
+ testOrSkip('should handle memoization patterns', async () => {
757
+ const result = await executor.execute(`
758
+ function memoize(fn) {
759
+ const cache = new Map();
760
+ return function(...args) {
761
+ const key = JSON.stringify(args);
762
+ if (cache.has(key)) return cache.get(key);
763
+ const result = fn(...args);
764
+ cache.set(key, result);
765
+ return result;
766
+ };
767
+ }
768
+ let callCount = 0;
769
+ const expensiveFn = memoize((n) => {
770
+ callCount++;
771
+ return n * n;
772
+ });
773
+ const results = [expensiveFn(5), expensiveFn(5), expensiveFn(3), expensiveFn(3)];
774
+ return { results, callCount };
775
+ `, {});
776
+ expect(result.error).toBeUndefined();
777
+ expect(result.result).toEqual({
778
+ results: [25, 25, 9, 9],
779
+ callCount: 2, // Only 2 unique calls
780
+ });
781
+ });
782
+ testOrSkip('should survive deep recursion', async () => {
783
+ const result = await executor.execute(`
784
+ function sumTo(n) {
785
+ if (n <= 0) return 0;
786
+ return n + sumTo(n - 1);
787
+ }
788
+ return sumTo(1000);
789
+ `, {});
790
+ expect(result.error).toBeUndefined();
791
+ expect(result.result).toBe(500500);
792
+ });
793
+ testOrSkip('should handle large array operations', async () => {
794
+ const result = await executor.execute(`
795
+ const size = 10000;
796
+ const arr = Array.from({ length: size }, (_, i) => i + 1);
797
+ const sum = arr.reduce((a, b) => a + b, 0);
798
+ const filtered = arr.filter(x => x % 2 === 0);
799
+ return { sum, evenCount: filtered.length, last: arr.at(-1) };
800
+ `, {});
801
+ expect(result.error).toBeUndefined();
802
+ expect(result.result).toEqual({
803
+ sum: 50005000,
804
+ evenCount: 5000,
805
+ last: 10000,
806
+ });
807
+ });
808
+ testOrSkip('should handle Proxy and Reflect', async () => {
809
+ const result = await executor.execute(`
810
+ const handler = {
811
+ get(target, prop) {
812
+ if (prop in target) return target[prop];
813
+ return 'default';
814
+ },
815
+ set(target, prop, value) {
816
+ target[prop] = typeof value === 'string' ? value.toUpperCase() : value;
817
+ return true;
818
+ },
819
+ };
820
+ const obj = new Proxy({}, handler);
821
+ obj.name = 'hello';
822
+ return {
823
+ name: obj.name,
824
+ missing: obj.nonexistent,
825
+ };
826
+ `, {});
827
+ expect(result.error).toBeUndefined();
828
+ expect(result.result).toEqual({
829
+ name: 'HELLO',
830
+ missing: 'default',
831
+ });
832
+ });
833
+ testOrSkip('should handle Symbol.iterator', async () => {
834
+ const result = await executor.execute(`
835
+ class Range {
836
+ constructor(start, end) {
837
+ this.start = start;
838
+ this.end = end;
839
+ }
840
+ [Symbol.iterator]() {
841
+ let current = this.start;
842
+ const end = this.end;
843
+ return {
844
+ next() {
845
+ if (current <= end) {
846
+ return { value: current++, done: false };
847
+ }
848
+ return { done: true };
849
+ },
850
+ };
851
+ }
852
+ }
853
+ return [...new Range(1, 5)];
854
+ `, {});
855
+ expect(result.error).toBeUndefined();
856
+ expect(result.result).toEqual([1, 2, 3, 4, 5]);
857
+ });
858
+ });
375
859
  });
376
860
  }
377
861
  // Run the test suite against both executors if available
@@ -205,21 +205,29 @@ export class IsolatedVmExecutor {
205
205
  }
206
206
  await context.global.set('codemode', new ivm.ExternalCopy(codemodeSandbox).copyInto({ transferIn: true }));
207
207
  // Set up the tool wrapper in isolate - uses notification pattern
208
+ // Refactored: mutate codemode in-place instead of reassigning, so it can be
209
+ // made non-configurable/non-writable on globalThis.
208
210
  await context.eval(`
209
- globalThis._pendingResolvers = {};
210
- globalThis._toolResults = {};
211
- globalThis._toolErrors = {};
212
-
213
- const _codemodeMethods = {};
214
- for (const toolName of Object.keys(codemode)) {
215
- _codemodeMethods[toolName] = (...args) => {
211
+ // Internal state — non-enumerable to hide from user code
212
+ Object.defineProperty(globalThis, '_pendingResolvers', { value: {}, writable: true, enumerable: false, configurable: true });
213
+ Object.defineProperty(globalThis, '_toolResults', { value: {}, writable: true, enumerable: false, configurable: true });
214
+ Object.defineProperty(globalThis, '_toolErrors', { value: {}, writable: true, enumerable: false, configurable: true });
215
+ Object.defineProperty(globalThis, '_hostExecuteTool', {
216
+ value: globalThis._hostExecuteTool,
217
+ writable: false, enumerable: false, configurable: false
218
+ });
219
+
220
+ // Replace codemode entries in-place with async tool wrappers
221
+ const _toolNames = Object.keys(codemode);
222
+ for (const key of Object.keys(codemode)) { delete codemode[key]; }
223
+ for (const toolName of _toolNames) {
224
+ codemode[toolName] = (...args) => {
216
225
  return new Promise((resolve, reject) => {
217
226
  const callId = _hostExecuteTool(toolName, args);
218
227
  globalThis._pendingResolvers[callId] = { resolve, reject };
219
228
  });
220
229
  };
221
230
  }
222
- codemode = _codemodeMethods;
223
231
  `);
224
232
  // Use Promise-based execution with async function
225
233
  const resultPromise = new Promise(async (resolve) => {
@@ -244,6 +252,45 @@ export class IsolatedVmExecutor {
244
252
  resolve: resolveCallback,
245
253
  reject: rejectCallback,
246
254
  }).copyInto({ transferIn: true }));
255
+ // Sandbox hardening — runs after ALL setup (including protocol), before user code
256
+ await context.eval(`
257
+ // 1. Freeze prototypes to prevent prototype pollution
258
+ Object.freeze(Object.prototype);
259
+ Object.freeze(Array.prototype);
260
+ Object.freeze(Function.prototype);
261
+
262
+ // 2. Block eval and Function constructor
263
+ Object.defineProperty(globalThis, 'eval', {
264
+ value: function() { throw new Error("eval is not allowed"); },
265
+ writable: false, enumerable: false, configurable: false
266
+ });
267
+ (function() {
268
+ var OrigFunction = Function;
269
+ function BlockedFunction() { throw new Error("Function constructor is not allowed"); }
270
+ BlockedFunction.prototype = OrigFunction.prototype;
271
+ globalThis.Function = BlockedFunction;
272
+ })();
273
+
274
+ // 3. Make codemode non-configurable & non-writable
275
+ Object.defineProperty(globalThis, 'codemode', {
276
+ value: globalThis.codemode,
277
+ writable: false,
278
+ configurable: false,
279
+ enumerable: true,
280
+ });
281
+
282
+ // 4. Make protocol & internal state non-enumerable
283
+ Object.defineProperty(globalThis, 'protocol', {
284
+ value: globalThis.protocol,
285
+ writable: false,
286
+ enumerable: false,
287
+ configurable: false,
288
+ });
289
+ // _hostExecuteTool already non-configurable from tool setup
290
+
291
+ // 5. Seal globalThis to prevent adding/removing properties
292
+ Object.seal(globalThis);
293
+ `);
247
294
  // Wrap code so it returns a Promise we can .then()
248
295
  const wrappedCode = wrapCode(code, { alwaysAsync: true });
249
296
  // Execute the async function and chain its Promise with .then()
@@ -52,6 +52,29 @@ export class VM2Executor {
52
52
  },
53
53
  },
54
54
  });
55
+ // Sandbox hardening — runs before user code
56
+ vm.run(`
57
+ // 1. Freeze prototypes to prevent prototype pollution
58
+ Object.freeze(Object.prototype);
59
+ Object.freeze(Array.prototype);
60
+ Object.freeze(Function.prototype);
61
+
62
+ // 2. Block Function constructor (equivalent to eval)
63
+ (function() {
64
+ var OrigFunction = Function;
65
+ function BlockedFunction() { throw new Error("Function constructor is not allowed"); }
66
+ BlockedFunction.prototype = OrigFunction.prototype;
67
+ globalThis.Function = BlockedFunction;
68
+ })();
69
+
70
+ // 3. Make codemode non-configurable & non-writable
71
+ Object.defineProperty(globalThis, 'codemode', {
72
+ value: globalThis.codemode,
73
+ writable: false,
74
+ configurable: false,
75
+ enumerable: true,
76
+ });
77
+ `, "codemode-hardening.js");
55
78
  // vm.run() returns a Promise if the code returns a Promise
56
79
  const rawResult = vm.run(wrappedCode, "codemode-execution.js");
57
80
  // Await the result if it's a promise
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruifung/codemode-bridge",
3
- "version": "1.0.3-1",
3
+ "version": "1.0.4",
4
4
  "description": "MCP bridge that connects to upstream MCP servers and exposes tools via a single codemode tool for orchestration",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -35,22 +35,21 @@
35
35
  "dependencies": {
36
36
  "@cloudflare/codemode": "^0.1.0",
37
37
  "@modelcontextprotocol/sdk": "^1.27.0",
38
- "@types/node": "^25.3.0",
39
38
  "acorn": "^8.16.0",
40
39
  "chalk": "^5.6.2",
41
40
  "commander": "^12.1.0",
42
41
  "isolated-vm": "^6.0.2",
43
42
  "open": "^10.1.0",
44
- "ts-node": "^10.9.2",
45
- "typescript": "^5.9.3",
46
43
  "uuid": "^13.0.0",
47
44
  "vm2": "^3.10.5",
48
45
  "winston": "^3.19.0",
49
46
  "zod": "^4.3.6"
50
47
  },
51
48
  "devDependencies": {
49
+ "@types/node": "^25.3.0",
52
50
  "@vitest/ui": "^4.0.18",
53
51
  "tsx": "^4.21.0",
52
+ "typescript": "^5.9.3",
54
53
  "vitest": "^4.0.18"
55
54
  }
56
55
  }