qwen-code/packages/node-repl
易良 2cfd390650
feat(computer-use): let a remote session use your desktop through a node_repl relay (#11799)
* docs(plans): add remote computer use plan for a local Mac

Plan for letting a Qwen Code session on a headless Linux dev box operate
the user's local Mac through computer use: the principle, the existing
pieces, three options, and a Mac companion that joins the reverse tool
channel.

* docs(plans): add handoff and slice-0 verification brief for remote computer use

Add a handoff document for the session or agent that continues this
work, and a real-Mac verification brief for slice 0. Update the plan
with a newly found pitfall: on macOS the driver's LaunchServices
auto-launch does not forward environment variables, so an auto-launched
daemon never enables the HTTP MCP endpoint.

* docs(plans): replace the driver relay with a node_repl relay for remote computer use

Rereading main shows the bundled computer-use skill never talks to the
qwen-cua-driver daemon: ComputerUse.create() embeds the driver inside the
node_repl process, and a standalone daemon rejects trusted sessions from
any connection that is not its embedding parent (serve.rs:787, error 77).
That invalidates the v1 plan's option B and makes options A and C relay
the wrong layer (the raw tool surface meant for external agents).

The plan now relays node_repl itself over the reverse tool channel, keeps
the skill's App API intact, and lists the one skill change this needs
(read references with read_file instead of inside node_repl). The
verification brief is rewritten around a ten-minute fact check on a real
Mac plus the slice-1 prototype acceptance; the handoff document follows.

* docs(plans): make the Mac relay a qwen subcommand, not a standalone app

Maintainer decision: the Mac-side relay for remote computer use is a
qwen subcommand (qwen mac-bridge) run from a terminal. That matches how
local computer use already works, where qwen launches node_repl from the
terminal and the TCC grants are recorded against the terminal, so it
adds no install step and no new permission identity.

The plan drops the signed-app, bundle-id and menu-bar requirements, makes
Ctrl-C the local stop control, and demotes the signed host to an optional
slice that is off by default. The handoff document records the decision
so the next session does not reopen it, and the verification brief uses
the subcommand throughout.

* docs(plans): name the relay qwen bridge and drop the macOS-only framing

The relay is for any machine with a graphical session, not for macOS:
it only moves MCP frames, the SDK reports the connected platform, and
the skill already carries per-platform references. macOS is the platform
verified in this round because the TCC facts were read from its code
path. The subcommand is qwen bridge, one new top-level command in
packages/cli/src/commands/bridge.ts; the plan file is renamed from
mac-companion to desktop-relay and the handoff and verification brief
follow.

* feat(computer-use): let a remote session use this desktop through a node_repl relay

A Qwen Code session on a headless machine can now drive the desktop the
user is sitting at. That computer lends its own node_repl (with
@qwen-code/cua-sdk and the embedded driver) to one remote session:

- `node-repl-mcp desktop-relay install` puts the runtime under
  ~/.qwen/desktop-relay and registers a launchd socket on 127.0.0.1:47821
  in inetd mode, so nothing runs until something connects.
- The Web Shell gains a "Use this computer" footer entry. It hands the
  daemon URL, session and token to the relay, which asks for approval in
  a native dialog, starts node_repl and registers it for that session
  over the daemon's reverse tool channel. A dropped connection ends the
  relay; control never resumes without a fresh approval.
- Terminal sessions reach the same socket through an SSH RemoteForward;
  their first tools/call asks for approval.

One node_repl serves the daemon's several MCP clients: the relay answers
repeated initialize requests from the first result, rewrites request ids
and turns replies above the 10 MB frame cap into errors. The skill needs
no change; main already reads references with read_file when node_repl
cannot.

Not built, typechecked or tested locally; CI and the real-Mac brief in
docs/verification/remote-computer-use/ are the verification.

* chore(deps): record ws in the pnpm lockfile for node-repl-mcp

The pnpm lockfile is generated from package-lock.json by
`corepack pnpm import`; the previous commit updated only the npm one,
so frozen installs failed on the node-repl importer's new ws and
@types/ws specifiers. Both resolve to the versions the cli importer
already records.

* fix(node-repl): drop the console override in the desktop relay agent

The repo lints packages with no-console, and the override guarded against
output nothing on the agent path produces.

* fix(node-repl): restore the desktop relay agent entry point

The previous commit spliced runtime.ts at stale offsets and left it
unparseable. This applies the intended change to the last good copy: no
console override and no unused format import.

* fix: harden remote computer-use relay

* fix(node-repl): forward unambiguous cancellations through the desktop relay

Stopping a turn in the remote session sends notifications/cancelled for
the running tools/call. The relay dropped every cancellation because the
reverse channel does not say which multiplexed MCP client sent it, so a
node_repl cell kept driving the desktop after the user had stopped the
agent. Forward a cancellation, with its request id rewritten, when exactly
one pending request carries that id; still drop it when none or several
do.

* docs(computer-use): refresh the remote desktop relay handoff for another machine

Switch the verification setup to the pnpm install main now uses, add a
check that stopping a turn stops a running desktop cell, and add a
troubleshooting guide that walks the chain from browser to launchd to
daemon to macOS permissions. The handoff records the current state, the
two decisions still open, and why SSH workspaces do not replace this.

* docs(computer-use): record the Linux-side verification results for the desktop relay

The Mac-side end-to-end run is still blocked: this machine has no GUI and no
reachable macOS host. What could be verified here is recorded instead - the
relay's real HTTP surface over a genuine fd-0 socket with launchd's inetd
activation emulated (B.1-B.4 plus the edge cases curl cannot express), and a
code-level walk of the cancellation chain the 2026-09-23 fix relies on.

Two new findings: a failing osascript is indistinguishable from an explicit
Deny and leaves nothing in agent.log, and launchd's default ThrottleInterval
may serialize these short-lived per-connection processes, so it should be
measured first on the Mac.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): gate desktop relay and surface browser permission

