@ruifung/codemode-bridge 1.0.7 → 1.0.9

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.
@@ -4,12 +4,13 @@
4
4
  * Implements subcommands for managing and running the bridge
5
5
  */
6
6
  import chalk from "chalk";
7
- import * as fs from "fs";
8
- import * as path from "path";
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
9
  import { loadConfig, saveConfig, addServer, removeServer, updateServer, getServer, listServers, validateServer, getConfigFilePath, } from "./config-manager.js";
10
10
  import { startCodeModeBridgeServer } from "../mcp/server.js";
11
11
  import { getServerConfig } from "../mcp/config.js";
12
12
  import { initializeLogger, logInfo, logError, flushStderrBuffer } from "../utils/logger.js";
13
+ import { getRuntimeName } from "../utils/env.js";
13
14
  import { tokenPersistence } from "../mcp/token-persistence.js";
14
15
  import { MCPClient } from "../mcp/mcp-client.js";
15
16
  /**
@@ -21,6 +22,7 @@ export async function runServer(configPath, servers, debug, executor) {
21
22
  initializeLogger(debug);
22
23
  console.error(chalk.cyan("\nšŸš€ Code Mode Bridge"));
23
24
  console.error(chalk.cyan("====================\n"));
25
+ logInfo(`Runtime: ${getRuntimeName()}`, { component: 'CLI' });
24
26
  // Load the bridge configuration
25
27
  const bridgeConfig = loadConfig(configPath);
26
28
  logInfo(`Loaded config from: ${getConfigFilePath(configPath)}`, { component: 'CLI' });
@@ -4,8 +4,8 @@
4
4
  * Manages the bridge configuration stored in .config/codemode-bridge/mcp.json
5
5
  * Provides utilities to load, save, and manipulate the configuration
6
6
  */
7
- import * as fs from "fs";
8
- import * as path from "path";
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
9
  /**
10
10
  * Get the default config directory for the current platform
11
11
  */
package/dist/cli/index.js CHANGED
@@ -19,14 +19,27 @@
19
19
  import { Command } from "commander";
20
20
  import { runServer, listServersCommand, showServerCommand, addServerCommand, removeServerCommand, editServerCommand, configInfoCommand, authLoginCommand, authLogoutCommand, authListCommand, } from "./commands.js";
21
21
  import { getConfigFilePath } from "./config-manager.js";
22
- import * as fs from "fs";
22
+ import { getExecutorStatus } from "../mcp/executor-status.js";
23
+ import * as fs from "node:fs";
23
24
  const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
24
25
  const defaultConfigPath = getConfigFilePath();
25
26
  const program = new Command();
27
+ // Add logic to get available executors for version output
28
+ let versionString = pkg.version;
29
+ if (process.argv.includes("-V") || process.argv.includes("--version")) {
30
+ const statuses = await getExecutorStatus();
31
+ const available = statuses
32
+ .filter((s) => s.isAvailable)
33
+ .map((s) => s.type)
34
+ .join(", ");
35
+ if (available) {
36
+ versionString = `${pkg.version} (executors: ${available})`;
37
+ }
38
+ }
26
39
  program
27
40
  .name("codemode-bridge")
28
41
  .description("Code Mode Bridge CLI - Connects to multiple MCP servers and exposes them as a single tool")
29
- .version(pkg.version);
42
+ .version(versionString);
30
43
  // Main 'run' command
31
44
  program
32
45
  .command("run", { isDefault: true })
@@ -34,7 +47,7 @@ program
34
47
  .option("-c, --config <path>", `Path to mcp.json configuration file (default: ${defaultConfigPath})`)
35
48
  .option("-s, --servers <names>", "Comma-separated list of servers to connect to")
36
49
  .option("-d, --debug", "Enable debug logging")
