winx-code-agent 0.2.352

Remote-first Rust MCP runtime with Streamable HTTP, durable PTYs, and guarded agent tools
Documentation
use std::sync::Arc;

use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};

use crate::daemon::{
    default_socket_path, DaemonClient, DaemonProcessRole, DaemonShellRuntime, HelloResult,
    BUILD_IDENTITY_CAPABILITY, TYPED_ACTION_RESULT_CAPABILITY,
};
use crate::errors::{Result, WinxError};

use super::{EmbeddedShellRuntime, ShellRuntime};

const GUARDIAN_LIFECYCLE_CAPABILITY: &str = "guardian_activity_clock";
const PLANNED_CONTROL_RESTART_CAPABILITY: &str = "planned_control_restart";
const SESSION_NEGOTIATE_CAPABILITY: &str = "session.negotiate";
const DAEMON_TRANSITION_TIMEOUT: Duration = Duration::from_secs(3);

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RuntimeMode {
    Daemon,
    Embedded,
}

fn enabled(value: Option<&str>) -> bool {
    value.and_then(crate::config::parse_bool).unwrap_or(false)
}

/// Pure runtime-mode selection, kept separate from process spawning so its
/// default and kill switches are deterministic under test.
pub fn select_runtime_mode(
    embedded: Option<&str>,
    runtime: Option<&str>,
    sandbox: Option<&str>,
) -> Result<RuntimeMode> {
    select_runtime_mode_for_platform(cfg!(any(unix, windows)), embedded, runtime, sandbox)
}

fn select_runtime_mode_for_platform(
    daemon_supported: bool,
    embedded: Option<&str>,
    runtime: Option<&str>,
    sandbox: Option<&str>,
) -> Result<RuntimeMode> {
    if enabled(sandbox) || enabled(embedded) {
        return Ok(RuntimeMode::Embedded);
    }
    match runtime {
        None | Some("" | "daemon") if daemon_supported => Ok(RuntimeMode::Daemon),
        None | Some("" | "embedded") => Ok(RuntimeMode::Embedded),
        Some("daemon") => Err(WinxError::ConfigurationError(
            "WINX_RUNTIME=\"daemon\" requires Unix or Windows; use `embedded` on this OS"
                .to_string(),
        )),
        Some(other) => Err(WinxError::ConfigurationError(format!(
            "invalid WINX_RUNTIME={other:?}; expected `daemon` or `embedded`"
        ))),
    }
}

pub fn configured_runtime_mode() -> Result<RuntimeMode> {
    let embedded = std::env::var("WINX_EMBEDDED").ok();
    let runtime = std::env::var("WINX_RUNTIME").ok();
    let sandbox = std::env::var("WINX_SANDBOX").ok();
    select_runtime_mode(embedded.as_deref(), runtime.as_deref(), sandbox.as_deref())
}

pub async fn configured_shell_runtime() -> Result<Arc<dyn ShellRuntime>> {
    match configured_runtime_mode()? {
        RuntimeMode::Embedded => Ok(Arc::new(EmbeddedShellRuntime)),
        RuntimeMode::Daemon => {
            let socket = default_socket_path();
            let binary = configured_daemon_binary()?;
            ensure_control_daemon_at(&socket, &binary).await?;
            Ok(Arc::new(DaemonShellRuntime::new(socket)))
        }
    }
}

pub fn configured_daemon_binary() -> Result<PathBuf> {
    if let Some(path) = std::env::var_os("WINXD_BIN") {
        return Ok(PathBuf::from(path));
    }
    let executable = std::env::current_exe().map_err(|error| {
        WinxError::ConfigurationError(format!("cannot locate current Winx executable: {error}"))
    })?;
    let sibling = executable.with_file_name(if cfg!(windows) { "winxd.exe" } else { "winxd" });
    if sibling.is_file() {
        Ok(sibling)
    } else {
        Err(WinxError::ConfigurationError(format!(
            "winxd not found beside {} (set WINXD_BIN or WINX_EMBEDDED=1)",
            executable.display()
        )))
    }
}

fn has_capability(hello: &HelloResult, name: &str) -> bool {
    hello.capabilities.iter().any(|capability| capability == name)
}

fn control_is_current(hello: &HelloResult) -> bool {
    hello.process_role == Some(DaemonProcessRole::Control)
        && [
            GUARDIAN_LIFECYCLE_CAPABILITY,
            TYPED_ACTION_RESULT_CAPABILITY,
            SESSION_NEGOTIATE_CAPABILITY,
            BUILD_IDENTITY_CAPABILITY,
        ]
        .into_iter()
        .all(|capability| has_capability(hello, capability))
        && hello.build_matches(&crate::build_info::BuildIdentity::current())
}

