You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/19 03:05:58 UTC

(maka) branch fix/wsl-computer-use-build created (now a6eb147477)

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a change to branch fix/wsl-computer-use-build
in repository https://gitbox.apache.org/repos/asf/maka.git


      at a6eb147477 fix(computer-use): isolate native desktop dependencies from WSL bridge builds

This branch includes the following new commits:

     new a6eb147477 fix(computer-use): isolate native desktop dependencies from WSL bridge builds

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


(maka) 01/01: fix(computer-use): isolate native desktop dependencies from WSL bridge builds

Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch fix/wsl-computer-use-build
in repository https://gitbox.apache.org/repos/asf/maka.git

commit a6eb147477d140ca7926ac8c04b30360ea43c11b
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Mon Oct 19 11:05:29 2026 +0800

    fix(computer-use): isolate native desktop dependencies from WSL bridge builds
---
 .github/workflows/ci.yml                           |   7 +
 Cargo.toml                                         |   2 +-
 README.md                                          |   2 +
 README.zh-CN.md                                    |   2 +
 justfile                                           |   2 +-
 platform/computer-use/Cargo.toml                   |  73 ++-
 platform/computer-use/README.md                    |  16 +-
 platform/computer-use/README.zh-CN.md              |  16 +-
 .../computer-use/src/{lib.rs => bridge_driver.rs}  |  39 +-
 platform/computer-use/src/browser.rs               |  33 +-
 platform/computer-use/src/cursor.rs                | 578 +--------------------
 .../src/{cursor.rs => cursor/native.rs}            |  53 +-
 platform/computer-use/src/desktop.rs               | 100 +++-
 platform/computer-use/src/desktop/worker.rs        |  75 ++-
 platform/computer-use/src/language.rs              |   6 +
 platform/computer-use/src/lib.rs                   |  10 +-
 platform/computer-use/tests/native.rs              |  26 +-
 platform/computer-use/tests/repl_contract.rs       |  13 +
 products/maka/cli/Cargo.toml                       |   4 +
 products/maka/cli/notices.json                     |   6 +-
 scripts/rust/check-cua-build.mjs                   |  68 +++
 scripts/rust/run-cli.mjs                           |  74 +++
 scripts/rust/run-cli.test.mjs                      | 144 +++++
 23 files changed, 620 insertions(+), 729 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 893fb3de01..f9089562c3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -106,6 +106,13 @@ jobs:
       - name: Select npm
         run: npm install --global --no-audit --no-fund "$(node -p 'require("./package.json").packageManager')"
       - run: just setup
+      - name: Verify Computer Use bridge build
+        run: |
+          node scripts/rust/check-cua-build.mjs
+          just product
+          cargo build --locked -p maka-cli --no-default-features
+          target/debug/maka --version
+          cargo test --locked -p maka-computer-use --no-default-features --lib --test native --test repl_contract
       - run: just check
       - run: just build
       - name: Native CLI smoke
diff --git a/Cargo.toml b/Cargo.toml
index 3244d0b35a..b231218357 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -134,7 +134,7 @@ maka-agent-native = { path = "plugins/agent/native" }
 maka-assistant = { path = "plugins/agent/assistant" }
 maka-background-health = { path = "platform/background-health" }
 maka-config = { path = "plugins/settings/store" }
-maka-computer-use = { path = "platform/computer-use" }
+maka-computer-use = { path = "platform/computer-use", default-features = false }
 maka-event-log = { path = "platform/event-log" }
 maka-external-agent = { path = "plugins/agent/acp-executor/engine" }
 maka-fs-tools = { path = "plugins/workspace/workspace-files/engine" }
diff --git a/README.md b/README.md
index 16e914b9f8..1bc3c00b46 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,8 @@ just run
 
 `just run` opens the terminal product. First launch confirms the current workspace and installs the bundled tools and skills; later launches reopen the saved application. Use `just run --help` for CLI commands. `just build --release` produces `target/release/maka`, `maka-desktop-shell` and the adjacent `app/` directory (executables use `.exe` on Windows). The build entry supports `--release` and `--target`. Node.js bundles provider SDKs for the CLI, which runs its embedded JavaScript in  [...]
 
+In WSL, `just run` builds the CLI without the local `native-desktop` capability or its Linux X11 dependencies; Computer Use selects the Windows desktop by default. Source builds need `MAKA_CUA_WINDOWS_EXECUTABLE` set to the absolute WSL-visible path of a matching Windows `maka.exe`. `MAKA_CUA_BUILD_MODE=auto|native|bridge` controls this development build selection. See [Computer Use](platform/computer-use/README.md) for setup. `just build` and release packaging still build the full nativ [...]
+
 Launch the desktop with `target/release/maka desktop --root HOST_ROOT`, selecting its actual business Host Root. The launcher initializes or reopens that Host and keeps its client alive while the shell runs. See [desktop-shell](products/maka/desktop-shell/README.md) for the packaged app and development commands.
 
 ## Repository
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 0b21a9caec..e62b7eb257 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -38,6 +38,8 @@ just run
 
 `just run` 打开终端产品,首次确认当前工作目录并安装内置工具与技能,后续重开已保存的应用。`just run --help` 查看 CLI 命令。`just build --release` 生成 `target/release/maka`、`maka-desktop-shell` 及相邻的 `app/` 目录,Windows 可执行文件使用 `.exe` 后缀。构建入口支持 `--release` 和 `--target`。Node.js 在构建时为 CLI 打包提供商 SDK,CLI 在内嵌 V8 中运行 JavaScript,桌面 shell 通过 QuickJS 执行 app。用 `just run application run --root PATH --deployment UUID --connections FILE` 启动已校验的终端组合,编写和安装方式见[普通 Agent 产品](products/maka/bundle/README.zh-CN.md)。
 
+在 WSL 中,`just run` 构建不含本地 `native-desktop` 能力及其 Linux X11 依赖的 CLI,Computer Use 默认选择 Windows 桌面。源码构建需通过 `MAKA_CUA_WINDOWS_EXECUTABLE` 指定匹配的 Windows `maka.exe`,使用 WSL 可见的绝对路径。`MAKA_CUA_BUILD_MODE=auto|native|bridge` 控制开发构建选择,设置方法见 [Computer Use](platform/computer-use/README.zh-CN.md)。`just build` 和发布打包仍构建完整原生产品,包含桌面 shell 及其 Linux GUI 依赖。
+
 通过 `target/release/maka desktop --root HOST_ROOT` 启动桌面,显式选择实际业务 Host Root。启动器初始化或重开 Host,并在 shell 运行期间保留其客户端。产物布局和开发命令见 [desktop-shell](products/maka/desktop-shell/README.zh-CN.md)。
 
 ## 目录
diff --git a/justfile b/justfile
index ae2db0998d..b02ed1c145 100644
--- a/justfile
+++ b/justfile
@@ -30,7 +30,7 @@ product:
 
 # Run the native CLI or TUI.
 run *args: product
-    cargo run --locked -p maka-cli -- "$@"
+    node scripts/rust/run-cli.mjs "$@"
 
 # Build the native CLI, desktop shell and authored app (--release / --target).
 build *args: product
diff --git a/platform/computer-use/Cargo.toml b/platform/computer-use/Cargo.toml
index e816f94bb7..19397291d6 100644
--- a/platform/computer-use/Cargo.toml
+++ b/platform/computer-use/Cargo.toml
@@ -22,40 +22,75 @@ edition.workspace = true
 license.workspace = true
 publish.workspace = true
 
+[features]
+default = []
+native-desktop = [
+    "dep:hex",
+    "dep:cua-driver-contract",
+    "dep:cua-driver-sdk",
+    "dep:cua-driver-core",
+    "dep:uuid",
+    "dep:libc",
+    "dep:base64",
+    "dep:cursor-overlay",
+    "dep:sha2",
+    "dep:tempfile",
+    "dep:platform-windows",
+    "dep:windows-sys",
+    "dep:platform-linux",
+    "dep:platform-macos",
+    "dep:core-foundation",
+    "dep:objc2",
+    "dep:objc2-foundation",
+    "dep:objc2-app-kit",
+]
+
 [dependencies]
-hex.workspace = true
+hex = { workspace = true, optional = true }
 maka-agent-facts.workspace = true
 maka-capabilities.workspace = true
-cua-driver-contract.workspace = true
-cua-driver-sdk.workspace = true
-cua-driver-core = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6" }
-maka-js-runtime.workspace = true
+cua-driver-contract = { workspace = true, optional = true }
+cua-driver-sdk = { workspace = true, optional = true }
+cua-driver-core = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6", optional = true }
 serde.workspace = true
 schemars.workspace = true
-uuid.workspace = true
-libc.workspace = true
+uuid = { workspace = true, optional = true }
+libc = { workspace = true, optional = true }
 reqwest.workspace = true
-base64 = "0.22.1"
-cursor-overlay = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6", features = ["theme-authoring"] }
-sha2.workspace = true
-tempfile.workspace = true
+base64 = { version = "0.22.1", optional = true }
+cursor-overlay = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6", features = ["theme-authoring"], optional = true }
+sha2 = { workspace = true, optional = true }
+tempfile = { workspace = true, optional = true }
 serde_json.workspace = true
 tokio = { workspace = true, features = ["io-std", "process"] }
 tokio-util.workspace = true
 
 [dev-dependencies]
