* 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> |
||
|---|---|---|
| .. | ||
| scripts | ||
| src | ||
| .gitignore | ||
| build.mjs | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.build.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
@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-serializablemetadata, 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/tmpDirandnodeRepl.getHeapStatus()are available.- Top-level static
importis not allowed — use dynamicawait 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
cwdnode_modulesplus any directory registered vianode_repl_add_node_module_dir; package entrypoints use Node singleton caching. Local.js/.mjsreload 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_resetor 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 withQWEN_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 startsnode_replhere 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 runfrom 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.