mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-25 16:44:36 +00:00
* feat(core): add persistent Node REPL runtime * fix(core): align Node REPL phase-one contract * fix(core): align Node REPL module compatibility * fix(core): remove unused Node REPL host broker * refactor(node-repl)!: deliver as a standalone MCP server, revert core tools Replaces the three built-in `packages/core` node_repl tools with a self-contained MCP server package, `@qwen-code/node-repl-mcp`. Why --- Issue #9333 triage accepted the runtime only "for exploration" and gated it on an open maintainer decision: built-in core tool vs MCP-server-first. Reversing OpenAI Codex 0.149.0 settled the shape — it ships code mode as a standalone host with an in-process fallback, and its real-Node `js_repl` (not the restricted V8 `exec`) is the analogue this roadmap needs, because stage 3 (#9335) imports cua-driver's N-API addons, which only real Node can load. Delivering out-of-core also keeps a security-relevant subsystem out of the maintainer-gated `packages/core` until its value is proven. What changed ------------ - New `packages/node-repl`: the kernel, module loader, cell transform, protocol and kernel manager, ported and now dependency-free (local `debug-log`/`win-path`/`tokenizer` replace core utils; `output-adapter` emits MCP content blocks in place of `result-converter`). - Reverted every `packages/core` change, the ~11 packaging files that existed only to ship the runtime assets into six distribution layouts, and the two design/plan docs describing the core delivery. - Deleted the trusted-package/sha256 layer: it was empty and unreachable in production (`module-loader.mjs` 882 -> 483 lines). - Wired the package into `scripts/build.js` and the root `vitest.config.ts`. Net effect: `packages/core` is untouched relative to main; the tool is opt-in via `mcpServers` instead of registered unconditionally for every user. Correctness fixes made while porting ------------------------------------ - Stack line numbers were wrong and drifted with binding count (source line 4 reported as 87). The prelude is now one physical line and the cell compiles with `lineOffset: -1`. - Top-level `var` nested in blocks/try/switch/loops was silently dropped; collection now walks the statement subtree, pruning at function boundaries. Verified against real Node for 16 constructs. - A binding named `nodeRepl` permanently broke the output channel; such cells are now rejected. Ordinary globals stay shadowable, matching plain Node. - Errors thrown by imported modules lost their class, `code` and stack. - A throwing frame handler could discard buffered protocol frames. - A hoisted `var` assigned before a throw is now kept, as Node does. - Unhandled rejections settling after a cell no longer vanish. - Live sandbox timers are capped, so one runaway loop cannot saturate the session's event loop. - Image MIME types are matched case-insensitively on both input paths. - Binding sort no longer depends on host locale collation. - The published bin lacked a shebang and mis-detected its entry point, and the server plus its kernel leaked on every host disconnect. Tests: 146 across 14 files, including a compiled N-API addon fixture, 100 consecutive cells, 10 concurrent isolated kernels, stack-line fidelity, and hoisting semantics. Three smoke scripts cover the adapter, the MCP wire and process lifecycle; the packed tarball was installed into a clean project and driven end to end. Refs #9333 * fix(node-repl): pin zod to the hoisted 3.x so the workspace build type-checks `packages/node-repl` declared `zod ^4.1.13`, so `npm ci` installed a nested zod 4.4.3 for it while `@modelcontextprotocol/sdk` resolved the hoisted zod 3.25.76. Two zod type identities in one compilation made every `registerTool` input schema unassignable (`Type 'ZodString' is not assignable to type 'AnySchema'`), failing `npm run build` for this workspace — and with it the install step of every CI job that builds workspaces. The SDK accepts `^3.25 || ^4.0`, so pin the hoisted 3.x line and drop the now-stale nested lockfile entry. One deduped zod, no behaviour change. This only reproduced with a lockfile-driven install; the local tree had no nested copy, which is why the build passed locally and failed in CI. * fix(lint): lint package .mjs node scripts with the node env The eslint "scripts we run with node" override matched `packages/*/scripts/**/*.js` but not `.mjs`, nor a package-root `build.mjs`. `packages/node-repl` is `type: module`, so its `build.mjs` and `scripts/*.mjs` were linted as browser code and `eslint .` failed with `'process' is not defined` / `'console' is not defined` / `'setTimeout' is not defined`. The pre-commit hook only lints `*.{js,jsx,ts,tsx}`, so `.mjs` files are not checked locally — this only surfaced in the root CI lint step. Add `packages/*/scripts/**/*.mjs` and `packages/*/build.mjs` to the override, matching the existing `.js` entries. Generic, additive, no behaviour change. * fix(node-repl): address round-3 review findings (resolution, error identity, image bound) Triaged the bot's round-3 critical findings against the ported code and fixed the three that were genuine defects here; each has a regression test. - R3-5 (resolution): a symlinked `<cwd>/node_modules` — the norm under pnpm, monorepo hoisting and shared CI caches — was silently dropped, so the documented zero-config `await import('pkg')` failed with "cannot resolve from 0 module roots" while plain Node resolved it. The implicit cwd root was applying a re-link self-comparison that only makes sense for registered roots (which carry a registration-time canonical baseline). Follow the symlink for the implicit root, but skip it entirely when the cwd node_modules is itself a registered root, so the registered root's revocation guard is not undermined. - R3-3 (error identity): an error thrown by a host builtin inside imported code (e.g. `fs.readFileSync` → ENOENT) was rewrapped message-only, dropping `code`/`errno`/`syscall`/`cause`/`stack` — so `catch (e) { if (e.code === 'ENOENT') }`, a ubiquitous Node idiom, silently took the wrong branch. Carry those fields onto the realm-wrapped error. - R3-6 (image bound): image-frame `mimeType` was unbounded — only `data.length` counted against the raw image budget — so a malformed/forged frame could retain a huge string that also got interpolated into a notice and tokenized. Bound the MIME at frame ingestion (a real image MIME is a few dozen chars). - R3-4 is by design (the runtime is not a security boundary), but the tool description recommended `createRequire` without noting it is not subject to the process denial or module-root containment the import path enforces; the description now says so. - R3-1 / R3-2 (the `.mjs` lint failure) were already fixed in an earlier commit. Suite: 148 tests (added symlinked-cwd resolution and host-builtin error-code regressions). The symlink fix initially defeated the registered-root revocation test; the registered-root deferral above resolves both. * docs(node-repl): note top-level function/class re-declaration needs a fresh name Round-3 review R3-12/R3-33: re-declaring an existing top-level function or class in a later cell is a link-time SyntaxError (they persist as `let`, which the prelude re-declares), whereas a plain Node REPL accepts it. A correct fix needs declaration-kind tracking the manager does not carry today; until then the tool description's rerun guidance ('prefer var') is corrected, since a function cannot be made rerunnable via var. --------- Co-authored-by: Claude <noreply@anthropic.com>
119 lines
3.9 KiB
JavaScript
119 lines
3.9 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { execSync } from 'node:child_process';
|
|
import { existsSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const root = join(__dirname, '..');
|
|
|
|
// npm install if node_modules was removed (e.g. via npm run clean or scripts/clean.js)
|
|
if (!existsSync(join(root, 'node_modules'))) {
|
|
execSync('npm install', { stdio: 'inherit', cwd: root });
|
|
}
|
|
|
|
// build all workspaces/packages in dependency order
|
|
execSync('npm run generate', { stdio: 'inherit', cwd: root });
|
|
|
|
// --cli-only: skip packages not needed by the CLI bundle
|
|
// (webui, web-shell, vscode-ide-companion are for IDE/web use only)
|
|
const cliOnly = process.argv.includes('--cli-only');
|
|
|
|
// Build in dependency order:
|
|
// 1. core (foundation package, includes test-utils)
|
|
// 2. web-templates (embeddable web templates - used by cli)
|
|
// 3. channel-base (base channel infrastructure - used by channel adapters and cli)
|
|
// 4. channel adapters (depend on channel-base)
|
|
// 5. audio-capture (native microphone backend used by cli)
|
|
// 6. acp-bridge (depends on core - used by cli)
|
|
// 7. sdk (build-time devDep on acp-bridge for shared constants, used by cli channel worker)
|
|
// 8. cli (depends on core, acp-bridge, web-templates, channel packages, sdk)
|
|
// 9. webui (shared UI components - used by vscode companion)
|
|
// 10. web-shell (depends on webui and sdk)
|
|
// 11. vscode-ide-companion (depends on webui)
|
|
// 12. external-context (private Qwen extension)
|
|
const buildOrder = [
|
|
'packages/core',
|
|
'packages/web-templates',
|
|
'packages/channels/base',
|
|
'packages/channels/telegram',
|
|
'packages/channels/weixin',
|
|
'packages/channels/dingtalk',
|
|
'packages/channels/wecom',
|
|
'packages/channels/feishu',
|
|
'packages/channels/qqbot',
|
|
'packages/channels/github',
|
|
// gitlab is a builtin of the cli channel registry like its siblings; it
|
|
// used to build only transitively via cli's tsconfig project reference.
|
|
'packages/channels/gitlab',
|
|
'packages/channels/plugin-example',
|
|
'packages/audio-capture',
|
|
'packages/node-repl',
|
|
'packages/acp-bridge',
|
|
'packages/sdk-typescript',
|
|
'packages/cli',
|
|
...(cliOnly
|
|
? []
|
|
: [
|
|
'packages/webui',
|
|
'packages/web-shell',
|
|
'packages/vscode-ide-companion',
|
|
'packages/chrome-extension',
|
|
'integrations/external-context',
|
|
]),
|
|
];
|
|
|
|
for (const workspace of buildOrder) {
|
|
const command =
|
|
workspace === 'packages/audio-capture'
|
|
? `npm run build:ts --workspace=${workspace}`
|
|
: `npm run build --workspace=${workspace}`;
|
|
execSync(command, { stdio: 'inherit', cwd: root });
|
|
|
|
// After cli is built, generate the JSON Schema for settings
|
|
// so the vscode-ide-companion extension can provide IntelliSense
|
|
if (workspace === 'packages/cli') {
|
|
execSync('node --import tsx/esm scripts/generate-settings-schema.ts', {
|
|
stdio: 'inherit',
|
|
cwd: root,
|
|
});
|
|
}
|
|
}
|
|
|
|
// also build container image if sandboxing is enabled
|
|
// skip (-s) npm install + build since we did that above
|
|
try {
|
|
execSync('node scripts/sandbox_command.js -q', {
|
|
stdio: 'inherit',
|
|
cwd: root,
|
|
});
|
|
if (
|
|
process.env.BUILD_SANDBOX === '1' ||
|
|
process.env.BUILD_SANDBOX === 'true'
|
|
) {
|
|
execSync('node scripts/build_sandbox.js -s', {
|
|
stdio: 'inherit',
|
|
cwd: root,
|
|
});
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|