/// Ensure the current control-plane feature set is reachable. An older `winxd`
/// that advertises safe planned restarts is replaced in place; per-session
/// guardians keep owning their PTYs throughout the control-plane transition.
pub async fn ensure_control_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<()> {
    ensure_daemon_at(socket, daemon_binary).await?;
    let client = DaemonClient::new(socket);
    let hello = client.hello().await?;
    if control_is_current(&hello) {
        return Ok(());
    }
    if !has_capability(&hello, PLANNED_CONTROL_RESTART_CAPABILITY) {
        return Err(WinxError::ConfigurationError(format!(
            "the running winxd (pid {}) lacks the current guardian lifecycle controls and cannot be safely \
             restarted automatically; stop it manually, then retry",
            hello.daemon_pid
        )));
    }

    tracing::info!(
        pid = hello.daemon_pid,
        "restarting older winxd control plane while preserving session guardians"
    );
    let restarted = restart_control_daemon_from_hello(socket, daemon_binary, hello).await?;
    for capability in [
        GUARDIAN_LIFECYCLE_CAPABILITY,
        TYPED_ACTION_RESULT_CAPABILITY,
        SESSION_NEGOTIATE_CAPABILITY,
        BUILD_IDENTITY_CAPABILITY,
    ] {
        if !has_capability(&restarted, capability) {
            return Err(WinxError::ConfigurationError(format!(
                "restarted winxd at {} still lacks required capability {capability}",
                socket.display()
            )));
        }
    }
    let expected = crate::build_info::BuildIdentity::current();
    if restarted.process_role != Some(DaemonProcessRole::Control)
        || !restarted.build_matches(&expected)
    {
        return Err(WinxError::ConfigurationError(format!(
            "restarted daemon at {} is not the expected winxd build {}",
            socket.display(),
            expected.display
        )));
    }
    Ok(())
}

/// Safely restart only `winxd`; guardian processes and their PTYs remain alive.
pub async fn restart_control_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<HelloResult> {
    let hello = DaemonClient::new(socket).hello().await?;
    restart_control_daemon_from_hello(socket, daemon_binary, hello).await
}

async fn restart_control_daemon_from_hello(
    socket: &Path,
    daemon_binary: &Path,
    previous: HelloResult,
) -> Result<HelloResult> {
    if !has_capability(&previous, PLANNED_CONTROL_RESTART_CAPABILITY) {
        return Err(WinxError::ConfigurationError(format!(
            "winxd pid {} does not advertise safe planned control restarts",
            previous.daemon_pid
        )));
    }
    signal_control_shutdown(socket, previous.daemon_pid).await?;

    let client = DaemonClient::new(socket);
    let deadline = Instant::now() + DAEMON_TRANSITION_TIMEOUT;
    while Instant::now() < deadline {
        match client.hello().await {
            Ok(current) if current.daemon_epoch != previous.daemon_epoch => return Ok(current),
            Ok(_) => tokio::time::sleep(Duration::from_millis(25)).await,
            Err(_) => break,
        }
    }
    if let Ok(current) = client.hello().await {
        if current.daemon_epoch == previous.daemon_epoch {
            return Err(WinxError::ShellInitializationError(format!(
                "winxd pid {} did not stop within {} seconds",
                previous.daemon_pid,
                DAEMON_TRANSITION_TIMEOUT.as_secs()
            )));
        }
        return Ok(current);
    }

    ensure_daemon_at(socket, daemon_binary).await?;
    let restarted = client.hello().await?;
    if restarted.daemon_epoch == previous.daemon_epoch {
        return Err(WinxError::ShellInitializationError(
            "winxd restart returned the previous daemon epoch".to_string(),
        ));
    }
    Ok(restarted)
}

/// Planned control restart: SIGTERM on Unix, the `winx.shutdown` RPC on
/// Windows. Either way guardians are separate detached processes and keep
/// their PTYs.
#[cfg_attr(unix, allow(clippy::unused_async))] // only the Windows branch awaits
async fn signal_control_shutdown(socket: &Path, pid: u32) -> Result<()> {
    #[cfg(unix)]
    {
        let _ = socket;
        crate::os::unix::signal_process(pid, libc::SIGTERM).map_err(Into::into)
    }
    #[cfg(not(unix))]
    {
        let _ = pid;
        DaemonClient::new(socket).shutdown().await
    }
}

