@ruifung/codemode-bridge 1.0.3-1 → 1.0.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/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
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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.
|
|
549
|
+
return typeof obj.crossExecPollution;
|
|
333
550
|
`, {});
|
|
334
|
-
expect(
|
|
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
|
-
|
|
210
|
-
globalThis
|
|
211
|
-
globalThis
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
"version": "1.0.5",
|
|
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,23 @@
|
|
|
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
|
-
"isolated-vm": "^6.0.2",
|
|
43
41
|
"open": "^10.1.0",
|
|
44
|
-
"ts-node": "^10.9.2",
|
|
45
|
-
"typescript": "^5.9.3",
|
|
46
42
|
"uuid": "^13.0.0",
|
|
47
43
|
"vm2": "^3.10.5",
|
|
48
44
|
"winston": "^3.19.0",
|
|
49
45
|
"zod": "^4.3.6"
|
|
50
46
|
},
|
|
51
47
|
"devDependencies": {
|
|
48
|
+
"@types/node": "^25.3.0",
|
|
52
49
|
"@vitest/ui": "^4.0.18",
|
|
53
50
|
"tsx": "^4.21.0",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
54
52
|
"vitest": "^4.0.18"
|
|
53
|
+
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"isolated-vm": "^6.0.2"
|
|
55
56
|
}
|
|
56
57
|
}
|