+maka-js-runtime.workspace = true
 tiny-skia = { version = "0.11", features = ["png-format"] }
 
 [target.'cfg(windows)'.dependencies]
-platform-windows = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6" }
-windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_System_Threading"] }
+platform-windows = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6", optional = true }
+windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_System_Threading"], optional = true }
 
 [target.'cfg(target_os = "linux")'.dependencies]
-platform-linux = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6" }
+platform-linux = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6", optional = true }
 
 [target.'cfg(target_os = "macos")'.dependencies]
-platform-macos = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6" }
-core-foundation = "0.10.1"
-objc2 = "0.6.4"
-objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSArray", "NSData", "NSString"] }
-objc2-app-kit = { version = "0.3.2", default-features = false, features = ["std", "NSPasteboard", "NSPasteboardItem"] }
+platform-macos = { git = "https://github.com/maka-agent/cua", rev = "79c9b58a1b61f66ae966635c464e54cb2c8151d6", optional = true }
+core-foundation = { version = "0.10.1", optional = true }
+objc2 = { version = "0.6.4", optional = true }
+objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSArray", "NSData", "NSString"], optional = true }
+objc2-app-kit = { version = "0.3.2", default-features = false, features = ["std", "NSPasteboard", "NSPasteboardItem"], optional = true }
+
+[[example]]
+name = "cursor-preview"
+required-features = ["native-desktop"]
+
+[[example]]
+name = "desktop-host"
+required-features = ["native-desktop"]
+
+[[test]]
+name = "repl_browser"
+required-features = ["native-desktop"]
diff --git a/platform/computer-use/README.md b/platform/computer-use/README.md
index 7c352dfa4c..32c30e7c42 100644
--- a/platform/computer-use/README.md
+++ b/platform/computer-use/README.md
@@ -39,8 +39,22 @@ The `desktop` setting defaults to `auto`: the Windows host in WSL, otherwise the
 
 Browser providers are explicitly configured CDP connections to existing profiles. When no provider is configured, use a native browser window. Configuration and platform-specific methods are described by the API reference; a missing provider does not trigger a browser restart or enable debugging.
 
+## Native and WSL development builds
+
+The CLI enables the `native-desktop` Cargo feature by default. It includes the local driver and cursor renderer for Linux, macOS or Windows. Native dependencies require Xcode Command Line Tools on macOS and X11 development libraries on Linux.
+
+`just run` automatically omits this feature in WSL, keeping the Windows bridge without compiling the Linux desktop driver. Set `MAKA_CUA_BUILD_MODE` to `auto` (the default), `native` or `bridge` to select the development build explicitly. For example, `MAKA_CUA_BUILD_MODE=native just run` includes the Linux driver in WSL. The runtime `desktop` setting still selects the destination; a bridge build rejects `local`, and `auto` requires WSL to select Windows.
+
+Source builds need a Windows `maka.exe` built from the same source with its default native features. Build it on Windows and provide its absolute path as visible from WSL:
+
+```sh
+MAKA_CUA_WINDOWS_EXECUTABLE=/mnt/c/path/to/maka.exe just run
+```
+
+Plain `cargo run -p maka-cli` retains the native default. To build the bridge directly, use `cargo run --locked -p maka-cli --no-default-features -- ...`. Cargo features combine across selected packages, so another selected package enabling `native-desktop` restores native dependencies. `just build` and release packaging retain the full native product; the Linux desktop shell separately requires Linux GUI development libraries.
+
 ## Lifetime
 
 Reset clears this Session's bindings without closing user apps or tabs. Navigation, window changes and fresh observations can invalidate previous input targets. Cancellation or timeout requires a reset; an uncertain action is never replayed automatically. Shutdown waits for accepted native work to settle.
 
-Native dependencies require Xcode Command Line Tools on macOS and X11 development libraries on Linux. Verify input and capture behavior on the target desktop; a REPL unit test cannot establish native focus or accessibility behavior.
+Verify input and capture behavior on the target desktop; a REPL unit test cannot establish native focus or accessibility behavior.
diff --git a/platform/computer-use/README.zh-CN.md b/platform/computer-use/README.zh-CN.md
index e57b5986cd..abf48cf80a 100644
--- a/platform/computer-use/README.zh-CN.md
+++ b/platform/computer-use/README.zh-CN.md
@@ -39,8 +39,22 @@ const app = await cua.getApp("Calculator");
 
 浏览器提供方是显式配置的 CDP 连接,使用已有 profile。没有配置提供方时,可操作原生浏览器窗口。配置与平台方法见 API 说明;缺少提供方不会触发重启浏览器或开启调试。
 
+## 原生与 WSL 开发构建
+
+CLI 默认启用 Cargo feature `native-desktop`,包含 Linux、macOS 或 Windows 的本地驱动及游标渲染器。原生依赖在 macOS 上需要 Xcode Command Line Tools,在 Linux 上需要 X11 开发库。
+
+`just run` 在 WSL 中自动关闭该 feature,保留 Windows 桥接且无需编译 Linux 桌面驱动。可将 `MAKA_CUA_BUILD_MODE` 设置为 `auto`(默认)、`native` 或 `bridge`,显式选择开发构建。例如 `MAKA_CUA_BUILD_MODE=native just run` 会在 WSL 中编入 Linux 驱动。运行时仍由 `desktop` 配置选择目标;桥接构建拒绝 `local`,`auto` 需要在 WSL 中才能选择 Windows。
+
+源码构建需要由同一份源码、使用默认原生 feature 构建的 Windows `maka.exe`。在 Windows 上构建后,提供 WSL 可见的绝对路径:
+
+```sh
+MAKA_CUA_WINDOWS_EXECUTABLE=/mnt/c/path/to/maka.exe just run
+```
+
+直接使用 `cargo run -p maka-cli` 保留原生默认值。直接构建桥接版本可用 `cargo run --locked -p maka-cli --no-default-features -- ...`。Cargo 会合并所选包的 feature,若同时选择的其他包启用了 `native-desktop`,便会恢复原生依赖。`just build` 和发布打包保留完整原生产品;Linux 桌面 shell 自身仍需要 Linux GUI 开发库。
+
 ## 生命周期
 
 重置清除本 Session 的绑定,不关闭用户应用或标签页。导航、窗口变化和新观察可能使原输入目标失效。取消或超时后需要重置,结果不确定的动作不会自动重放。关闭流程等待已接受的原生工作完成清理。
 
-原生依赖在 macOS 上需要 Xcode Command Line Tools,在 Linux 上需要 X11 开发库。输入与捕获行为应在目标桌面验证;REPL 单元测试不能证明原生焦点或无障碍行为。
+输入与捕获行为应在目标桌面验证;REPL 单元测试不能证明原生焦点或无障碍行为。
diff --git a/platform/computer-use/src/lib.rs b/platform/computer-use/src/bridge_driver.rs
similarity index 51%
copy from platform/computer-use/src/lib.rs
copy to platform/computer-use/src/bridge_driver.rs
index 00b9a3c843..ef7312fa85 100644
--- a/platform/computer-use/src/lib.rs
+++ b/platform/computer-use/src/bridge_driver.rs
@@ -17,36 +17,17 @@
  * under the License.
  */
 
-//! Independent Computer Use REPL and Session-owned Cua targets.
-//!
-//! Host supplies authorization and journal ownership; this crate supplies the
-//! pinned native implementation. Perception and driver administration are absent.
+//! A bridge build owns only the Windows desktop worker transport.
+use maka_agent_facts::tools::ToolError;
 