/// Start a daemon process that outlives this one: a new session on Unix, a
/// detached process outside our console and job on Windows. Windows job
/// objects may forbid breakaway (MCP clients often wrap servers in one), so a
/// refused breakaway retries without it and the daemon then lives as long as
/// that job does.
fn spawn_detached(daemon_binary: &Path, socket: &Path) -> Result<std::process::Child> {
    let build = |breakaway: bool| {
        let mut command = Command::new(daemon_binary);
        command
            .arg("--socket")
            .arg(socket)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null());
        #[cfg(unix)]
        {
            let _ = breakaway;
            crate::os::unix::configure_detached(&mut command);
        }
        #[cfg(windows)]
        crate::os::windows::configure_detached(&mut command, breakaway);
        command
    };
    let failure = |error: std::io::Error| {
        WinxError::ShellInitializationError(format!(
            "failed to auto-start {}: {error}",
            daemon_binary.display()
        ))
    };
    match build(true).spawn() {
        Ok(child) => Ok(child),
        Err(error) if cfg!(windows) => {
            tracing::debug!(%error, "detached spawn with job breakaway refused; retrying inside the job");
            build(false).spawn().map_err(failure)
        }
        Err(error) => Err(failure(error)),
    }
}

/// Ensure a compatible daemon is reachable, starting `daemon_binary` only when
/// the socket cannot be reached. A reachable incompatible daemon is never
/// replaced or killed.
pub async fn ensure_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<()> {
    let client = DaemonClient::new(socket);
    match client.hello().await {
        Ok(_) => return Ok(()),
        Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
        Err(_) => {}
    }

    let mut child = spawn_detached(daemon_binary, socket)?;

    // Several adapters starting at once each spawn a daemon; the kernel lets
    // exactly one claim the socket and the others exit at once. An early
    // child exit is therefore not a failure yet: keep asking the socket until
    // the deadline, because the winner may belong to a sibling adapter.
    let deadline = Instant::now() + DAEMON_TRANSITION_TIMEOUT;
    let mut child_exit = None;
    while Instant::now() < deadline {
        match client.hello().await {
            Ok(_) => {
                if child_exit.is_none() {
                    std::thread::spawn(move || {
                        let _ = child.wait();
                    });
                }
                return Ok(());
            }
            Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
            Err(_) => {}
        }
        if child_exit.is_none() {
            child_exit = child.try_wait()?;
        }
        tokio::time::sleep(Duration::from_millis(25)).await;
    }
    Err(WinxError::ShellInitializationError(match child_exit {
        Some(status) => format!(
            "winxd exited ({status}) and nothing answered at {} within {} seconds",
            socket.display(),
            DAEMON_TRANSITION_TIMEOUT.as_secs()
        ),
        None => format!("timed out waiting for winxd at {}", socket.display()),
    }))
}

#[cfg(test)]
mod tests {
    use super::{select_runtime_mode_for_platform, RuntimeMode};

    #[test]
    fn platform_without_unix_sockets_defaults_to_embedded() {
        assert!(matches!(
            select_runtime_mode_for_platform(false, None, None, None),
            Ok(RuntimeMode::Embedded)
        ));
        assert!(select_runtime_mode_for_platform(false, None, Some("daemon"), None).is_err());
    }

    #[test]
    fn protocol_1_4_control_without_session_negotiate_requires_upgrade() {
        let hello = crate::daemon::HelloResult {
            protocol_major: 1,
            protocol_minor: 4,
            capabilities: vec![
                super::GUARDIAN_LIFECYCLE_CAPABILITY.to_string(),
                super::TYPED_ACTION_RESULT_CAPABILITY.to_string(),
                super::PLANNED_CONTROL_RESTART_CAPABILITY.to_string(),
            ],
            max_frame_bytes: 1024,
            daemon_epoch: "control-14".to_string(),
            daemon_pid: 14,
            process_role: None,
            build: None,
        };
        assert!(!super::control_is_current(&hello));
        assert!(super::has_capability(&hello, super::PLANNED_CONTROL_RESTART_CAPABILITY));
    }

    #[test]
    fn current_control_requires_the_exact_role_and_build() {
        let capabilities = vec![
            super::GUARDIAN_LIFECYCLE_CAPABILITY.to_string(),
            super::TYPED_ACTION_RESULT_CAPABILITY.to_string(),
            super::SESSION_NEGOTIATE_CAPABILITY.to_string(),
            super::BUILD_IDENTITY_CAPABILITY.to_string(),
        ];
        let hello = crate::daemon::HelloResult::current(
            crate::daemon::DaemonProcessRole::Control,
            capabilities,
            "current",
        );
        assert!(super::control_is_current(&hello));

        let mut wrong_role = hello.clone();
        wrong_role.process_role = Some(crate::daemon::DaemonProcessRole::Guardian);
        assert!(!super::control_is_current(&wrong_role));

        let mut wrong_build = hello;
        if let Some(build) = wrong_build.build.as_mut() {
            build.display.push_str("-different");
        }
        assert!(!super::control_is_current(&wrong_build));
    }
}