Skip to main content

theway_daemon/executor/
mod.rs

1//! Execution-environment seam: the kernel-side executor implementations and
2//! process primitives.
3//!
4//! - [`local::LocalExecutor`] — reference [`ToolExecutor`] backed by the local
5//!   filesystem (`tokio::fs`) and process table (`tokio::process`).
6//! - [`sandbox::SandboxExecutor`] — stub executor for remote-sandbox execution
7//!   (`ExecutorKind::Sandbox`); every operation fails promptly with
8//!   [`ExecutorError::UnsupportedKind`] until a real backend lands.
9//! - [`file_lock::FileLock`] — cross-process advisory lock for the editing
10//!   tools' read→modify→write cycle (issue #17).
11//!
12//! Both executors are always compiled. The execution environment is selected
13//! at runtime by `[executor] kind = "local" | "sandbox"` in `config.toml`
14//! (issue #123): the TUI passes the choice to `thewayd` as `--executor-kind`,
15//! and the composition root binds it via [`executor_for_kind`]. The legacy
16//! `local` / `sandbox` cargo features remain as compatibility labels and no
17//! longer gate executor code.
18
19use std::sync::Arc;
20
21use theway_core::executor::{ExecutorKind, ToolExecutor};
22
23pub mod file_lock;
24pub mod local;
25pub mod sandbox;
26
27/// Parse an executor-kind string (`"local"` / `"sandbox"`, case-insensitive)
28/// for the `[executor] kind` config value and the `--executor-kind` CLI flag.
29pub fn parse_executor_kind(raw: &str) -> Result<ExecutorKind, String> {
30    match raw.trim().to_ascii_lowercase().as_str() {
31        "local" => Ok(ExecutorKind::Local),
32        "sandbox" => Ok(ExecutorKind::Sandbox),
33        other => Err(format!(
34            "invalid executor kind {other:?}: expected \"local\" or \"sandbox\""
35        )),
36    }
37}
38
39/// The composition-root executor for a runtime-selected execution
40/// environment. `local` roots a [`local::LocalExecutor`] at `cwd`; `sandbox`
41/// returns the [`sandbox::SandboxExecutor`] stub.
42pub fn executor_for_kind(
43    kind: ExecutorKind,
44    cwd: impl Into<std::path::PathBuf>,
45) -> Arc<dyn ToolExecutor> {
46    match kind {
47        ExecutorKind::Local => Arc::new(local::LocalExecutor::with_cwd(cwd)),
48        ExecutorKind::Sandbox => Arc::new(sandbox::SandboxExecutor::new()),
49    }
50}
51
52/// Compatibility composition root: the default local executor for callers
53/// that do not carry a runtime executor choice.
54pub fn default_executor() -> Arc<dyn ToolExecutor> {
55    executor_for_cwd(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")))
56}
57
58/// Build a local executor with an explicit cwd. Kept for callers/tests that
59/// predate the runtime selection; daemon startup uses [`executor_for_kind`].
60pub fn executor_for_cwd(cwd: impl Into<std::path::PathBuf>) -> Arc<dyn ToolExecutor> {
61    executor_for_kind(ExecutorKind::Local, cwd)
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn parse_executor_kind_accepts_both_values_case_insensitively() {
70        assert_eq!(parse_executor_kind("local").unwrap(), ExecutorKind::Local);
71        assert_eq!(
72            parse_executor_kind("Sandbox").unwrap(),
73            ExecutorKind::Sandbox
74        );
75        assert_eq!(
76            parse_executor_kind(" SANDBOX ").unwrap(),
77            ExecutorKind::Sandbox
78        );
79        assert!(parse_executor_kind("docker").is_err());
80        assert!(parse_executor_kind("").is_err());
81    }
82
83    #[tokio::test]
84    async fn executor_for_cwd_roots_local_executor_at_cwd() {
85        let dir = tempfile::tempdir().unwrap();
86        std::fs::write(dir.path().join("probe.txt"), "ok").unwrap();
87        let executor = executor_for_cwd(dir.path());
88        assert_eq!(executor.kind().await, ExecutorKind::Local);
89        assert_eq!(
90            executor
91                .read_file(std::path::Path::new("probe.txt"))
92                .await
93                .unwrap(),
94            "ok"
95        );
96    }
97
98    #[tokio::test]
99    async fn executor_for_kind_returns_the_configured_environment() {
100        let dir = tempfile::tempdir().unwrap();
101        let local = executor_for_kind(ExecutorKind::Local, dir.path());
102        assert_eq!(local.kind().await, ExecutorKind::Local);
103        let sandbox = executor_for_kind(ExecutorKind::Sandbox, dir.path());
104        assert_eq!(sandbox.kind().await, ExecutorKind::Sandbox);
105    }
106}