* fix(computer-use): keep desktop relay usable after cancellation

* fix(computer-use): complete remote desktop relay acceptance

* fix(node-repl): harden desktop relay consent, lifecycle, and cancellation paths (#11799)

- skill: probe the desktop tool with `in` so the code-mode tools proxy
  does not abort the script before the fallback runs
- agent: bound the raw JSON-RPC buffer (64 KiB, matching the HTTP path)
  and retire tools/call cancelled while the consent dialog is open
- launchd: roll back the plist when bootstrap fails; confirm bootout
  actually unloaded the agent before deleting its plist
- consent: log dialog failures to stderr instead of reporting them as a
  human denial with no trace
- mcp-child-relay: forward an ambiguous-id cancellation to every match
  instead of dropping it, clamp relayed yield_time_ms under the daemon's
  30 s frame budget, and never let a throwing child reject handle()
- docs: replace the overlay merge recipe with a three-way merge, scope
  the tccutil reset guidance, and align the cancellation check with the
  channel timeout

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(node-repl): bound desktop relay connection failures

* fix(web-shell): preserve active desktop relay revocation

* docs(computer-use): correct setup and redact verification details

* fix(desktop-relay): close review security gaps

* fix(desktop-relay): harden process ownership and prewarm

* fix(desktop-relay): pin the setup command to a published node-repl version

DESKTOP_RELAY_INSTALL_COMMAND, packages/node-repl/README.md and
docs/users/features/computer-use.md all handed out
`npx -y @qwen-code/node-repl-mcp@latest desktop-relay install`. The
published `latest` (0.1.6) has no `desktop-relay` subcommand and its
entrypoint never reads argv, so it connects a stdio MCP server and blocks
on stdin: zero output, nothing installed, no launchd socket. The Web Shell
panel then re-serves that same string behind a Copy button in exactly the
`missing` phase the command produced, so the loop has no exit.

Pin the spec to the package version the way the repo already does for this
package (packages/core/src/skills/bundled/browser-use/skill.test.ts), and
bump packages/node-repl 0.1.6 -> 0.1.7 in this PR so the publish carries
the relay. Without the bump, cd-cua-driver.yml rejects the release with
"already exists with different integrity" and the pinned version would
still lack the subcommand. Sync the three version lines the release
validate-version gate reads (computer-use SKILL.md, browser-use SKILL.md,
computer-use user guide) plus the hard pin in
scripts/tests/release-workflow.test.js.

desktop-relay-client.test.ts now fails when the constant drifts from
packages/node-repl/package.json or reintroduces `@latest`.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(desktop-relay): stop reporting relay state that was never confirmed

Five review findings shared one cause: the Web Shell panel and the launchd
teardown each asserted a state they had not actually confirmed.

- deriveDesktopRelayStatus let another session's live relay mask this
  session's connect failure, because input.error was only consulted after
  the other-session return. The error now wins over other-session; a relay
  that is live for this session still outranks it, since that is the truth.
- awaitingApproval and error were component-global while the probe was
  scoped by sessionId and daemonUrl, so switching session or daemon showed
  the previous one's pending dialog or stale error. Both are reset when
  either changes.
- retainLiveDesktopRelayProbe kept a live observation for any non-ready
  probe without limit, so a relay that vanished stayed "connected" for as
  long as the panel was mounted. Retention now expires after three
  inconclusive probes, which still covers a transient blip.
- disconnectDesktopRelay resolved void and never read the response status,
  so a revocation that never reached the relay was indistinguishable from
  one that did, and disconnect() cleared the probe unconditionally. It now
  reports success only on an ok response, and the panel keeps its last
  observation until the relay confirms, leaving Disconnect retryable.
- currentUid() returned 0 under sudo, so uninstall queried gui/0 while the
  agent was loaded in the invoking user's domain. The existing "still
  loaded" guard in uninstallLaunchAgent therefore could not see the agent
  and deleted the plist out from under it. SUDO_UID now supplies the
  domain; Number() rather than parseInt() rejects a malformed value
  instead of truncating it into a wrong uid.

Addresses review findings R3-5, R3-6, R3-7, R2-10 and R3-14. Each has a
regression test that was verified to fail when its fix is reverted.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(desktop-relay): cancel stale approvals and bound registration

* fix(desktop-relay): keep revocation available when unreachable

* fix(desktop-relay): serialize consent and allow cold runtime startup

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-28 06:43:22 +00:00
..
scripts chore(release): bump node repl to 0.1.4 (#11771) 2026-09-13 10:38:51 +00:00
src feat(computer-use): let a remote session use your desktop through a node_repl relay (#11799) 2026-09-28 06:43:22 +00:00
.gitignore refactor(node-repl)!: deliver the persistent Node REPL as a standalone MCP server (#9499) 2026-08-23 14:20:39 +00:00
build.mjs feat(core): add CodeModeOnly tool execution (#10607) 2026-09-11 07:40:35 +00:00
LICENSE feat(computer-use): replace built-in tools with bundled skill (#9856) 2026-08-24 11:05:23 +00:00
package.json feat(computer-use): let a remote session use your desktop through a node_repl relay (#11799) 2026-09-28 06:43:22 +00:00
README.md feat(computer-use): let a remote session use your desktop through a node_repl relay (#11799) 2026-09-28 06:43:22 +00:00
tsconfig.build.json refactor(node-repl)!: deliver the persistent Node REPL as a standalone MCP server (#9499) 2026-08-23 14:20:39 +00:00
tsconfig.json refactor(node-repl)!: deliver the persistent Node REPL as a standalone MCP server (#9499) 2026-08-23 14:20:39 +00:00
vitest.config.ts fix(ci): align non-Linux Vitest unhandled-error handling across unit suites (#10443) 2026-08-31 08:59:48 +00:00

@qwen-code/node-repl-mcp

A standalone Model Context Protocol server that exposes a session-persistent Node.js REPL as five MCP tools. It runs a real Node.js kernel in a dedicated child process; top-level bindings, closures, and module state persist across calls within a session.

This package is fully independent of @qwen-code/qwen-code-core — any MCP client (Qwen Code via mcpServers, Claude, Codex, etc.) can run it.

Tools

Tool Description
node_repl Start one JavaScript cell. { code, timeout_ms?, yield_time_ms?, title? }; yields a cell ID if it remains active.
node_repl_wait Wait for the active cell by ID without cancelling it.
node_repl_cancel Cancel the active cell by ID; terminate an unresponsive kernel after five seconds.
node_repl_reset Terminate the kernel process and discard all bindings/module state.
node_repl_add_node_module_dir Register an extra node_modules directory for bare-package resolution.

Cell semantics

  • Explicit output only: nodeRepl.write(value) for text, nodeRepl.emitImage(png|jpeg|webp) for images; byte payloads may include JSON-serializable metadata, which is returned immediately before each retained image, independently of the prose output budget. Metadata for rejected or omitted images is also omitted. console.* is captured. Plain expression results are not returned.
  • nodeRepl.cwd / homeDir / tmpDir and nodeRepl.getHeapStatus() are available.
  • Top-level static import is not allowed — use dynamic await import().
  • Declared bindings stay live across cells: after replacing an observation, helpers that captured its binding read the replacement. Helper assignments and direct assignments share the same binding, including within one cell.
  • Bare packages resolve from the session cwd node_modules plus any directory registered via node_repl_add_node_module_dir; package entrypoints use Node singleton caching. Local .js/.mjs reload on each execution.
  • Node builtins are importable except process/node:process. Use (await import('node:module')).createRequire(import.meta.url) for CommonJS or native (N-API) addons.
  • Timeout and cancellation normally stop only the active cell. Earlier bindings and the kernel process remain available, while new bindings from that cell are not committed. If the kernel does not return a terminal result within five seconds of cancellation, the host terminates it and reports that all bindings were lost. A subsequent cell starts a fresh kernel. External actions may have completed, so verify external state before retrying. node_repl_reset or a real process crash also discards all bindings.
  • When the kernel is retained, runtime errors keep completed statement/declarator checkpoints; cancellation and timeout restore binding values from cell entry. Object mutations and external side effects are not rolled back.

Isolation note: the VM context provides lifecycle/namespace isolation, not an OS security sandbox. Imported packages and builtins run with ordinary Node.js authority and inherit the parent environment. Grant this server only in trusted contexts.

Usage

Once published, the packaged bin is the simplest entry point:

// qwen-code settings.json (or any MCP client)
{
  "mcpServers": {
    "node-repl": {
      "command": "npx",
      "args": ["-y", "@qwen-code/node-repl-mcp"],
      "cwd": "/your/workspace",
      "env": { "QWEN_NODE_REPL_ROOTS": "/extra/node_modules/parent" },
    },
  },
}

For local development against a build in this repo, point at dist/index.js:

{
  "mcpServers": {
    "node-repl": {
      "command": "node",
      "args": ["/path/to/packages/node-repl/dist/index.js"],
      "cwd": "/your/workspace",
    },
  },
}

The cwd you set is the kernel's working directory and the base for bare-package resolution (<cwd>/node_modules).

Environment:

  • QWEN_NODE_REPL_ROOTS — extra readable roots (path-list, OS delimiter).
  • QWEN_NODE_REPL_DEBUG — set truthy for stderr debug logging.

Linux desktop sessions

When using the CUA SDK, start the MCP server with the environment of the desktop session it will control. The kernel inherits the server's environment at startup; variables removed by the MCP client cannot be recovered by the SDK. For Codex, add the following allowlist to the existing server entry in config.toml:

[mcp_servers.node_repl]
command = "npx"
args = ["-y", "@qwen-code/node-repl-mcp"]
env_vars = ["DISPLAY", "XAUTHORITY", "DBUS_SESSION_BUS_ADDRESS", "AT_SPI_BUS_ADDRESS", "XDG_RUNTIME_DIR", "WAYLAND_DISPLAY", "GTK_MODULES", "QT_ACCESSIBILITY", "QT_LINUX_ACCESSIBILITY_ALWAYS_ON"]

The client process must already have the correct values. For hosts that use an explicit env map, populate it from that same desktop session; display numbers, authentication paths, and bus addresses vary by machine and user. Restart the MCP server after changing its environment. An X11 connection failure is reported as desktop_unavailable by CUA discovery, rather than as an empty desktop.

Uncaught errors preserve a string code and bounded details text in MCP output, including action and verification diagnostics. External side effects may already have happened when an error is thrown; use those diagnostics before retrying.

Desktop relay

node-repl-mcp desktop-relay lends this server to a Qwen Code session running somewhere else, typically a headless dev box, so the computer-use skill can drive the desktop you are sitting at. On macOS:

npx -y @qwen-code/node-repl-mcp@0.1.7 desktop-relay install

This installs the server and @qwen-code/cua-sdk under ~/.qwen/desktop-relay and registers a launchd socket on 127.0.0.1:47821 in inetd mode: nothing runs until something connects, and each connection gets its own short-lived process.

  • Web Shell. In a session served by a remote qwen serve (started with QWEN_SERVE_CLIENT_MCP_OVER_WS=1), choose Use this computer in the sidebar footer. A native dialog on this computer asks for approval; once allowed, the relay starts node_repl here and registers it for that one session over the daemon's reverse tool channel. Disconnect from the same entry.

An approved session can run code on this computer with your permissions and see and control its screen, exactly as a local node_repl can. Every connection is approved separately; nothing is remembered. desktop-relay status shows the last connection, desktop-relay uninstall [--purge] removes the socket (and the runtime).

If the browser cannot reach the relay, the remote connection may still be running. Request a local disconnect without using the loopback socket:

~/.qwen/desktop-relay/node_modules/.bin/node-repl-mcp desktop-relay disconnect

This checks the recorded process identity before sending SIGTERM; it does not download a package. For a custom installation, use that installation's executable and add --home <dir>. The command reports a disconnect request, not a confirmed shutdown; desktop-relay status shows the last recorded state.

Build & test

npm run build         # tsc (tsconfig.build.json) + copy runtime assets into dist/runtime
npm run typecheck     # includes the test files
npm test              # vitest: kernel integration, N-API, 100-cell + 10-kernel scale,
                      # line fidelity, hoisting semantics, MCP surface
npm run smoke           # kernel manager + output adapter, in-process
npm run smoke:mcp       # real MCP client <-> built stdio server
npm run smoke:lifecycle # proves the kernel child is reaped on stdin EOF / signals

Run vitest from inside this package. A bare npx vitest run from the repo root executes every workspace project (tens of thousands of tests) and can exhaust the default heap.

Provenance

Ported from the Qwen Code PR #9499 Node REPL core (kernel, module loader, cell transform, protocol, security policy, kernel manager). The qwen-coupled result converter was replaced by output-adapter.ts, which emits MCP content blocks. The empty-in-production trusted-package / sha256-pinning layer was removed entirely: this runtime has no trusted-package or capability mechanism.