Skip to main content

zeroclaw_config/platform/
native.rs

1use std::path::{Path, PathBuf};
2use zeroclaw_api::runtime_traits::RuntimeAdapter;
3
4/// Native runtime — full access, runs on Mac/Linux/Windows/Docker/Raspberry Pi
5pub struct NativeRuntime;
6
7impl Default for NativeRuntime {
8    fn default() -> Self {
9        Self::new()
10    }
11}
12
13impl NativeRuntime {
14    pub fn new() -> Self {
15        Self
16    }
17}
18
19impl RuntimeAdapter for NativeRuntime {
20    fn name(&self) -> &str {
21        "native"
22    }
23
24    fn has_shell_access(&self) -> bool {
25        true
26    }
27
28    fn has_filesystem_access(&self) -> bool {
29        true
30    }
31
32    fn storage_path(&self) -> PathBuf {
33        directories::UserDirs::new().map_or_else(
34            || PathBuf::from(".zeroclaw"),
35            |u| u.home_dir().join(".zeroclaw"),
36        )
37    }
38
39    fn supports_long_running(&self) -> bool {
40        true
41    }
42
43    fn build_shell_command(
44        &self,
45        command: &str,
46        workspace_dir: &Path,
47    ) -> anyhow::Result<tokio::process::Command> {
48        #[cfg(not(target_os = "windows"))]
49        {
50            let mut process = tokio::process::Command::new("sh");
51            process.arg("-c").arg(command).current_dir(workspace_dir);
52            Ok(process)
53        }
54
55        #[cfg(target_os = "windows")]
56        {
57            const CREATE_NO_WINDOW: u32 = 0x08000000;
58
59            let mut process = tokio::process::Command::new("cmd.exe");
60            // Use raw_arg so the command string is passed verbatim to cmd.exe,
61            // bypassing Rust's CommandLineToArgvW escaping which would mangle
62            // embedded double quotes with backslash escapes that cmd doesn't
63            // understand (see #7083).
64            process
65                .raw_arg("/C")
66                .raw_arg(format!("\"{command}\""))
67                .current_dir(workspace_dir)
68                .creation_flags(CREATE_NO_WINDOW);
69            Ok(process)
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn native_name() {
80        assert_eq!(NativeRuntime::new().name(), "native");
81    }
82
83    #[test]
84    fn native_has_shell_access() {
85        assert!(NativeRuntime::new().has_shell_access());
86    }
87
88    #[test]
89    fn native_has_filesystem_access() {
90        assert!(NativeRuntime::new().has_filesystem_access());
91    }
92
93    #[test]
94    fn native_supports_long_running() {
95        assert!(NativeRuntime::new().supports_long_running());
96    }
97
98    #[test]
99    fn native_memory_budget_unlimited() {
100        assert_eq!(NativeRuntime::new().memory_budget(), 0);
101    }
102
103    #[test]
104    fn native_storage_path_contains_zeroclaw() {
105        let path = NativeRuntime::new().storage_path();
106        assert!(path.to_string_lossy().contains("zeroclaw"));
107    }
108
109    #[test]
110    fn native_builds_shell_command() {
111        let cwd = std::env::temp_dir();
112        let command = NativeRuntime::new()
113            .build_shell_command("echo hello", &cwd)
114            .unwrap();
115        let debug = format!("{command:?}");
116        assert!(debug.contains("echo hello"));
117    }
118
119    /// On Windows, `std::process::Command` applies `CommandLineToArgvW`
120    /// escaping to each `.arg()`, which mangles embedded double quotes
121    /// with backslash escapes that `cmd.exe` does not understand.
122    /// `raw_arg` must pass the command verbatim so that quoted paths
123    /// and arguments survive intact (see #7083).
124    #[test]
125    fn shell_command_preserves_double_quotes() {
126        let cwd = std::env::temp_dir();
127        let command = NativeRuntime::new()
128            .build_shell_command(r#"dir "C:\Users\test\Desktop" /b"#, &cwd)
129            .unwrap();
130        let debug = format!("{command:?}");
131
132        // The command string must contain the core command text.
133        assert!(
134            debug.contains("dir"),
135            "debug output must contain the command, got: {debug}"
136        );
137        assert!(
138            debug.contains("Desktop"),
139            "debug output must contain the path, got: {debug}"
140        );
141
142        // On Windows, raw_arg must NOT produce backslash-escaped quotes
143        // (the core issue in #7083).
144        #[cfg(target_os = "windows")]
145        {
146            assert!(
147                debug.contains(r#""C:\Users\test\Desktop""#),
148                "Windows: double-quoted path must appear verbatim, got: {debug}"
149            );
150            assert!(
151                !debug.contains(r#"\\\""#) && !debug.contains(r#"\""#),
152                "Windows: must not contain backslash-escaped quotes, got: {debug}"
153            );
154        }
155    }
156
157    /// A command with mixed quoted and unquoted segments must pass
158    /// through without mangling any part of the command line.
159    #[test]
160    fn shell_command_preserves_mixed_quoted_unquoted() {
161        let cwd = std::env::temp_dir();
162        let command = NativeRuntime::new()
163            .build_shell_command(
164                r#"dir "C:\path with spaces" /b 2>nul || echo "directory missing""#,
165                &cwd,
166            )
167            .unwrap();
168        let debug = format!("{command:?}");
169
170        // The core command text and operators must be present.
171        assert!(debug.contains("dir"), "missing dir command, got: {debug}");
172        assert!(
173            debug.contains("path with spaces"),
174            "missing path, got: {debug}"
175        );
176        assert!(
177            debug.contains("2>nul"),
178            "redirect operator must be present, got: {debug}"
179        );
180        assert!(
181            debug.contains("||"),
182            "pipe operator must be present, got: {debug}"
183        );
184        assert!(
185            debug.contains("directory missing"),
186            "missing echo message, got: {debug}"
187        );
188
189        // On Windows, raw_arg must preserve quotes verbatim.
190        #[cfg(target_os = "windows")]
191        {
192            assert!(
193                debug.contains(r#""C:\path with spaces""#),
194                "Windows: quoted path must appear verbatim, got: {debug}"
195            );
196            assert!(
197                debug.contains(r#""directory missing""#),
198                "Windows: quoted echo message must appear verbatim, got: {debug}"
199            );
200        }
201    }
202
203    /// On Windows, actually invoke `cmd /C` with a quoted `echo`
204    /// argument to confirm the fix works end-to-end. Skipped on
205    /// non-Windows hosts since there's no `cmd.exe`.
206    #[tokio::test]
207    #[cfg(target_os = "windows")]
208    async fn windows_echo_quoted_argument_succeeds() {
209        let cwd = std::env::temp_dir();
210        let output = NativeRuntime::new()
211            .build_shell_command(r#"echo "hello world""#, &cwd)
212            .unwrap()
213            .output()
214            .await
215            .expect("cmd /C echo should execute");
216
217        assert!(output.status.success(), "cmd must exit 0");
218        let stdout = String::from_utf8_lossy(&output.stdout);
219        assert!(
220            stdout.contains("hello world"),
221            "quoted echo output mismatch, got: {stdout}"
222        );
223    }
224
225    /// On Windows, verify `dir` with a quoted path works (previous
226    /// behavior: "The filename, directory name, or volume label
227    /// syntax is incorrect").
228    #[tokio::test]
229    #[cfg(target_os = "windows")]
230    async fn windows_dir_quoted_path_succeeds() {
231        let cwd = std::env::temp_dir();
232        let output = NativeRuntime::new()
233            .build_shell_command(r#"dir "C:\Windows" /b"#, &cwd)
234            .unwrap()
235            .output()
236            .await
237            .expect("cmd /C dir should execute");
238
239        assert!(output.status.success(), "cmd must exit 0");
240        let stdout = String::from_utf8_lossy(&output.stdout);
241        assert!(
242            stdout.contains("explorer.exe") || stdout.contains("System32"),
243            "dir should list C:\\Windows contents, got: {stdout}"
244        );
245    }
246
247    /// Verify a command with entirely unquoted arguments still works
248    /// (regression check for the raw_arg conversion).
249    #[test]
250    fn shell_command_no_quotes_still_works() {
251        let cwd = std::env::temp_dir();
252        let command = NativeRuntime::new()
253            .build_shell_command("echo hello_world", &cwd)
254            .unwrap();
255        let debug = format!("{command:?}");
256        assert!(debug.contains("echo hello_world"));
257    }
258
259    /// Verify `echo %VAR%` expansion syntax is preserved verbatim
260    /// and not mangled by escaping.
261    #[tokio::test]
262    #[cfg(target_os = "windows")]
263    async fn windows_echo_percent_expansion_preserved() {
264        let cwd = std::env::temp_dir();
265        let output = NativeRuntime::new()
266            .build_shell_command("echo %USERPROFILE%", &cwd)
267            .unwrap()
268            .output()
269            .await
270            .expect("cmd /C echo should execute");
271
272        assert!(output.status.success(), "cmd must exit 0");
273        let stdout = String::from_utf8_lossy(&output.stdout);
274        assert!(
275            stdout.contains(":\\"),
276            "%%USERPROFILE%% should expand to a path, got: {stdout}"
277        );
278    }
279}