37
- .option("-e, --executor <type>", "Executor type (isolated-vm, container, vm2)")
50
+ .option("-e, --executor <type>", "Executor type (isolated-vm, container, deno, vm2)")
38
51
  .action(async (options) => {
39
52
  const servers = options.servers ? options.servers.split(",").map((s) => s.trim()) : undefined;
40
53
  await runServer(options.config, servers, options.debug, options.executor);
@@ -18,8 +18,12 @@ import type { Executor, ExecuteResult } from '@cloudflare/codemode';
18
18
  export interface ContainerExecutorOptions {
19
19
  /** Execution timeout per call in ms (default 30000) */
20
20
  timeout?: number;
21
- /** Container image (default 'node:22-slim') */
21
+ /** Container image (default 'node:24-slim') */
22
22
  image?: string;
23
+ /** Container user (default based on image) */
24
+ user?: string;
25
+ /** Container command to run the runner (default based on image) */
26
+ command?: string[];
23
27
  /** Container runtime command — 'docker' | 'podman' | auto-detect */
24
28
  runtime?: string;
25
29
  /** Memory limit (default '256m') */
@@ -30,6 +34,8 @@ export interface ContainerExecutorOptions {
30
34
  export declare class ContainerExecutor implements Executor {
31
35
  private runtime;
32
36
  private image;
37
+ private containerUser;
38
+ private containerCommand;
33
39
  private timeout;
34
40
  private memoryLimit;
35
41
  private cpuLimit;
@@ -20,6 +20,7 @@ import { createInterface } from 'node:readline';
20
20
  import { fileURLToPath } from 'node:url';
21
21
  import { dirname, join } from 'node:path';
22
22
  import { wrapCode } from './wrap-code.js';
23
+ import { isBun, isDeno } from '../utils/env.js';
23
24
  // ── Runtime detection ───────────────────────────────────────────────
24
25
  function detectRuntime(requested) {
25
26
  if (requested)
@@ -56,23 +57,52 @@ function getScriptPaths() {
56
57
  }
57
58
  // ── ContainerExecutor ───────────────────────────────────────────────
58
59
  export class ContainerExecutor {
60
+ runtime;
61
+ image;
62
+ containerUser;
63
+ containerCommand;
64
+ timeout;
65
+ memoryLimit;
66
+ cpuLimit;
67
+ containerId = null;
68
+ process = null;
69
+ readline = null;
70
+ ready = false;
71
+ /** Resolved when the container sends { type: 'ready' } */
72
+ readyResolve = null;
73
+ /** Pending execution — only one at a time */
74
+ pendingExecution = null;
75
+ initPromise = null;
59
76
  constructor(options = {}) {
60
- this.containerId = null;
61
- this.process = null;
62
- this.readline = null;
63
- this.ready = false;
64
- /** Resolved when the container sends { type: 'ready' } */
65
- this.readyResolve = null;
66
- /** Pending execution — only one at a time */
67
- this.pendingExecution = null;
68
- this.initPromise = null;
69
77
  this.timeout = options.timeout ?? 30000;
70
- this.image = options.image ?? 'node:24-slim';
71
- this.memoryLimit = options.memoryLimit ?? '256m';
78
+ this.memoryLimit = options.memoryLimit ?? '512M';
72
79
  this.cpuLimit = options.cpuLimit ?? 1.0;
73
80
  this.runtime = detectRuntime(options.runtime);
81
+ // Platform-specific defaults
82
+ if (options.image) {
83
+ this.image = options.image;
84
+ this.containerUser = options.user ?? '1000';
85
+ this.containerCommand = options.command ?? ['node'];
86
+ }
87
+ else if (isBun()) {
88
+ this.image = 'oven/bun:debian';
89
+ this.containerUser = options.user ?? '1000'; // bun user is 1000
90
+ this.containerCommand = ['bun', 'run'];
91
+ }
92
+ else if (isDeno()) {
93
+ this.image = 'denoland/deno:debian';
94
+ this.containerUser = options.user ?? '1000'; // deno:debian has no 'deno' user by default
95
+ this.containerCommand = ['deno', 'run', '-A'];
96
+ }
97
+ else {
98
+ this.image = 'node:24-slim';
99
+ this.containerUser = options.user ?? '1000'; // node user is 1000
100
+ this.containerCommand = ['node'];
101
+ }
74
102
  // Start initialization immediately to create the container before the first execution.
75
- // Errors are caught and logged, but will also be thrown when execute() awaits this.init().
103
+ if (process.env.DEBUG || process.env.NODE_ENV === 'test') {
104
+ console.log(`[ContainerExecutor] Using image: ${this.image}, user: ${this.containerUser}, command: ${this.containerCommand.join(' ')}`);
105
+ }
76
106
  this.init().catch(err => {
77
107
  process.stderr.write(`[container-executor] Immediate initialization failed: ${err.message}\n`);
78
108
  });
@@ -145,15 +175,15 @@ export class ContainerExecutor {
145
175
  '--read-only', // immutable rootfs
146
176
  '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', // writable /tmp
147
177
  '--cap-drop=ALL', // drop all capabilities
148
- '--user', 'node', // run as non-root user
178
+ '--user', this.containerUser,
149
179
  '--memory', this.memoryLimit,
150
180
  `--cpus=${this.cpuLimit}`,
151
- '--pids-limit=64', // limit process spawning
181
+ '--pids-limit=128', // limit process spawning
152
182
  '-v', `${scripts.runner}:/app/container-runner.mjs:ro`, // mount runner script
153
183
  '-v', `${scripts.worker}:/app/container-worker.mjs:ro`, // mount worker script
154
184
  '-w', '/app',
155
185
  this.image,
156
- 'node', '/app/container-runner.mjs',
186
+ ...this.containerCommand, '/app/container-runner.mjs',
157
187
  ];
158
188
  this.process = spawn(this.runtime, args, {
159
189
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -0,0 +1,26 @@
1
+ import type { Executor, ExecuteResult } from '@cloudflare/codemode';
2
+ export interface DenoExecutorOptions {
3
+ /** Execution timeout per call in ms (default 30000) */
4
+ timeout?: number;
5
+ /** Deno executable path — 'deno' | auto-detect */
6
+ denoPath?: string;
7
+ }
8
+ export declare class DenoExecutor implements Executor {
9
+ private denoPath;
10
+ private timeout;
11
+ private process;
12
+ private readline;
13
+ private ready;
14
+ private readyResolve;
15
+ private pendingExecution;
16
+ private initPromise;
17
+ constructor(options?: DenoExecutorOptions);
18
+ private init;
19
+ private _init;
20
+ private handleMessage;
21
+ private handleToolCall;
22
+ private send;
23
+ execute(code: string, fns: Record<string, (...args: unknown[]) => Promise<unknown>>): Promise<ExecuteResult>;
24
+ dispose(): void;
25
+ }
26
+ export declare function createDenoExecutor(options?: DenoExecutorOptions): DenoExecutor;
@@ -0,0 +1,253 @@
1
+ import { spawn, execSync } from 'node:child_process';
2
+ import { createInterface } from 'node:readline';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
+ import { wrapCode } from './wrap-code.js';
6
+ // ── Runtime detection ───────────────────────────────────────────────
7
+ function detectDeno(requested) {
8
+ if (requested)
9
+ return requested;
10
+ // If we are already running on Deno, use the current executable path
11
+ if (typeof globalThis.Deno?.execPath === 'function') {
12
+ return globalThis.Deno.execPath();
13
+ }
14
+ // Try 'deno' command in PATH
15
+ try {
16
+ execSync('deno --version', { stdio: 'ignore' });
17
+ return 'deno';
18
+ }
19
+ catch {
20
+ // not available
21
+ }
22
+ throw new Error('Deno executable not found. Install Deno or set DENO_PATH environment variable.');
23
+ }
24
+ // ── Resolve runner script path ──────────────────────────────────────
25
+ function getScriptPaths() {
26
+ let dir;
27
+ try {
28
+ dir = dirname(fileURLToPath(import.meta.url));
29
+ }
30
+ catch {
31
+ dir = __dirname;
32
+ }
33
+ return {
34
+ runner: join(dir, 'container-runner.mjs'),
35
+ worker: join(dir, 'container-worker.mjs'),
36
+ };
37
+ }
38
+ // ── DenoExecutor ────────────────────────────────────────────────────
39
+ export class DenoExecutor {
40
+ denoPath;
41
+ timeout;
42
+ process = null;
43
+ readline = null;
44
+ ready = false;
45
+ readyResolve = null;
46
+ pendingExecution = null;
47
+ initPromise = null;
48
+ constructor(options = {}) {
49
+ this.timeout = options.timeout ?? 30000;
50
+ this.denoPath = detectDeno(options.denoPath);
51
+ this.init().catch(err => {
52
+ process.stderr.write(`[deno-executor] Immediate initialization failed: ${err.message}\n`);
53
+ });
54
+ }
55
+ async init() {
56
+ if (this.ready)
57
+ return;
58
+ if (this.initPromise)
59
+ return this.initPromise;
60
+ this.initPromise = this._init();
61
+ return this.initPromise;
62
+ }
63
+ async _init() {
64
+ const scripts = getScriptPaths();
65
+ // Start a long-lived Deno process with restricted permissions
66
+ const args = [
67
+ 'run',
68
+ '--no-prompt',
69
+ '--no-config',
70
+ '--no-npm',
71
+ '--no-remote',
72
+ '--allow-read=' + scripts.runner + ',' + scripts.worker,
73
+ '--deny-net',
74
+ '--deny-write',
75
+ '--deny-run',
76
+ '--deny-env',
77
+ '--deny-sys',
78
+ '--deny-ffi',
79
+ ];
80
+ args.push(scripts.runner);
81
+ this.process = spawn(this.denoPath, args, {
82
+ stdio: ['pipe', 'pipe', 'pipe'],
83
+ });
84
+ this.process.stderr?.on('data', (data) => {
85
+ process.stderr.write(`[deno] ${data.toString()}`);
86
+ });
87
+ this.process.on('exit', (code) => {
88
+ this.ready = false;
89
+ if (this.pendingExecution) {
90
+ clearTimeout(this.pendingExecution.timeoutHandle);
91
+ this.pendingExecution.reject(new Error(`Deno process exited unexpectedly with code ${code}`));
92
+ this.pendingExecution = null;
93
+ }
94
+ });
95
+ this.readline = createInterface({
96
+ input: this.process.stdout,
97
+ terminal: false,
98
+ });
99
+ this.readline.on('line', (line) => this.handleMessage(line));
100
+ await new Promise((resolve, reject) => {
101
+ const timeout = setTimeout(() => {
102
+ reject(new Error('Deno process failed to become ready within 10s'));
103
+ }, 10000);
104
+ this.readyResolve = () => {
105
+ clearTimeout(timeout);
106
+ this.ready = true;
107
+ this.readyResolve = null;
108
+ resolve();
109
+ };
110
+ this.process.on('error', (err) => {
111
+ clearTimeout(timeout);
112
+ this.readyResolve = null;
113
+ reject(new Error(`Failed to start Deno: ${err.message}`));
114
+ });
115
+ });
116
+ }
117
+ handleMessage(line) {
118
+ if (!line.trim())
119
+ return;
120
+ let msg;
121
+ try {
122
+ msg = JSON.parse(line);
123
+ }
124
+ catch {
125
+ process.stderr.write(`[deno-executor] bad JSON from deno: ${line}\n`);
126
+ return;
127
+ }
128
+ switch (msg.type) {
129
+ case 'tool-call':
130
+ this.handleToolCall(msg);
131
+ break;
132
+ case 'result':
133
+ if (this.pendingExecution && this.pendingExecution.id === msg.id) {
134
+ clearTimeout(this.pendingExecution.timeoutHandle);
135
+ this.pendingExecution.resolve({
136
+ result: msg.result,
137
+ logs: msg.logs,
138
+ });
139
+ this.pendingExecution = null;
140
+ }
141
+ break;
142
+ case 'error':
143
+ if (this.pendingExecution && this.pendingExecution.id === msg.id) {
144
+ clearTimeout(this.pendingExecution.timeoutHandle);
145
+ this.pendingExecution.resolve({
146
+ result: undefined,
147
+ error: msg.error,
148
+ logs: msg.logs,
149
+ });
150
+ this.pendingExecution = null;
151
+ }
152
+ break;
153
+ case 'ready':
154
+ if (this.readyResolve) {
155
+ this.readyResolve();
156
+ }
157
+ break;
158
+ }
159
+ }
160
+ async handleToolCall(msg) {
161
+ if (!this.pendingExecution) {
162
+ this.send({ type: 'tool-error', id: msg.id, error: 'No active execution context' });
163
+ return;
164
+ }
165
+ const fns = this.pendingExecution.fns;
166
+ const fn = fns[msg.name];
167
+ if (!fn) {
168
+ this.send({
169
+ type: 'tool-error',
170
+ id: msg.id,
171
+ error: `Tool '${msg.name}' not found. Available tools: ${Object.keys(fns).join(', ')}`,
172
+ });
173
+ return;
174
+ }
175
+ try {
176
+ const args = msg.args;
177
+ const result = await fn(...(Array.isArray(args) ? args : [args]));
178
+ this.send({ type: 'tool-result', id: msg.id, result });
179
+ }
180
+ catch (err) {
181
+ this.send({
182
+ type: 'tool-error',
183
+ id: msg.id,
184
+ error: err instanceof Error ? err.message : String(err),
185
+ });
186
+ }
187
+ }
188
+ send(msg) {
189
+ if (!this.process?.stdin?.writable) {
190
+ throw new Error('Deno stdin is not writable');
191
+ }
192
+ this.process.stdin.write(JSON.stringify(msg) + '\n');
193
+ }
194
+ async execute(code, fns) {
195
+ await this.init();
196
+ if (this.pendingExecution) {
197
+ return {
198
+ result: undefined,
199
+ error: 'Another execution is already in progress',
200
+ };
201
+ }
202
+ const id = `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
203
+ const wrappedCode = wrapCode(code);
204
+ return new Promise((resolve, reject) => {
205
+ const timeoutHandle = setTimeout(() => {
206
+ if (this.pendingExecution?.id === id) {
207
+ this.pendingExecution = null;
208
+ resolve({
209
+ result: undefined,
210
+ error: `Code execution timeout after ${this.timeout}ms`,
211
+ });
212
+ }
213
+ }, this.timeout);
214
+ this.pendingExecution = { id, resolve, reject, fns, timeoutHandle };
215
+ this.send({ type: 'execute', id, code: wrappedCode });
216
+ });
217
+ }
218
+ dispose() {
219
+ if (this.pendingExecution) {
220
+ clearTimeout(this.pendingExecution.timeoutHandle);
221
+ this.pendingExecution.reject(new Error('Executor disposed'));
222
+ this.pendingExecution = null;
223
+ }
224
+ if (this.process?.stdin?.writable) {
225
+ try {
226
+ this.send({ type: 'shutdown' });
227
+ }
228
+ catch {
229
+ // ignore
230
+ }
231
+ }
232
+ if (this.readline) {
233
+ this.readline.close();
234
+ this.readline = null;
235
+ }
236
+ if (this.process) {
237
+ this.process.kill('SIGTERM');
238
+ const proc = this.process;
239
+ setTimeout(() => {
240
+ try {
241
+ proc.kill('SIGKILL');
242
+ }
243
+ catch { /* already dead */ }
244
+ }, 2000);
245
+ this.process = null;
246
+ }
247
+ this.ready = false;
248
+ this.initPromise = null;
249
+ }
250
+ }
251
+ export function createDenoExecutor(options) {
252
+ return new DenoExecutor(options);
253
+ }
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
2
+ import { getRuntimeName } from '../utils/env.js';
2
3
  export function createExecutorTestSuite(name, createExecutor, options) {
3
4
  const skipSet = new Set(options?.skipTests ?? []);
4
5
  /** Use it.skip for tests in the skip list, it otherwise */
@@ -11,6 +12,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
11
12
  }
12
13
  };
13
14
  describe(`Executor: ${name}`, () => {
15
+ console.log(`[Test] Runtime: ${getRuntimeName()}`);
14
16
  let executor;
15
17
  beforeAll(async () => {
16
18
  executor = createExecutor();
@@ -335,7 +337,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
335
337
  });
336
338
  describe('Isolation & Safety', () => {
337
339
  testOrSkip('should not allow access to require', async () => {
338
- const result = await executor.execute('return require("fs");', {});
340
+ const result = await executor.execute('return require("node:fs");', {});
339
341
  expect(result.error).toBeDefined();
340
342
  });
341
343
  testOrSkip('should not allow process access', async () => {
@@ -361,7 +363,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
361
363
  return 'network allowed via fetch';
362
364
  }
363
365
  if (typeof require === 'function') {
364
- const http = require('http');
366
+ const http = require('node:http');
365
367
  await new Promise((resolve, reject) => {
366
368
  http.get('http://example.com', resolve).on('error', reject);
367
369
  });
@@ -389,7 +391,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
389
391
  if (typeof require === 'function') {
390
392
  // Try net.Socket (TCP)
391
393
  try {
392
- const net = require('net');
394
+ const net = require('node:net');
393
395
  await new Promise((resolve, reject) => {
394
396
  const sock = new net.Socket();
395
397
  sock.setTimeout(2000);
@@ -402,7 +404,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
402
404
 
403
405
  // Try dgram (UDP)
404
406
  try {
405
- const dgram = require('dgram');
407
+ const dgram = require('node:dgram');
406
408
  const sock = dgram.createSocket('udp4');
407
409
  await new Promise((resolve, reject) => {
408
410
  sock.on('error', reject);
@@ -416,7 +418,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
416
418
 
417
419
  // Try tls.connect
418
420
  try {
419
- const tls = require('tls');
421
+ const tls = require('node:tls');
420
422
  await new Promise((resolve, reject) => {
421
423
  const sock = tls.connect(443, '1.1.1.1', {}, resolve);
422
424
  sock.on('error', reject);
@@ -426,7 +428,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
426
428
 
427
429
  // Try dns.resolve
428
430
  try {
429
- const dns = require('dns');
431
+ const dns = require('node:dns');
430
432
  await new Promise((resolve, reject) => {
431
433
  dns.resolve('example.com', (err, addresses) => {
432
434
  err ? reject(err) : resolve(addresses);
@@ -529,7 +531,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
529
531
  testOrSkip('should block dynamic import', async () => {
530
532
  const result = await executor.execute(`
531
533
  try {
532
- const m = await import('fs');
534
+ const m = await import('node:fs');
533
535
  return 'import allowed';
534
536
  } catch (e) {
535
537
  return 'blocked';
@@ -55,9 +55,11 @@ function stringify(value) {
55
55
  * - Explicit serialization boundaries
56
56
  */
57
57
  export class IsolatedVmExecutor {
58
+ isolate;
59
+ context = null;
60
+ metrics = null;
61
+ options;
58
62
  constructor(options = {}) {
59
- this.context = null;
60
- this.metrics = null;
61
63
  this.options = {
62
64
  memoryLimit: options.memoryLimit ?? 128,
63
65
  inspector: options.inspector ?? false,
@@ -9,6 +9,7 @@ import { wrapCode } from "./wrap-code.js";
9
9
  * Runs LLM-generated code in an isolated sandbox with access to tools via codemode.* namespace
10
10
  */
11
11
  export class VM2Executor {
12
+ timeout;
12
13
  constructor(timeout = 30000) {
13
14
  this.timeout = timeout;
14
15
  }
@@ -2,8 +2,8 @@
2
2
  * Config Loader - Load MCP server configurations from files
3
3
  * Supports VS Code's mcp.json format and other config files
4
4
  */
5
- import * as fs from "fs";
6
- import * as path from "path";
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
7
  /**
8
8
  * Load MCP server configurations from VS Code's mcp.json file
9
9
  * Default location: ~/.config/Code/User/mcp.json (Linux/Mac) or
@@ -26,6 +26,7 @@ import { createCodeTool } from '@cloudflare/codemode/ai';
26
26
  import { z } from 'zod';
27
27
  import { adaptAISDKToolToMCP } from './mcp-adapter.js';
28
28
  import { jsonSchemaToZod } from './server.js';
29
+ import { getRuntimeName } from '../utils/env.js';
29
30
  // ── Helpers ─────────────────────────────────────────────────────────────────
30
31
  /**
31
32
  * Create a mock upstream MCP server with test tools and connect an MCP client
@@ -141,6 +142,7 @@ export function createE2EBridgeTestSuite(executorName, createExecutor, options)
141
142
  }
142
143
  };
143
144
  describe(`E2E Bridge Pipeline [${executorName}]`, () => {
145
+ console.log(`[Test] Runtime: ${getRuntimeName()}`);
144
146
  let upstreamState;
145
147
  let bridgeState;
146
148
  let client;
@@ -378,10 +380,10 @@ export function createE2EBridgeTestSuite(executorName, createExecutor, options)
378
380
  testOrSkip('should not allow require access', async () => {
379
381
  const response = await client.callTool({
380
382
  name: 'eval',
381
- arguments: { code: 'async () => { return require("fs"); }' },
383
+ arguments: { code: 'async () => { return require("node:fs"); }' },
382
384
  });
383
385
  const text = response.content?.[0]?.text || '';
384
- expect(text.toLowerCase()).toMatch(/error|not defined|not allowed/);
386
+ expect(text.toLowerCase()).toMatch(/error|not defined|not allowed|requires .* access|could not be cloned/);
385
387
  });
386
388
  testOrSkip('should not allow process access', async () => {
387
389
  const response = await client.callTool({
@@ -389,7 +391,7 @@ export function createE2EBridgeTestSuite(executorName, createExecutor, options)
389
391
  arguments: { code: 'async () => { return process.env; }' },
390
392
  });
391
393
  const text = response.content?.[0]?.text || '';
392
- expect(text.toLowerCase()).toMatch(/error|not defined|not allowed/);
394
+ expect(text.toLowerCase()).toMatch(/error|not defined|not allowed|requires .* access|could not be cloned/);
393
395
  });
394
396
  testOrSkip('should isolate state between executions', async () => {
395
397
  await callCodemode(client, 'async () => { globalThis.__test = 123; return "set"; }');
@@ -0,0 +1,6 @@
1
+ export type ExecutorType = 'isolated-vm' | 'container' | 'deno' | 'vm2';
2
+ export interface ExecutorStatus {
3
+ type: ExecutorType;
4
+ isAvailable: boolean;
5
+ }
6
+ export declare function getExecutorStatus(): Promise<ExecutorStatus[]>;
@@ -0,0 +1,58 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { isNode, isDeno, isBun, getNodeMajorVersion } from "../utils/env.js";
3
+ async function isDenoAvailable() {
4
+ return isDeno();
5
+ }
6
+ async function isIsolatedVmAvailable() {
7
+ const majorVersion = getNodeMajorVersion();
8
+ const isEvenVersion = majorVersion > 0 && majorVersion % 2 === 0;
9
+ if (!isNode() || !isEvenVersion) {
10
+ return false;
11
+ }
12
+ try {
13
+ const checkScript = `
14
+ import ivm from 'isolated-vm';
15
+ const isolate = new ivm.Isolate({ memoryLimit: 8 });
16
+ isolate.dispose();
17
+ process.exit(0);
18
+ `;
19
+ const { execSync } = await import('node:child_process');
20
+ execSync(`node -e "${checkScript.replace(/"/g, '\\"').replace(/\n/g, '')}"`, { stdio: 'ignore', timeout: 2000 });
21
+ return true;
22
+ }
23
+ catch (err) {
24
+ return false;
25
+ }
26
+ }
27
+ async function isContainerRuntimeAvailable() {
28
+ for (const cmd of ['docker', 'podman']) {
29
+ try {
30
+ execFileSync(cmd, ['ps'], { stdio: 'ignore', timeout: 2000 });
31
+ return true;
32
+ }
33
+ catch (err) {
34
+ // not available
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+ async function isVM2Available() {
40
+ if (isBun())
41
+ return false;
42
+ try {
43
+ await import('vm2');
44
+ return true;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ export async function getExecutorStatus() {
51
+ const statuses = [
52
+ { type: 'deno', isAvailable: await isDenoAvailable() },
53
+ { type: 'isolated-vm', isAvailable: await isIsolatedVmAvailable() },
54
+ { type: 'container', isAvailable: await isContainerRuntimeAvailable() },
55
+ { type: 'vm2', isAvailable: await isVM2Available() },
56
+ ];
57
+ return statuses;
58
+ }
@@ -3,7 +3,7 @@
3
3
  * Selects the best available executor (isolated-vm → container → vm2)
4
4
  */
5
5
  import type { Executor } from "@cloudflare/codemode";
6
- export type ExecutorType = 'isolated-vm' | 'container' | 'vm2';
6
+ export type ExecutorType = 'isolated-vm' | 'container' | 'deno' | 'vm2';
7
7
  /**
8
8
  * Metadata about the executor that was created.
9
9
  */
@@ -4,11 +4,27 @@
4
4
  */
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { logInfo, logDebug } from "../utils/logger.js";
7
+ import { isNode, isDeno, isBun, getNodeMajorVersion } from "../utils/env.js";
7
8
  // ── Availability checks (cached) ────────────────────────────────────
8
9
  let _isolatedVmAvailable = null;
10
+ async function isDenoAvailable() {
11
+ // Only allow Deno executor if running on Deno
12
+ return isDeno();
13
+ }
9
14
  async function isIsolatedVmAvailable() {
10
15
  if (_isolatedVmAvailable !== null)
11
16
  return _isolatedVmAvailable;
17
+ // isolated-vm is a native module specifically built for Node.js.
18
+ // While Bun and Deno provide compatibility layers, they often fail or
19
+ // exhibit unstable behavior with native Node modules like isolated-vm.
20
+ // Additionally, isolated-vm only supports LTS (even-numbered) Node.js versions.
21
+ const majorVersion = getNodeMajorVersion();
22
+ const isEvenVersion = majorVersion > 0 && majorVersion % 2 === 0;
23
+ if (!isNode() || !isEvenVersion) {
24
+ logDebug('isolated-vm is only supported on LTS (even-numbered) versions of native Node.js (not Bun, Deno, or odd-numbered Node versions)', { component: 'Executor' });
25
+ _isolatedVmAvailable = false;
26
+ return false;
27
+ }
12
28
  logDebug('Checking isolated-vm availability...', { component: 'Executor' });
13
29
  try {
14
30
  // We run the check in a separate process because isolated-vm can cause
@@ -60,8 +76,17 @@ async function isContainerRuntimeAvailable() {
60
76
  // ── Executor registry (sorted by preference, lowest first) ─────────
61
77
  const executorRegistry = [
62
78
  {
63
- type: 'isolated-vm',
79
+ type: 'deno',
64
80
  preference: 0,
81
+ isAvailable: isDenoAvailable,
82
+ async create(timeout) {
83
+ const { createDenoExecutor } = await import('../executor/deno-executor.js');
84
+ return createDenoExecutor({ timeout });
85
+ },
86
+ },
87
+ {
88
+ type: 'isolated-vm',
89
+ preference: 1,
65
90
  isAvailable: isIsolatedVmAvailable,
66
91
  async create(timeout) {
67
92
  const { createIsolatedVmExecutor } = await import('../executor/isolated-vm-executor.js');
@@ -70,7 +95,7 @@ const executorRegistry = [
70
95
  },
71
96
  {
72
97
  type: 'container',
73
- preference: 1,
98
+ preference: 2,
74
99
  isAvailable: isContainerRuntimeAvailable,
75
100
  async create(timeout) {
76
101
  const { createContainerExecutor } = await import('../executor/container-executor.js');
@@ -79,8 +104,12 @@ const executorRegistry = [
79
104
  },
80
105
  {
81
106
  type: 'vm2',
82
- preference: 2,
107
+ preference: 3,
83
108
  isAvailable: async () => {
109
+ // vm2 is fundamentally broken on Bun (prototype freezing issues)
110
+ // and Node.js built-in 'node:vm' is not safe for untrusted code.
111
+ if (isBun())
112
+ return false;
84
113
  try {
85
114
  await import('vm2');
86
115
  return true;
@@ -19,6 +19,15 @@ import { logDebug, logInfo } from "../utils/logger.js";
19
19
  * Supports both pre-registered clients and dynamic client registration (RFC 7591)
20
20
  */
21
21
  class SimpleOAuthProvider {
22
+ config;
23
+ serverUrl;
24
+ static DEFAULT_REDIRECT_URL = 'http://localhost:3000/oauth/callback';
25
+ _tokens;
26
+ _codeVerifier;
27
+ _clientInfo;
28
+ _discoveryState;
29
+ _callbackServer;
30
+ _finishAuthCallback;
22
31
  constructor(config, serverUrl) {
23
32
  this.config = config;
24
33
  this.serverUrl = serverUrl;
@@ -196,15 +205,17 @@ class SimpleOAuthProvider {
196
205
  return undefined;
197
206
  }
198
207
  }
199
- SimpleOAuthProvider.DEFAULT_REDIRECT_URL = 'http://localhost:3000/oauth/callback';
200
208
  /**
201
209
  * MCP Client wrapper using official SDK
202
210
  */
203
211
  export class MCPClient {
212
+ config;
213
+ client;
214
+ transport = null;
215
+ connected = false;
216
+ oauthProvider;
204
217
  constructor(config) {
205
218
  this.config = config;
206
- this.transport = null;
207
- this.connected = false;
208
219
  this.client = new Client({
209
220
  name: `codemode-bridge-client-${config.name}`,
210
221
  version: "1.0.0",
@@ -6,15 +6,17 @@
6
6
  * to our loopback server with the authorization code, which we capture and use
7
7
  * to complete the authorization flow.
8
8
  */
9
- import { createServer } from 'http';
10
- import { URL } from 'url';
9
+ import { createServer } from 'node:http';
10
+ import { URL } from 'node:url';
11
11
  /**
12
12
  * Loopback HTTP server that listens for OAuth2 redirect callbacks
13
13
  */
14
14
  export class OAuthCallbackServer {
15
+ server;
16
+ port = 0; // 0 means OS will assign an available port
17
+ host = 'localhost';
18
+ pendingAuthorization;
15
19
  constructor(redirectUrl) {
16
- this.port = 0; // 0 means OS will assign an available port
17
- this.host = 'localhost';
18
20
  // Parse redirect URL to extract host and port
19
21
  if (redirectUrl) {
20
22
  try {
@@ -4,15 +4,17 @@
4
4
  * Persists OAuth tokens and client information to disk for reuse across sessions.
5
5
  * Stored in ~/.config/codemode-bridge/mcp-tokens.json
6
6
  */
7
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
8
- import { join } from 'path';
9
- import { homedir } from 'os';
7
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { homedir } from 'node:os';
10
10
  /**
11
11
  * Manages OAuth token storage for MCP server connections
12
12
  */
13
13
  export class TokenPersistence {
14
+ configDir;
15
+ tokenFile;
16
+ storage = {};
14
17
  constructor() {
15
- this.storage = {};
16
18
  this.configDir = join(homedir(), '.config', 'codemode-bridge');
17
19
  this.tokenFile = join(this.configDir, 'mcp-tokens.json');
18
20
  this.loadStorage();
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Environment detection utilities
3
+ */
4
+ /**
5
+ * Returns true if the current runtime is Node.js
6
+ */
7
+ export declare function isNode(): boolean;
8
+ /**
9
+ * Returns the Node.js major version, or 0 if not running on Node.js
10
+ */
11
+ export declare function getNodeMajorVersion(): number;
12
+ /**
13
+ * Returns true if the current runtime is Bun
14
+ */
15
+ export declare function isBun(): boolean;
16
+ /**
17
+ * Returns true if the current runtime is Deno
18
+ */
19
+ export declare function isDeno(): boolean;
20
+ /**
21
+ * Returns the name of the current runtime (Node.js, Bun, or Deno)
22
+ */
23
+ export declare function getRuntimeName(): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Environment detection utilities
3
+ */
4
+ /**
5
+ * Returns true if the current runtime is Node.js
6
+ */
7
+ export function isNode() {
8
+ return (typeof process !== 'undefined' &&
9
+ !!process.versions?.node &&
10
+ !isBun() &&
11
+ !isDeno());
12
+ }
13
+ /**
14
+ * Returns the Node.js major version, or 0 if not running on Node.js
15
+ */
16
+ export function getNodeMajorVersion() {
17
+ if (typeof process === 'undefined' || !process.versions?.node) {
18
+ return 0;
19
+ }
20
+ return parseInt(process.versions.node.split('.')[0], 10);
21
+ }
22
+ /**
23
+ * Returns true if the current runtime is Bun
24
+ */
25
+ export function isBun() {
26
+ return (typeof process !== 'undefined' &&
27
+ !!process.versions?.bun);
28
+ }
29
+ /**
30
+ * Returns true if the current runtime is Deno
31
+ */
32
+ export function isDeno() {
33
+ return ((typeof globalThis !== 'undefined' && !!globalThis.Deno) ||
34
+ (typeof process !== 'undefined' && !!process.versions?.deno));
35
+ }
36
+ /**
37
+ * Returns the name of the current runtime (Node.js, Bun, or Deno)
38
+ */
39
+ export function getRuntimeName() {
40
+ if (isBun())
41
+ return "Bun";
42
+ if (isDeno())
43
+ return "Deno";
44
+ if (isNode())
45
+ return "Node.js";
46
+ return "Unknown";
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruifung/codemode-bridge",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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": {
@@ -14,8 +14,11 @@
14
14
  "build": "tsc",
15
15
  "prepare": "npm run build",
16
16
  "dev": "tsx src/cli/index.ts",
17
+ "dev:bun": "bun run src/cli/index.ts",
18
+ "dev:deno": "deno run --allow-net --allow-read src/cli/index.ts",
17
19
  "test": "vitest",
18
- "test:e2e": "vitest run src/mcp/e2e-bridge-runner.test.ts"
20
+ "test:bun": "bun test",
21
+ "test:deno": "deno run -A npm:vitest"
19
22
  },
20
23
  "keywords": [
21
24
  "mcp",