-mod activity;
-mod browser;
-pub mod cursor;
-pub mod desktop;
-mod driver;
-mod identity;
-pub mod language;
-#[cfg(target_os = "macos")]
-mod macos;
-mod observation;
-pub mod protocol;
-mod session;
-
-pub use desktop::Session;
-pub use driver::Driver;
-pub fn facade() -> String {
-    facade_for(desktop::local_platform())
+#[derive(Default)]
+pub struct Driver {
+    pub(crate) windows: crate::desktop::worker::Worker,
 }
 
-pub fn facade_for(platform: &str) -> String {
-    format!(
-        "const nativePlatform = {platform:?};\n{}",
-        include_str!("facade.js")
-    )
+impl Driver {
+    /// Host calls this after all Session resources retire.
+    pub async fn close(&mut self) -> Result<(), ToolError> {
+        self.windows.close().await
+    }
 }
-
-pub const ID: &str = "maka.computer-use";
-pub const REVISION: &str = "cua-repl-v1";
diff --git a/platform/computer-use/src/browser.rs b/platform/computer-use/src/browser.rs
index b2258412d9..d70baf6601 100644
--- a/platform/computer-use/src/browser.rs
+++ b/platform/computer-use/src/browser.rs
@@ -35,38 +35,7 @@ struct ElementRef {
     frame: Option<frames::Frame>,
 }
 
-pub(crate) fn validate_connections(connections: &[BrowserConnection]) -> Result<(), ToolError> {
-    let mut ids = std::collections::HashSet::new();
-    if connections.len() > 8 {
-        return Err(failed("at most 8 browser providers may be configured"));
-    }
-    for connection in connections {
-        let url = reqwest::Url::parse(&connection.endpoint).map_err(failed)?;
-        let loopback = url.host_str().is_some_and(|host| {
-            host == "localhost"
-                || host
-                    .trim_matches(['[', ']'])
-                    .parse::<std::net::IpAddr>()
-                    .is_ok_and(|ip| ip.is_loopback())
-        });
-        if !loopback
-            || url.scheme() != "http"
-            || !url.username().is_empty()
-            || url.password().is_some()
-            || url.query().is_some()
-            || url.fragment().is_some()
-            || url.path() != "/"
-        {
-            return Err(failed(
-                "browser endpoint must be a loopback HTTP origin, without credentials, path or query",
-            ));
-        }
-        if connection.id.is_empty() || !ids.insert(&connection.id) {
-            return Err(failed("browser IDs must be nonempty and unique"));
-        }
-    }
-    Ok(())
-}
+use crate::desktop::validate_connections;
 
 #[derive(Default)]
 pub(crate) struct Browsers {
diff --git a/platform/computer-use/src/cursor.rs b/platform/computer-use/src/cursor.rs
index a9f0d94157..2b6faa20ba 100644
--- a/platform/computer-use/src/cursor.rs
+++ b/platform/computer-use/src/cursor.rs
@@ -17,47 +17,27 @@
  * under the License.
  */
 
-use maka_agent_facts::tools::ToolError;
+//! Cursor configuration shared with the desktop worker protocol.
 use serde::{Deserialize, Serialize};
-use std::{
-    path::PathBuf,
-    process::{ExitCode, Stdio},
-    sync::OnceLock,
-    time::Duration,
-};
-use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
-use tokio::process::{Child, ChildStdin, ChildStdout};
 
-mod helper;
+#[cfg(feature = "native-desktop")]
+mod native;
+#[cfg(feature = "native-desktop")]
 pub mod theme;
+#[cfg(feature = "native-desktop")]
+pub use native::bootstrap;
+#[cfg(feature = "native-desktop")]
+pub(crate) use native::{Host, RenderState, Spec};
 
-const ENTRY: &str = "__maka-cua-cursor";
-const MAX_MESSAGE: u64 = 16 * 1024;
-static HOST_EXECUTABLE: OnceLock<PathBuf> = OnceLock::new();
-static NEXT_COLOR: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
-
-/// Call at the executable entry point, before starting runtime threads.
-/// The private child only renders cursors; it has no computer input operations.
-pub fn bootstrap() -> Option<ExitCode> {
-    if std::env::args_os().nth(1).as_deref() == Some(std::ffi::OsStr::new(ENTRY)) {
-        return Some(match helper::run() {
-            Ok(()) => ExitCode::SUCCESS,
-            Err(error) => {
-                use std::io::Write as _;
-                let _ = writeln!(
-                    std::io::stdout(),
-                    "{}",
-                    serde_json::to_string(&RenderState::unavailable(&error)).unwrap()
-                );
-                let _ = writeln!(std::io::stderr(), "Maka cursor: {error}");
-                ExitCode::FAILURE
-            }
-        });
-    }
-    if let Ok(path) = std::env::current_exe() {
-        let _ = HOST_EXECUTABLE.set(path);
-    }
-    None
+/// Call before starting runtime threads to dispatch a private cursor helper.
+#[cfg(not(feature = "native-desktop"))]
+pub fn bootstrap() -> Option<std::process::ExitCode> {
+    (std::env::args_os().nth(1).as_deref() == Some(std::ffi::OsStr::new("__maka-cua-cursor"))).then(
+        || {
+            eprintln!("Maka cursor: native desktop support is not included in this build");
+            std::process::ExitCode::FAILURE
+        },
+    )
 }
 
 #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
@@ -109,527 +89,3 @@ pub struct Options {
     pub enabled: Option<bool>,
     pub reduced_motion: Option<bool>,
 }
-
-#[derive(Debug, Default, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub(crate) enum RenderStatus {
-    #[default]
-    Idle,
-    Ready,
-    Unavailable,
-}
-
-#[derive(Debug, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase", deny_unknown_fields)]
-pub(crate) struct RenderState {
-    pub status: RenderStatus,
-    pub visible: bool,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub requested_position: Option<[f64; 2]>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub theme: Option<String>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub renderer_pid: Option<u32>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub error: Option<String>,
-}
-impl RenderState {
-    pub(crate) fn unavailable(error: impl std::fmt::Display) -> Self {
-        Self {
-            status: RenderStatus::Unavailable,
-            error: Some(error.to_string()),
-            ..Default::default()
-        }
-    }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase", deny_unknown_fields)]
-pub(crate) struct Spec {
-    pub id: String,
-    pub label: String,
-    pub color: Color,
-    pub enabled: bool,
-    pub reduced_motion: bool,
-}
-impl Default for Spec {
-    fn default() -> Self {
-        let id = uuid::Uuid::new_v4();
-        Self {
-            color: Color::ALL
-                [NEXT_COLOR.fetch_add(1, std::sync::atomic::Ordering::Relaxed) % Color::ALL.len()],
-            id: id.to_string(),
-            label: "Maka".into(),
-            enabled: true,
-            reduced_motion: false,
-        }
-    }
-}
-impl Spec {
-    pub(crate) fn configure(&mut self, options: Options) -> Result<(), ToolError> {
-        if let Some(label) = &options.label
-            && (label.chars().count() > 48 || label.chars().any(char::is_control))
-        {
-            return Err(failed(
-                "cursor label must be at most 48 characters without control characters",
-            ));
-        }
-        if let Some(label) = options.label {
-            self.label = label;
-        }
-        if let Some(color) = options.color {
-            self.color = color;
-        }
-        if let Some(enabled) = options.enabled {
-            self.enabled = enabled;
-        }
-        if let Some(reduced) = options.reduced_motion {
-            self.reduced_motion = reduced;
-        }
-        Ok(())
-    }
-    pub(crate) fn theme_id(&self) -> String {
-        format!("org.apache.maka.cursor.{}", self.color.name())
-    }
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
-enum Request {
-    Update {
-        cursor: Spec,
-        point: Option<[f64; 2]>,
-        action: cursor_overlay::CursorAction,
-        window: Option<u64>,
-    },
-    State {
-        id: String,
-    },
-    Remove {
-        id: String,
-    },
-}
-
-pub(crate) struct Host {
-    executable: Option<PathBuf>,
-    sender: Option<tokio::sync::mpsc::Sender<Job>>,
-}
-struct Job {
-    command: Control,
-    reply: tokio::sync::oneshot::Sender<Result<RenderState, ToolError>>,
-}
-enum Control {
-    Call(Request),
-    Configure(Spec),
-    Stop,
-}
-impl Default for Host {
-    fn default() -> Self {
-        Self {
-            executable: HOST_EXECUTABLE.get().cloned(),
-            sender: None,
-        }
-    }
-}
-impl Host {
-    pub(crate) fn with_executable(path: PathBuf) -> Self {
-        Self {
-            executable: Some(path),
-            sender: None,
-        }
-    }
-    pub(crate) fn registered(&self) -> bool {
-        self.executable.is_some()
-    }
-    fn sender(&mut self) -> tokio::sync::mpsc::Sender<Job> {
-        self.sender
-            .get_or_insert_with(|| {
-                let (sender, mut receiver) = tokio::sync::mpsc::channel::<Job>(32);
-                let mut transport = Transport {
-                    executable: self.executable.clone(),
-                    process: None,
-                    last_error: None,
-                };
-                tokio::spawn(async move {
-                    while let Some(job) = receiver.recv().await {
-                        let stopping = matches!(job.command, Control::Stop);
-                        let result = match job.command {
-                            Control::Call(Request::State { id }) => Ok(transport.state(&id).await),
-                            Control::Call(Request::Remove { id }) => {
-                                transport.remove(&id).await;
-                                Ok(RenderState::default())
-                            }
-                            Control::Call(request) => transport.request(request).await,
-                            Control::Configure(cursor) => Ok(transport.configure(&cursor).await),
-                            Control::Stop => {
-                                transport.stop().await;
-                                Ok(RenderState::default())
-                            }
-                        };
-                        let _ = job.reply.send(result);
-                        if stopping {
-                            break;
-                        }
-                    }
-                    transport.stop().await;
-                });
-                sender
-            })
-            .clone()
-    }
-    async fn call(&mut self, command: Control) -> Result<RenderState, ToolError> {
-        if self
-            .sender
-            .as_ref()
-            .is_some_and(tokio::sync::mpsc::Sender::is_closed)
-        {
-            self.sender = None;
-        }
-        let (reply, receive) = tokio::sync::oneshot::channel();
-        self.sender()
-            .send(Job { command, reply })
-            .await
-            .map_err(|_| failed("cursor renderer owner closed"))?;
-        receive
-            .await
-            .map_err(|_| failed("cursor renderer owner stopped"))?
-    }
-    pub(crate) async fn update(
-        &mut self,
-        cursor: &Spec,
-        point: Option<[f64; 2]>,
-        action: cursor_overlay::CursorAction,
-        window: Option<u64>,
-    ) -> Result<RenderState, ToolError> {
-        self.call(Control::Call(Request::Update {
-            cursor: cursor.clone(),
-            point,
-            action,
-            window,
-        }))
-        .await
-    }
-    pub(crate) async fn state(&mut self, cursor: &Spec) -> RenderState {
-        self.call(Control::Call(Request::State {
-            id: cursor.id.clone(),
-        }))
-        .await
-        .unwrap_or_else(RenderState::unavailable)
-    }
-    pub(crate) async fn configure(&mut self, cursor: &Spec) -> RenderState {
-        self.call(Control::Configure(cursor.clone()))
-            .await
-            .unwrap_or_else(RenderState::unavailable)
-    }
-    pub(crate) async fn remove(&mut self, id: &str) {
-        if self.sender.is_some() {
-            let _ = self
-                .call(Control::Call(Request::Remove { id: id.into() }))
-                .await;
-        }
-    }
-    pub(crate) async fn stop(&mut self) {
-        if self.sender.is_some() {
-            let _ = self.call(Control::Stop).await;
-            self.sender = None;
-        }
-    }
-}
-
-struct Transport {
-    executable: Option<PathBuf>,
-    process: Option<Process>,
-    last_error: Option<String>,
-}
-struct Process {
-    child: Child,
-    input: ChildStdin,
-    output: BufReader<ChildStdout>,
-    _themes: tempfile::TempDir,
-}
-impl Transport {
-    async fn start(&mut self) -> Result<(), ToolError> {
-        if self.process.is_some() {
-            return Ok(());
-        }
-        let executable=self.executable.as_ref().ok_or_else(|| failed("cursor_unavailable: this executable did not register the native cursor entry point"))?;
-        let themes = tempfile::Builder::new()
-            .prefix("maka-cursors-")
-            .tempdir()
-            .map_err(failed)?;
-        theme::write_artifacts(themes.path()).map_err(failed)?;
-        let mut command = tokio::process::Command::new(executable);
-        command
-            .arg(ENTRY)
-            .stdin(Stdio::piped())
-            .stdout(Stdio::piped())
-            .stderr(Stdio::null())
-            .kill_on_drop(true)
-            .env_clear();
-        for name in [
-            "HOME",
-            "PATH",
-            "LANG",
-            "LC_ALL",
-            "DISPLAY",
-            "WAYLAND_DISPLAY",
-            "XDG_RUNTIME_DIR",
-            "XAUTHORITY",
-            "DBUS_SESSION_BUS_ADDRESS",
-            "LOCALAPPDATA",
-            "SYSTEMROOT",
-            "WINDIR",
-            "TEMP",
-            "TMP",
-        ] {
-            if let Some(value) = std::env::var_os(name) {
-                command.env(name, value);
-            }
-        }
-        command.env("CUA_DRIVER_CURSOR_THEME_DIR", themes.path());
-        #[cfg(unix)]
-        {
-            use std::os::unix::process::CommandExt;
-            command.as_std_mut().process_group(0);
-        }
-        #[cfg(windows)]
-        command.creation_flags(
-            windows_sys::Win32::System::Threading::CREATE_NEW_PROCESS_GROUP
-                | windows_sys::Win32::System::Threading::CREATE_NO_WINDOW,
-        );
-        let mut child = command.spawn().map_err(failed)?;
-        let input = child
-            .stdin
-            .take()
-            .ok_or_else(|| failed("cursor stdin unavailable"))?;
-        let output = BufReader::new(
-            child
-                .stdout
-                .take()
-                .ok_or_else(|| failed("cursor stdout unavailable"))?,
-        );
-        self.process = Some(Process {
-            child,
-            input,
-            output,
-            _themes: themes,
-        });
-        Ok(())
-    }
-    async fn request(&mut self, request: Request) -> Result<RenderState, ToolError> {
-        if let Some(error) = &self.last_error {
-            return Err(failed(error));
-        }
-        let starting = self.process.is_none();
-        if let Err(error) = self.start().await {
-            self.last_error = Some(error.to_string());
-            return Err(error);
-        }
-        let process = self.process.as_mut().unwrap();
-        let result =
-            tokio::time::timeout(Duration::from_secs(if starting { 10 } else { 3 }), async {
-                let mut bytes = serde_json::to_vec(&request).map_err(failed)?;
-                bytes.push(b'\n');
-                process.input.write_all(&bytes).await.map_err(failed)?;
-                let mut line = String::new();
-                (&mut process.output)
-                    .take(MAX_MESSAGE + 1)
-                    .read_line(&mut line)
-                    .await
-                    .map_err(failed)?;
-                if line.is_empty() || line.len() as u64 > MAX_MESSAGE {
-                    return Err(failed("cursor renderer returned an invalid response"));
-                }
-                let value: RenderState = serde_json::from_str(&line).map_err(failed)?;
-                if let Some(error) = &value.error {
-                    return Err(failed(error));
-                }
-                Ok(value)
-            })
-            .await
-            .unwrap_or_else(|_| Err(failed("cursor renderer did not respond")));
-        if let Err(error) = &result {
-            self.last_error = Some(error.to_string());
-            self.stop().await;
-        }
-        result
-    }
-    pub(crate) async fn update(
-        &mut self,
-        cursor: &Spec,
-        point: Option<[f64; 2]>,
-        action: cursor_overlay::CursorAction,
-        window: Option<u64>,
-    ) -> Result<RenderState, ToolError> {
-        self.request(Request::Update {
-            cursor: cursor.clone(),
-            point,
-            action,
-            window,
-        })
-        .await
-    }
-    async fn state(&mut self, id: &str) -> RenderState {
-        if self.process.is_none() {
-            return match &self.last_error {
-                Some(error) => RenderState::unavailable(error),
-                None if self.executable.is_none() => {
-                    RenderState::unavailable("no cursor display host registered")
-                }
-                None => RenderState::default(),
-            };
-        }
-        match self.request(Request::State { id: id.into() }).await {
-            Ok(value) => value,
-            Err(error) => RenderState::unavailable(error),
-        }
-    }
-    pub(crate) async fn configure(&mut self, cursor: &Spec) -> RenderState {
-        self.last_error = None;
-        if self.process.is_some() {
-            match self
-                .update(cursor, None, cursor_overlay::CursorAction::Idle, None)
-                .await
-            {
-                Ok(value) => value,
-                Err(error) => RenderState::unavailable(error),
-            }
-        } else {
-            self.state(&cursor.id).await
-        }
-    }
-    pub(crate) async fn remove(&mut self, id: &str) {
-        if self.process.is_some() {
-            let _ = self.request(Request::Remove { id: id.into() }).await;
-        }
-    }
-    pub(crate) async fn stop(&mut self) {
-        if let Some(mut process) = self.process.take() {
-            let _ = process.input.shutdown().await;
-            drop(process.input);
-            if tokio::time::timeout(Duration::from_secs(2), process.child.wait())
-                .await
-                .is_err()
-            {
-                let _ = process.child.kill().await;
-                let _ = process.child.wait().await;
-            }
-        }
-    }
-}
-fn failed(error: impl std::fmt::Display) -> ToolError {
-    ToolError::Failed(error.to_string())
-}
-
-impl crate::protocol::Action {
-    pub(crate) fn cursor_target(&self) -> Option<crate::protocol::Position> {
-        use crate::protocol::{Action, Position};
-        match self {
-            Action::MoveCursor { target }
-            | Action::Click { target, .. }
-            | Action::Scroll { target, .. } => Some(target.clone()),
-            Action::Drag { from, .. } => Some(Position::Point(*from)),
-            Action::SetValue { index, .. }
-            | Action::SelectText { index, .. }
-            | Action::Secondary { index, .. } => Some(Position::Element(*index)),
-            Action::TypeText { index, .. }
-            | Action::Paste { index, .. }
-            | Action::PressKey { index, .. } => index.map(Position::Element),
-            _ => None,
-        }
-    }
-    pub(crate) fn cursor_action(&self) -> cursor_overlay::CursorAction {
-        use crate::protocol::Action;
-        use cursor_overlay::CursorAction as Cue;
-        match self {
-            Action::MoveCursor { .. } => Cue::Idle,
-            Action::Click { .. } | Action::Secondary { .. } => Cue::Click,
-            Action::Drag { .. } => Cue::Drag,
-            Action::Scroll { .. } => Cue::Scroll,
-            Action::TypeText { .. }
-            | Action::Paste { .. }
-            | Action::SetValue { .. }
-            | Action::SelectText { .. } => Cue::Text,
-            Action::PressKey { .. } => Cue::Key,
-            Action::Navigate { .. } | Action::Back | Action::Forward | Action::Reload => {
-                Cue::Navigate
-            }
-            Action::Close => Cue::App,
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    #[tokio::test]
-    #[ignore = "requires MAKA_CUA_TEST_HOST; displays a native cursor without sending input"]
-    async fn renderer_keeps_cursor_until_disabled_and_exits_with_its_channel() {
-        let executable =
-            std::env::var_os("MAKA_CUA_TEST_HOST").expect("set the built maka executable");
-        let mut host = Host::with_executable(executable.clone().into());
-        let mut cursor = Spec {
-            enabled: false,
-            color: Color::Blue,
-            ..Default::default()
-        };
-        let state = host
-            .update(&cursor, None, cursor_overlay::CursorAction::Idle, None)
-            .await
-            .unwrap();
-        assert_eq!(state.theme.as_deref(), Some(cursor.theme_id().as_str()));
-        assert!(!state.visible);
-        let pid = state.renderer_pid.unwrap();
-        cursor.enabled = true;
-        host.update(
-            &cursor,
-            Some([60.0, 60.0]),
-            cursor_overlay::CursorAction::Click,
-            None,
-        )
-        .await
-        .unwrap();
-        tokio::time::sleep(Duration::from_secs(4)).await;
-        assert!(
-            host.state(&cursor).await.visible,
-            "an idle Session keeps its cursor"
-        );
-        cursor.enabled = false;
-        assert!(!host.configure(&cursor).await.visible);
-        host.stop().await;
-        #[cfg(unix)]
-        assert_ne!(
-            unsafe { libc::kill(pid as i32, 0) },
-            0,
-            "renderer must exit after EOF"
-        );
-        #[cfg(not(unix))]
-        let _ = pid;
-
-        // A broken response pipe must not strand the AppKit main loop after
-        // its control thread encounters an I/O error.
-        let mut child = tokio::process::Command::new(executable)
-            .arg(ENTRY)
-            .stdin(Stdio::piped())
-            .stdout(Stdio::piped())
-            .stderr(Stdio::null())
-            .kill_on_drop(true)
-            .spawn()
-            .unwrap();
-        drop(child.stdout.take());
-        let mut request = serde_json::to_vec(&Request::State { id: cursor.id }).unwrap();
-        request.push(b'\n');
-        child
-            .stdin
-            .as_mut()
-            .unwrap()
-            .write_all(&request)
-            .await
-            .unwrap();
-        let exit = tokio::time::timeout(Duration::from_secs(10), child.wait())
-            .await
-            .expect("broken pipe must stop the renderer")
-            .unwrap();
-        assert!(!exit.success());
-    }
-}
diff --git a/platform/computer-use/src/cursor.rs b/platform/computer-use/src/cursor/native.rs
similarity index 93%
copy from platform/computer-use/src/cursor.rs
copy to platform/computer-use/src/cursor/native.rs
index a9f0d94157..42164e966e 100644
--- a/platform/computer-use/src/cursor.rs
+++ b/platform/computer-use/src/cursor/native.rs
@@ -28,8 +28,9 @@ use std::{
 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
 use tokio::process::{Child, ChildStdin, ChildStdout};
 
+#[path = "helper.rs"]
 mod helper;
-pub mod theme;
+use super::{Color, Options, theme};
 
 const ENTRY: &str = "__maka-cua-cursor";
 const MAX_MESSAGE: u64 = 16 * 1024;
@@ -60,56 +61,6 @@ pub fn bootstrap() -> Option<ExitCode> {
     None
 }
 
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum Color {
-    Blue,
-    Mint,
-    Violet,
-    Amber,
-    Rose,
-    Cyan,
-}
-impl Color {
-    pub const ALL: [Self; 6] = [
-        Self::Blue,
-        Self::Mint,
-        Self::Violet,
-        Self::Amber,
-        Self::Rose,
-        Self::Cyan,
-    ];
-    pub fn name(self) -> &'static str {
-        match self {
-            Self::Blue => "blue",
-            Self::Mint => "mint",
-            Self::Violet => "violet",
-            Self::Amber => "amber",
-            Self::Rose => "rose",
-            Self::Cyan => "cyan",
-        }
-    }
-    pub fn rgba(self) -> [u8; 4] {
-        match self {
-            Self::Blue => [102, 158, 255, 255],
-            Self::Mint => [41, 176, 147, 255],
-            Self::Violet => [155, 123, 237, 255],
-            Self::Amber => [224, 159, 48, 255],
-            Self::Rose => [228, 112, 153, 255],
-            Self::Cyan => [48, 170, 211, 255],
-        }
-    }
-}
-
-#[derive(Debug, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase", deny_unknown_fields)]
-pub struct Options {
-    pub label: Option<String>,
-    pub color: Option<Color>,
-    pub enabled: Option<bool>,
-    pub reduced_motion: Option<bool>,
-}
-
 #[derive(Debug, Default, Serialize, Deserialize)]
 #[serde(rename_all = "snake_case")]
 pub(crate) enum RenderStatus {
diff --git a/platform/computer-use/src/desktop.rs b/platform/computer-use/src/desktop.rs
index fe59c882ce..8dd56c860b 100644
--- a/platform/computer-use/src/desktop.rs
+++ b/platform/computer-use/src/desktop.rs
@@ -18,7 +18,9 @@
  */
 
 //! Select the desktop independently of the OS running the Agent.
-use crate::{Driver, protocol::Command, session};
+#[cfg(feature = "native-desktop")]
+use crate::session;
+use crate::{Driver, protocol::Command};
 use maka_agent_facts::{language::Language, tools::ToolError};
 use maka_capabilities::computer::BrowserConnection;
 use maka_capabilities::computer::Desktop;
@@ -74,6 +76,7 @@ pub struct Session {
     closed: bool,
 }
 enum Backend {
+    #[cfg(feature = "native-desktop")]
     Local(session::Session),
     Windows {
         id: Option<String>,
@@ -93,6 +96,7 @@ impl Session {
     /// Remote/unknown bindings use a generic action, never a guessed window name.
     pub fn activity(&self, command: &Command) -> maka_agent_facts::display::Text {
         let target = match (&self.backend, command) {
+            #[cfg(feature = "native-desktop")]
             (
                 Some(Backend::Local(local)),
                 Command::Observe { handle, .. } | Command::Action { handle, .. },
@@ -111,7 +115,7 @@ impl Session {
         if self.closed {
             return Err(failed("Computer Use Session is closed"));
         }
-        crate::browser::validate_connections(&browsers)?;
+        validate_connections(&browsers)?;
         let windows = use_windows(desktop, local_platform(), is_wsl())?;
         if let Some(backend) = &self.backend
             && windows != matches!(backend, Backend::Windows { .. })
@@ -124,17 +128,23 @@ impl Session {
                 Language::ZhTw => "桌面設定已變更;請先重設 Cua,再切換桌面",
             }));
         }
-        let backend = self.backend.get_or_insert_with(|| {
-            if windows {
+        if self.backend.is_none() {
+            self.backend = Some(if windows {
                 Backend::Windows {
                     id: None,
                     browsers: Vec::new(),
                 }
             } else {
-                Backend::Local(session::Session::new(self.driver.clone()))
-            }
-        });
-        match backend {
+                #[cfg(feature = "native-desktop")]
+                {
+                    Backend::Local(session::Session::new(self.driver.clone()))
+                }
+                #[cfg(not(feature = "native-desktop"))]
+                return Err(native_unavailable());
+            });
+        }
+        match self.backend.as_mut().unwrap() {
+            #[cfg(feature = "native-desktop")]
             Backend::Local(local) => local.configure_browsers(browsers, language),
             Backend::Windows {
                 browsers: current, ..
@@ -205,6 +215,7 @@ impl Session {
             self.configure(Desktop::Auto, Vec::new(), language)?;
         }
         match self.backend.as_mut().unwrap() {
+            #[cfg(feature = "native-desktop")]
             Backend::Local(local) => local.invoke(command, session, cancellation, language).await,
             Backend::Windows { id, browsers } => {
                 if id.as_deref().is_some_and(|id| id != session) {
@@ -234,6 +245,7 @@ impl Session {
     pub async fn close(&mut self) -> Result<(), ToolError> {
         self.closed = true;
         match &mut self.backend {
+            #[cfg(feature = "native-desktop")]
             Some(Backend::Local(local)) => local.close().await,
             Some(Backend::Windows { id: Some(id), .. }) => {
                 self.driver.lock().await.windows.release_session(id).await?;
@@ -245,6 +257,46 @@ impl Session {
     }
 }
 
+pub(crate) fn validate_connections(connections: &[BrowserConnection]) -> Result<(), ToolError> {
+    let mut ids = std::collections::HashSet::new();
+    if connections.len() > 8 {
+        return Err(failed("at most 8 browser providers may be configured"));
+    }
+    for connection in connections {
+        let url = reqwest::Url::parse(&connection.endpoint).map_err(failed)?;
+        let loopback = url.host_str().is_some_and(|host| {
+            host == "localhost"
+                || host
+                    .trim_matches(['[', ']'])
+                    .parse::<std::net::IpAddr>()
+                    .is_ok_and(|ip| ip.is_loopback())
+        });
+        if !loopback
+            || url.scheme() != "http"
+            || !url.username().is_empty()
+            || url.password().is_some()
+            || url.query().is_some()
+            || url.fragment().is_some()
+            || url.path() != "/"
+        {
+            return Err(failed(
+                "browser endpoint must be a loopback HTTP origin, without credentials, path or query",
+            ));
+        }
+        if connection.id.is_empty() || !ids.insert(&connection.id) {
+            return Err(failed("browser IDs must be nonempty and unique"));
+        }
+    }
+    Ok(())
+}
+
+#[cfg(not(feature = "native-desktop"))]
+fn native_unavailable() -> ToolError {
+    failed(
+        "Local desktop support is not included in this build; build Maka with the native-desktop feature, or use the Windows desktop from WSL",
+    )
+}
+
 fn failed(error: impl std::fmt::Display) -> ToolError {
     ToolError::Failed(error.to_string())
 }
@@ -273,4 +325,36 @@ mod tests {
             }
         }
     }
+
+    #[cfg(not(feature = "native-desktop"))]
+    #[tokio::test]
+    async fn bridge_build_rejects_local_configuration_and_keeps_windows_available() {
+        let driver = Arc::new(Mutex::new(Driver::default()));
+        let mut session = Session::new(driver.clone());
+        let error = session
+            .configure(Desktop::Local, Vec::new(), Language::En)
+            .unwrap_err();
+        assert_eq!(
+            error.to_string(),
+            "tool failed: Local desktop support is not included in this build; build Maka with the native-desktop feature, or use the Windows desktop from WSL"
+        );
+        // Auto may select the Windows worker only in WSL. Elsewhere it must
+        // report the missing native capability during configuration too.
+        if !is_wsl() {
+            assert_eq!(
+                session
+                    .configure(Desktop::Auto, Vec::new(), Language::En)
+                    .unwrap_err()
+                    .to_string(),
+                error.to_string()
+            );
+        } else {
+            session
+                .configure(Desktop::Auto, Vec::new(), Language::En)
+                .unwrap();
+            assert_eq!(session.platform(), "windows");
+        }
+        session.close().await.unwrap();
+        driver.lock().await.close().await.unwrap();
+    }
 }
diff --git a/platform/computer-use/src/desktop/worker.rs b/platform/computer-use/src/desktop/worker.rs
index f82e0e8cbe..b0aae74a56 100644
--- a/platform/computer-use/src/desktop/worker.rs
+++ b/platform/computer-use/src/desktop/worker.rs
@@ -18,25 +18,33 @@
  */
 
 //! Private, parent-owned desktop worker. There is no listener or reconnect path.
-use super::{failed, local_platform};
-use crate::{Driver, protocol::Command, session::Session};
+use super::failed;
+#[cfg(feature = "native-desktop")]
+use super::local_platform;
+use crate::protocol::Command;
+#[cfg(feature = "native-desktop")]
+use crate::{Driver, session::Session};
 use maka_agent_facts::tools::ToolError;
 use maka_capabilities::computer::BrowserConnection;
 use serde::{Deserialize, Serialize, de::DeserializeOwned};
 use serde_json::Value;
+#[cfg(feature = "native-desktop")]
+use std::{collections::HashMap, sync::Arc};
 use std::{
-    collections::HashMap,
     io,
     path::PathBuf,
     process::{ExitCode, Stdio},
-    sync::Arc,
     time::Duration,
 };
+#[cfg(feature = "native-desktop")]
 use tokio::{
-    io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader},
-    process::{Child, ChildStdin, ChildStdout},
+    io::AsyncWrite,
     sync::{Mutex, mpsc},
 };
+use tokio::{
+    io::{AsyncRead, AsyncReadExt, AsyncWriteExt, BufReader},
+    process::{Child, ChildStdin, ChildStdout},
+};
 use tokio_util::sync::CancellationToken;
 
 const ENTRY: &str = "__maka-cua-desktop";
@@ -296,10 +304,20 @@ pub fn bootstrap() -> Option<ExitCode> {
     let result = if std::env::args_os().len() != 2 {
         Err(io::Error::other("desktop worker takes no arguments"))
     } else {
-        tokio::runtime::Builder::new_current_thread()
-            .enable_all()
-            .build()
-            .and_then(|runtime| runtime.block_on(serve_stdio()))
+        #[cfg(feature = "native-desktop")]
+        {
+            tokio::runtime::Builder::new_current_thread()
+                .enable_all()
+                .build()
+                .and_then(|runtime| runtime.block_on(serve_stdio()))
+        }
+        #[cfg(not(feature = "native-desktop"))]
+        {
+            Err(io::Error::new(
+                io::ErrorKind::Unsupported,
+                "native desktop support is not included in this build; rebuild with the native-desktop feature",
+            ))
+        }
     };
     Some(match result {
         Ok(()) => ExitCode::SUCCESS,
@@ -310,6 +328,7 @@ pub fn bootstrap() -> Option<ExitCode> {
     })
 }
 
+#[cfg(feature = "native-desktop")]
 async fn serve_stdio() -> io::Result<()> {
     let (send, receive) = mpsc::channel(1);
     let disconnected = CancellationToken::new();
@@ -329,6 +348,7 @@ async fn serve_stdio() -> io::Result<()> {
     serve(receive, tokio::io::stdout(), disconnected, driver).await
 }
 
+#[cfg(feature = "native-desktop")]
 async fn serve(
     mut receive: mpsc::Receiver<Request>,
     mut output: impl AsyncWrite + Unpin,
@@ -439,6 +459,7 @@ fn encode(value: &impl Serialize) -> io::Result<Vec<u8>> {
     bytes[..4].copy_from_slice(&(size as u32).to_be_bytes());
     Ok(bytes)
 }
+#[cfg(feature = "native-desktop")]
 async fn write(output: &mut (impl AsyncWrite + Unpin), value: &impl Serialize) -> io::Result<()> {
     output.write_all(&encode(value)?).await?;
     output.flush().await
@@ -448,6 +469,7 @@ async fn read<R: AsyncRead + Unpin, T: DeserializeOwned>(input: &mut R) -> io::R
     input.read_exact(&mut bytes).await?;
     serde_json::from_slice(&bytes).map_err(Into::into)
 }
+#[cfg(any(feature = "native-desktop", test))]
 fn read_sync<T: DeserializeOwned>(input: &mut impl io::Read) -> io::Result<T> {
     let mut header = [0; 4];
     input.read_exact(&mut header)?;
@@ -470,6 +492,37 @@ mod tests {
         }
     }
 
+    #[test]
+    fn desktop_frames_preserve_invoke_language() {
+        use maka_agent_facts::language::Language;
+
+        for (language, tag) in [
+            (Language::ZhCn, "zh-CN"),
+            (Language::ZhTw, "zh-TW"),
+            (Language::En, "en"),
+        ] {
+            let request = Request::Invoke {
+                session: "same-session".into(),
+                browsers: Vec::new(),
+                command: Command::Documentation,
+                language,
+            };
+            let framed = encode(&request).unwrap();
+            let decoded: Request = read_sync(&mut framed.as_slice()).unwrap();
+            assert_eq!(
+                serde_json::to_value(decoded).unwrap(),
+                json!({
+                    "method": "invoke",
+                    "session": "same-session",
+                    "browsers": [],
+                    "command": { "method": "documentation" },
+                    "language": tag,
+                }),
+            );
+        }
+    }
+
+    #[cfg(feature = "native-desktop")]
     #[tokio::test]
     async fn desktop_sessions_own_targets_and_reset_independently() {
         let (send, receive) = mpsc::channel(1);
@@ -514,6 +567,7 @@ mod tests {
         task.await.unwrap().unwrap();
     }
 
+    #[cfg(feature = "native-desktop")]
     #[tokio::test]
     async fn desktop_transport_keeps_language_per_invoke_not_per_session() {
         use maka_agent_facts::language::Language;
@@ -557,6 +611,7 @@ mod tests {
         task.await.unwrap().unwrap();
     }
 
+    #[cfg(feature = "native-desktop")]
     #[tokio::test]
     async fn disconnected_desktop_discards_queued_work_and_closes_output() {
         let (send, receive) = mpsc::channel(1);
diff --git a/platform/computer-use/src/language.rs b/platform/computer-use/src/language.rs
index 6347a15cdb..646dc39cd2 100644
--- a/platform/computer-use/src/language.rs
+++ b/platform/computer-use/src/language.rs
@@ -20,6 +20,7 @@
 //! Invocation-owned copy for CUA documentation and recovery guidance.
 use maka_agent_facts::language::Language;
 
+#[cfg(feature = "native-desktop")]
 pub(crate) fn documentation(language: Language) -> &'static str {
     match language {
         Language::En => include_str!("api.md"),
@@ -57,6 +58,7 @@ pub fn closed(language: Language) -> &'static str {
         }
     }
 }
+#[cfg(feature = "native-desktop")]
 pub(crate) fn uncertain(language: Language) -> &'static str {
     match language {
         Language::En => {
@@ -67,6 +69,7 @@ pub(crate) fn uncertain(language: Language) -> &'static str {
     }
 }
 
+#[cfg(feature = "native-desktop")]
 pub(crate) fn inactive(language: Language) -> &'static str {
     match language {
         Language::En => "Computer Use session is inactive. Call cua_reset and rebind targets.",
@@ -74,6 +77,7 @@ pub(crate) fn inactive(language: Language) -> &'static str {
         Language::ZhTw => "Computer Use 工作階段已失效。請呼叫 cua_reset 並重新綁定目標。",
     }
 }
+#[cfg(feature = "native-desktop")]
 pub(crate) fn terminal(language: Language) -> &'static str {
     match language {
         Language::En => {
@@ -83,6 +87,7 @@ pub(crate) fn terminal(language: Language) -> &'static str {
         Language::ZhTw => "\n請呼叫 cua_reset 並重新綁定目標。此次被拒絕的操作未派發輸入。",
     }
 }
+#[cfg(feature = "native-desktop")]
 pub(crate) fn background_unavailable(language: Language) -> &'static str {
     match language {
         Language::En => {
@@ -99,6 +104,7 @@ pub(crate) fn background_unavailable(language: Language) -> &'static str {
 
 // CDP preserves the original error and uncertainty class; the accepted browser
 // dispatch owns this warning for every unknown outcome.
+#[cfg(feature = "native-desktop")]
 pub(crate) fn browser_unknown(language: Language) -> &'static str {
     match language {
         Language::En => "; repeating a mutation may duplicate its effects",
diff --git a/platform/computer-use/src/lib.rs b/platform/computer-use/src/lib.rs
index 00b9a3c843..f8dd8e1e0c 100644
--- a/platform/computer-use/src/lib.rs
+++ b/platform/computer-use/src/lib.rs
@@ -23,16 +23,24 @@
 //! pinned native implementation. Perception and driver administration are absent.
 
 mod activity;
+#[cfg(feature = "native-desktop")]
 mod browser;
 pub mod cursor;
 pub mod desktop;
+#[cfg(feature = "native-desktop")]
 mod driver;
+#[cfg(not(feature = "native-desktop"))]
+#[path = "bridge_driver.rs"]
+mod driver;
+#[cfg(feature = "native-desktop")]
 mod identity;
 pub mod language;
-#[cfg(target_os = "macos")]
+#[cfg(all(feature = "native-desktop", target_os = "macos"))]
 mod macos;
+#[cfg(feature = "native-desktop")]
 mod observation;
 pub mod protocol;
+#[cfg(feature = "native-desktop")]
 mod session;
 
 pub use desktop::Session;
diff --git a/platform/computer-use/tests/native.rs b/platform/computer-use/tests/native.rs
index 0a218b88dc..fc01755e18 100644
--- a/platform/computer-use/tests/native.rs
+++ b/platform/computer-use/tests/native.rs
@@ -69,16 +69,19 @@ async fn wsl_uses_windows_targets_input_cursors_and_session_cleanup() {
     assert!(
         matches!(a.configure(Desktop::Local, vec![], maka_agent_facts::language::Language::En), Err(maka_agent_facts::tools::ToolError::Failed(message)) if message.contains("reset Cua"))
     );
-    let mut local = Session::new(driver.clone());
-    local
-        .configure(
-            Desktop::Local,
-            vec![],
-            maka_agent_facts::language::Language::En,
-        )
-        .unwrap();
-    assert_eq!(local.platform(), "linux");
-    local.close().await.unwrap();
+    #[cfg(feature = "native-desktop")]
+    {
+        let mut local = Session::new(driver.clone());
+        local
+            .configure(
+                Desktop::Local,
+                vec![],
+                maka_agent_facts::language::Language::En,
+            )
+            .unwrap();
+        assert_eq!(local.platform(), "linux");
+        local.close().await.unwrap();
+    }
 
     let app = call(
         &mut a,
@@ -245,6 +248,7 @@ async fn closed_or_cancelled_session_cannot_start_native_work() {
     run.close().await.unwrap();
 }
 
+#[cfg(feature = "native-desktop")]
 #[tokio::test]
 #[ignore = "requires an interactive desktop; lists applications without sending input"]
 async fn native_application_observation_and_shutdown() {
@@ -292,7 +296,7 @@ async fn native_application_observation_and_shutdown() {
     assert!(result.is_array());
 }
 
-#[cfg(target_os = "macos")]
+#[cfg(all(feature = "native-desktop", target_os = "macos"))]
 #[tokio::test]
 #[ignore = "requires a locked macOS desktop; verifies refusal without launching or controlling an app"]
 async fn locked_desktop_refuses_target_binding_before_app_resolution() {
diff --git a/platform/computer-use/tests/repl_contract.rs b/platform/computer-use/tests/repl_contract.rs
index c02f520b4a..bb95c66ed2 100644
--- a/platform/computer-use/tests/repl_contract.rs
+++ b/platform/computer-use/tests/repl_contract.rs
@@ -126,6 +126,7 @@ async fn inventory_failures_remain_visible_and_auto_explicit_screenshot_output_i
     repl.close().await.unwrap();
 }
 
+#[cfg(feature = "native-desktop")]
 #[tokio::test]
 async fn browser_discovery_distinguishes_no_configuration_from_an_unavailable_selection() {
     let driver = Arc::new(tokio::sync::Mutex::new(maka_computer_use::Driver::default()));
@@ -170,10 +171,12 @@ async fn browser_discovery_distinguishes_no_configuration_from_an_unavailable_se
 
 // Each evaluation owns its language; the same REPL and native target owner can
 // serve differently captured invocations without changing persistent JS state.
+#[cfg(feature = "native-desktop")]
 struct LocalBridge {
     native: Arc<tokio::sync::Mutex<maka_computer_use::Session>>,
     language: maka_agent_facts::language::Language,
 }
+#[cfg(feature = "native-desktop")]
 impl ToolExecutor for LocalBridge {
     fn names(&self) -> Vec<String> {
         vec!["cua".into()]
@@ -196,6 +199,7 @@ impl ToolExecutor for LocalBridge {
     }
 }
 
+#[cfg(feature = "native-desktop")]
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn persistent_repl_uses_each_evaluations_language_without_rewriting_user_output() {
     use maka_agent_facts::language::Language;
@@ -203,6 +207,15 @@ async fn persistent_repl_uses_each_evaluations_language_without_rewriting_user_o
     let native = Arc::new(tokio::sync::Mutex::new(maka_computer_use::Session::new(
         driver.clone(),
     )));
+    native
+        .lock()
+        .await
+        .configure(
+            maka_capabilities::computer::Desktop::Local,
+            vec![],
+            Language::En,
+        )
+        .unwrap();
     let repl = Repl::new(facade(), CellLimits::default());
     for (language, recovery) in [
         (Language::ZhCn, "重新绑定目标"),
diff --git a/products/maka/cli/Cargo.toml b/products/maka/cli/Cargo.toml
index 3aef448ee6..f03192153e 100644
--- a/products/maka/cli/Cargo.toml
+++ b/products/maka/cli/Cargo.toml
@@ -24,6 +24,10 @@ edition.workspace = true
 license.workspace = true
 publish.workspace = true
 
+[features]
+default = ["native-desktop"]
+native-desktop = ["maka-computer-use/native-desktop"]
+
 [[bin]]
 name = "maka"
 path = "src/main.rs"
diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json
index b1531bc980..c5747c4474 100644
--- a/products/maka/cli/notices.json
+++ b/products/maka/cli/notices.json
@@ -1,7 +1,7 @@
 {
   "inputs": {
     "Cargo.lock": "9d2ee639c0f682b6b5b91e36922fa8a0fdb647eba21f9eafcedc45d201cd96f9",
-    "Cargo.toml": "84dad9aaa98e01f9d194633a830d2c886c1e81de823436efc8057ad10a44f894",
+    "Cargo.toml": "a8de04fd6213b52cf187f9208c0f36bb15d8dab4d8ef8e7c482503d75c9b7e43",
     "package-lock.json": "a5af15aca21fabe0e1bdd48784167df4dc81cee17777d54861dcbd641168a4eb",
     "package.json": "38baf3beeb5876849ca16e31868d59bfaee769f260c7555b4a8a1847b92e28a1",
     "packages/plugin-sdk/src/configurations.ts": "e7bb3e7245adc43f27a3834c2522b2645540ae5a6a1300a706842abd4a4da852",
@@ -14,7 +14,7 @@
     "patches/zod+4.6.5.patch": "5a6cbff1b31ca2a5d2b7c006abb7b1002241dbf4ae42c315c27940201f52b081",
     "platform/background-health/Cargo.toml": "41e89dc7e43adf066306b03b3083cbd4163a5550f39d5be6669463d3c16c53a5",
     "platform/capabilities/Cargo.toml": "b6a0d3d90f3289fcc2f263fcad661df3d76ac2f5c953c07940f2b5c9c8afa5c8",
-    "platform/computer-use/Cargo.toml": "fc8a60fbb29acf1c9391a95dbca312203bf893fa28e873264ccf26226c22241f",
+    "platform/computer-use/Cargo.toml": "b3397d40d450aca20eb2a1ca0612e5eb565980b2b83b5557d20bd14ec9e58db5",
     "platform/computer-use/THIRD_PARTY_NOTICES": "709951ed2ee5c4b9ed933f375a37c5b15ec0efa2dea340dc0f289906d72a7bde",
     "platform/event-log/Cargo.toml": "ee5cf4bf860e3a6584101bf7421742ac490e30bd8e61ff5ddcab72b5efeabfd3",
     "platform/host-js/Cargo.toml": "ad83d0133b55b69298aeb65db408c6de4170bf1d55afb47f93b121992335a3b5",
@@ -393,7 +393,7 @@
     "products/maka/bundle/scripts/build.mjs": "db11d8cd6f29ef7ead087261848568da1ad3889a870a89f23fa969449d769d47",
     "products/maka/bundle/src/composition.ts": "a4ba3e1ede5e2a52d89fa98775e5e3ade4478844da8bf416fac3e2eaf3117e89",
     "products/maka/bundle/src/installation.ts": "f1819851114b4280252afcbd16ef82e08269901faeacc171120c5abff74be72e",
-    "products/maka/cli/Cargo.toml": "2a41d9dd434d08bb62cfe784e32b9e9facd41c58bbe9eb1b809b3fde3a2ed446",
+    "products/maka/cli/Cargo.toml": "4580d872cfcf525a7fa7b753a35f94d17cfe4da06fab7f2ac6fe6d1ef69de93f",
     "products/maka/cli/build.rs": "852960c81f7f6ee03e882eabfa07603bcb8727b98f22a3e6e2552e6e375668cd",
     "products/maka/cli/build/product.rs": "6302eefa7c818dbd73e75f7796d331542315b79032a62727fa43f53413c25766",
     "products/maka/desktop-shell/Cargo.toml": "3b5d668dea0bbdec2925a7449fd7f5a5cd7703cf0783e4cd9a74bd9800c5ac75",
diff --git a/scripts/rust/check-cua-build.mjs b/scripts/rust/check-cua-build.mjs
new file mode 100644
index 0000000000..9861e9e520
--- /dev/null
+++ b/scripts/rust/check-cua-build.mjs
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 assert from 'node:assert/strict';
+import { execFileSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { rustTarget } from './build-cli.mjs';
+
+const root = fileURLToPath(new URL('../../', import.meta.url));
+const target = rustTarget();
+function dependencies(native) {
+  const tree = execFileSync(
+    'cargo',
+    [
+      'tree',
+      '--locked',
+      '-p',
+      'maka-cli',
+      '--target',
+      target,
+      '--edges',
+      'normal,build',
+      '--prefix',
+      'none',
+      '--format',
+      '{p}',
+      ...(native ? [] : ['--no-default-features']),
+    ],
+    { cwd: root, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 },
+  );
+  return new Set(tree.split('\n').map((line) => line.split(' ')[0]));
+}
+
+const bridge = dependencies(false);
+assert(bridge.has('maka-computer-use'), 'bridge build must retain Computer Use');
+for (const name of [
+  'cua-driver-sdk',
+  'platform-linux',
+  'platform-macos',
+  'platform-windows',
+  'x11',
+])
+  assert(!bridge.has(name), `bridge build must not compile native desktop dependency: ${name}`);
+
+const native = dependencies(true);
+assert(native.has('cua-driver-sdk'), 'default CLI build must retain the native SDK');
+const platform = { linux: 'platform-linux', darwin: 'platform-macos', win32: 'platform-windows' }[
+  process.platform
+];
+assert(native.has(platform), `default CLI build must retain ${platform}`);
+if (process.platform === 'linux') assert(native.has('x11'), 'native Linux must retain X11');
+console.log(`Computer Use bridge and native dependency boundaries verified for ${target}`);
diff --git a/scripts/rust/run-cli.mjs b/scripts/rust/run-cli.mjs
new file mode 100644
index 0000000000..90473fc3c9
--- /dev/null
+++ b/scripts/rust/run-cli.mjs
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 { spawn } from 'node:child_process';
+import { release } from 'node:os';
+import { resolve } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+
+const root = fileURLToPath(new URL('../../', import.meta.url));
+
+/** Select development build capabilities before Cargo resolves native dependencies. */
+export async function runCli(
+  argv,
+  {
+    env = process.env,
+    platform = process.platform,
+    osRelease = release(),
+    repositoryRoot = root,
+  } = {},
+) {
+  const mode = env.MAKA_CUA_BUILD_MODE ?? 'auto';
+  if (!['auto', 'native', 'bridge'].includes(mode)) {
+    throw new Error('MAKA_CUA_BUILD_MODE must be auto, native, or bridge');
+  }
+  const bridge =
+    mode === 'bridge' ||
+    (mode === 'auto' && platform === 'linux' && osRelease.toLowerCase().includes('microsoft'));
+  const args = [
+    'run',
+    '--locked',
+    '-p',
+    'maka-cli',
+    ...(bridge ? ['--no-default-features'] : []),
+    '--',
+    ...argv,
+  ];
+  return await new Promise((resolveRun, reject) => {
+    const child = spawn('cargo', args, {
+      cwd: repositoryRoot,
+      env,
+      stdio: 'inherit',
+      windowsHide: true,
+    });
+    child.once('error', reject);
+    child.once('exit', (code, signal) => resolveRun({ code, signal }));
+  });
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
+  try {
+    const { code, signal } = await runCli(process.argv.slice(2));
+    if (signal) process.kill(process.pid, signal);
+    else process.exitCode = code;
+  } catch (error) {
+    console.error(error.message);
+    process.exitCode = 1;
+  }
+}
diff --git a/scripts/rust/run-cli.test.mjs b/scripts/rust/run-cli.test.mjs
new file mode 100644
index 0000000000..c8f56789f9
--- /dev/null
+++ b/scripts/rust/run-cli.test.mjs
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 assert from 'node:assert/strict';
+import childProcess from 'node:child_process';
+import { EventEmitter } from 'node:events';
+import { syncBuiltinESMExports } from 'node:module';
+import test from 'node:test';
+import { fileURLToPath } from 'node:url';
+import { runCli } from './run-cli.mjs';
+
+const launcher = fileURLToPath(new URL('./run-cli.mjs', import.meta.url));
+const root = fileURLToPath(new URL('../../', import.meta.url));
+
+test('development builds select the native capability before forwarding CLI arguments', async (t) => {
+  let invocation;
+  t.mock.method(childProcess, 'spawn', (command, args, options) => {
+    invocation = { command, args, options };
+    const child = new EventEmitter();
+    queueMicrotask(() => child.emit('exit', 0, null));
+    return child;
+  });
+  syncBuiltinESMExports();
+  t.after(() => {
+    t.mock.restoreAll();
+    syncBuiltinESMExports();
+  });
+  const native = ['run', '--locked', '-p', 'maka-cli', '--', 'code', '--log', 'a b.sqlite'];
+  const bridge = [
+    'run',
+    '--locked',
+    '-p',
+    'maka-cli',
+    '--no-default-features',
+    '--',
+    'code',
+    '--log',
+    'a b.sqlite',
+  ];
+  for (const [mode, platform, osRelease, expected] of [
+    [undefined, 'linux', '6.8.0-generic', native],
+    [undefined, 'linux', '6.6.87.2-microsoft-standard-WSL2', bridge],
+    ['auto', 'linux', '4.4.0-Microsoft', bridge],
+    ['auto', 'darwin', 'Microsoft', native],
+    ['auto', 'win32', 'Microsoft', native],
+    ['native', 'linux', '6.6.87.2-microsoft-standard-WSL2', native],
+    ['bridge', 'linux', '6.8.0-generic', bridge],
+  ]) {
+    const env = mode === undefined ? {} : { MAKA_CUA_BUILD_MODE: mode };
+    assert.deepEqual(
+      await runCli(['code', '--log', 'a b.sqlite'], {
+        env,
+        platform,
+        osRelease,
+        repositoryRoot: '/source checkout',
+      }),
+      { code: 0, signal: null },
+    );
+    assert.deepEqual(invocation, {
+      command: 'cargo',
+      args: expected,
+      options: {
+        cwd: '/source checkout',
+        env,
+        stdio: 'inherit',
+        windowsHide: true,
+      },
+    });
+  }
+});
+
+// Run the public Node entrypoint while replacing only its external Cargo process.
+function invokeLauncher(event, env = {}) {
+  const preload = `
+    import childProcess from 'node:child_process';
+    import { EventEmitter } from 'node:events';
+    import { syncBuiltinESMExports } from 'node:module';
+    childProcess.spawn = (command, args, options) => {
+      process.stdout.write(JSON.stringify({ command, args, cwd: options.cwd }) + '\\n');
+      const child = new EventEmitter();
+      queueMicrotask(() => ${event});
+      return child;
+    };
+    syncBuiltinESMExports();
+  `;
+  return childProcess.spawnSync(
+    process.execPath,
+    ['--import', 'data:text/javascript,' + encodeURIComponent(preload), launcher, '--help'],
+    {
+      env: { ...process.env, MAKA_CUA_BUILD_MODE: 'bridge', ...env },
+      encoding: 'utf8',
+      timeout: 10_000,
+      windowsHide: true,
+    },
+  );
+}
+
+test('the launcher preserves Cargo failures and invokes Cargo from the repository', () => {
+  const result = invokeLauncher("child.emit('exit', 29, null)");
+  assert.equal(result.status, 29);
+  assert.equal(result.stderr, '');
+  assert.deepEqual(JSON.parse(result.stdout), {
+    command: 'cargo',
+    args: ['run', '--locked', '-p', 'maka-cli', '--no-default-features', '--', '--help'],
+    cwd: root,
+  });
+});
+
+test('the launcher reports a Cargo spawn error', () => {
+  const result = invokeLauncher("child.emit('error', new Error('Cargo could not start'))");
+  assert.equal(result.status, 1);
+  assert.equal(result.stderr, 'Cargo could not start\n');
+});
+
+test('the launcher rejects unknown build modes before starting Cargo', () => {
+  const result = invokeLauncher("child.emit('exit', 0, null)", { MAKA_CUA_BUILD_MODE: 'typo' });
+  assert.equal(result.status, 1);
+  assert.equal(result.stdout, '');
+  assert.equal(result.stderr, 'MAKA_CUA_BUILD_MODE must be auto, native, or bridge\n');
+});
+
+test('the launcher preserves Cargo termination signals', {
+  skip: process.platform === 'win32',
+}, () => {
+  const result = invokeLauncher("child.emit('exit', null, 'SIGTERM')");
+  assert.equal(result.status, null);
+  assert.equal(result.signal, 'SIGTERM');
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org