winx-code-agent 0.2.357

Remote-first Rust MCP runtime with Streamable HTTP, durable PTYs, and guarded agent tools
Documentation
use crate::server::usage::lifecycle::{phase, Phase};

use rmcp::model::CallToolResult;
use rmcp::ErrorData as McpError;
use serde_json::json;

use super::super::executions::{
    bounded_timeout, history_failure, workflow_envelope, WorkflowContext,
};
use super::super::outcomes::ToolResultStatus;
use super::super::workflow_types::{AgentOperation, AgentRequest, WorkflowAuthority};
use super::super::WinxService;
use crate::state::execution_history::{HistoryError, OutputSource};
use crate::tools::herdr::{HerdrAction, HerdrAdapter, HerdrContext, HerdrError, HerdrRequest};

async fn run_adapter(
    adapter: &HerdrAdapter,
    context: &WorkflowContext,
    request: HerdrRequest,
    authority: &WorkflowAuthority,
) -> Result<serde_json::Value, HerdrError> {
    let prompt_guard = if request.action == HerdrAction::Prompt {
        let guard = context.slot.lock().await;
        let state = guard
            .as_ref()
            .ok_or_else(|| HerdrError::Denied("session is no longer initialized".into()))?;
        if state.current_thread_id != context.scope.thread_id
            || state.workspace_root != context.scope.workspace_root
        {
            return Err(HerdrError::Denied(
                "session thread/workspace binding changed before submission".into(),
            ));
        }
        if !authority.bash_allowed || !matches!(state.mode, crate::types::Modes::Wcgw) {
            return Err(HerdrError::Denied(
                "prompt requires current wcgw mode and BashCommand authority".into(),
            ));
        }
        Some(guard)
    } else {
        None
    };
    let herdr_context = HerdrContext {
        principal_key: authority.principal_key.clone(),
        workspace_root: context.scope.workspace_root.clone(),
        allow_prompt: authority.bash_allowed && context.allow_prompt,
    };
    let result = adapter.run(&herdr_context, request).await;
    drop(prompt_guard);
    result
}

impl WinxService {
    pub(in crate::server) async fn handle_herdr_agent(
        &self,
        context: &WorkflowContext,
        request: AgentRequest,
        authority: &WorkflowAuthority,
    ) -> Result<CallToolResult, McpError> {
        if request.prompt_id.is_some()
            || request.recognizer.is_some()
            || request.cursor.is_some()
            || request.page_bytes.is_some()
            || request.source != OutputSource::Output
        {
            return history_failure("Agent", &HistoryError::InvalidInput(
                "Herdr uses configured target aliases, recent text and lifecycle waits; native prompt receipts, recognizers and output cursors are not supported by this backend".into(),
            ));
        }
        if request.action != AgentOperation::Prompt && request.text.is_some() {
            return history_failure(
                "Agent",
                &HistoryError::InvalidInput("text is only accepted by prompt".into()),
            );
        }
        if request.action != AgentOperation::Wait && request.timeout_seconds.is_some() {
            return history_failure("Agent", &HistoryError::InvalidInput(
                "Herdr timeout_seconds is a lifecycle wait limit; use a separate wait action after prompt".into(),
            ));
        }
        if request.action != AgentOperation::Wait && !request.until.is_empty() {
            return history_failure(
                "Agent",
                &HistoryError::InvalidInput("until is only accepted by wait".into()),
            );
        }
        let timeout = match bounded_timeout(request.timeout_seconds) {
            Ok(timeout) => timeout,
            Err(error) => return history_failure("Agent", &error),
        };
        let adapter = match HerdrAdapter::from_env() {
            Ok(adapter) => adapter,
            Err(error) => return herdr_failure(&error),
        };
        let action = match request.action {
            AgentOperation::List => HerdrAction::List,
            AgentOperation::Get => HerdrAction::Get,
            AgentOperation::Explain => HerdrAction::Explain,
            AgentOperation::Read => HerdrAction::Read,
            AgentOperation::Prompt => HerdrAction::Prompt,
            AgentOperation::Wait => HerdrAction::Wait,
        };
        let herdr_request = HerdrRequest {
            action,
            target: request.target,
            text: request.text,
            lines: request.lines,
            timeout_ms: (request.action == AgentOperation::Wait)
                .then(|| u64::try_from(timeout.as_millis()).unwrap_or(30_000)),
            until: request.until,
        };
        let guard = phase(Phase::HerdrAdapter);
        let result = run_adapter(&adapter, context, herdr_request, authority).await;
        guard.finish(result.is_ok());
        match result {
            Ok(value) => herdr_success(action, &value),
            Err(error) => herdr_failure(&error),
        }
    }
}

fn herdr_success(
    action: HerdrAction,
    value: &serde_json::Value,
) -> Result<CallToolResult, McpError> {
    let mut data = json!({ "backend": "herdr", "result": value });
    let message = if action == HerdrAction::Prompt {
        data["submission_status"] = json!("accepted");
        data["input_sent"] = json!(true);
        data["prompt_correlated"] = json!(false);
        data["automatic_resend_allowed"] = json!(false);
        "Herdr prompt submitted; completion has not been observed. Use a separate wait action to inspect lifecycle state."
    } else {
        "Herdr action completed."
    };
    workflow_envelope("Agent", ToolResultStatus::Completed, message, None, None, data)
}

fn herdr_failure(error: &HerdrError) -> Result<CallToolResult, McpError> {
    let (status, code) = match error {
        HerdrError::Disabled => (ToolResultStatus::Failed, "herdr_disabled"),
        HerdrError::Configuration(_) => (ToolResultStatus::Failed, "herdr_configuration"),
        HerdrError::Denied(_) => (ToolResultStatus::Denied, "herdr_scope_denied"),
        HerdrError::Unavailable(_) => (ToolResultStatus::Failed, "herdr_unavailable"),
        HerdrError::InvalidRequest(_) => (ToolResultStatus::InvalidInput, "invalid_herdr_request"),
        HerdrError::InvalidResponse(_) => (ToolResultStatus::Failed, "invalid_herdr_response"),
        HerdrError::OutputLimit => (ToolResultStatus::Failed, "herdr_output_limit"),
        HerdrError::Timeout { uncertain: true } | HerdrError::DeliveryUncertain(_) => {
            (ToolResultStatus::CompletedWithIssues, "herdr_delivery_uncertain")
        }
        HerdrError::Timeout { uncertain: false } => (ToolResultStatus::Failed, "herdr_timeout"),
        HerdrError::Failed(_) => (ToolResultStatus::Failed, "herdr_failed"),
    };
    workflow_envelope(
        "Agent",
        status,
        &error.to_string(),
        Some(code),
        None,
        json!({
            "backend": "herdr",
            "input_may_have_been_sent": matches!(error, HerdrError::Timeout { uncertain: true } | HerdrError::DeliveryUncertain(_)),
            "automatic_resend_allowed": false,
        }),
    )
}

#[cfg(all(test, unix))]
mod tests {
    use super::*;
    use crate::state::bash_state::BashState;
    use crate::types::Modes;
    use anyhow::{ensure, Context};
    use std::{fs, os::unix::fs::PermissionsExt};

    #[test]
    fn herdr_prompt_success_confirms_submission_only() -> anyhow::Result<()> {
        let result = herdr_success(
            HerdrAction::Prompt,
            &json!({ "agent": { "terminal_id": "terminal-1" } }),
        )?;
        let envelope = result.structured_content.context("envelope")?;
        assert_eq!(envelope["data"]["submission_status"], "accepted");
        assert_eq!(envelope["data"]["input_sent"], true);
        assert_eq!(envelope["data"]["prompt_correlated"], false);
        assert_eq!(envelope["data"]["automatic_resend_allowed"], false);
        assert!(envelope["message"]
            .as_str()
            .is_some_and(|text| text.contains("completion has not been observed")));
        Ok(())
    }

    #[tokio::test]
    async fn stale_herdr_prompt_context_is_denied_before_cli_spawn() -> anyhow::Result<()> {
        let directory = tempfile::tempdir()?;
        let root = directory.path().canonicalize()?;
        let executable = root.join("herdr-fixture");
        let marker = root.join("spawned");
        fs::write(&executable, format!(
            "#!/usr/bin/python3\nfrom pathlib import Path\nPath({}).write_text('spawned')\nprint('{{\"id\":\"x\",\"result\":{{\"type\":\"agent_info\",\"agent\":{{\"terminal_id\":\"terminal-1\"}}}}}}')\n",
            serde_json::to_string(&marker.to_string_lossy())?,
        ))?;
        fs::set_permissions(&executable, fs::Permissions::from_mode(0o700))?;
        let config = root.join("operator.json");
        fs::write(
            &config,
            serde_json::to_vec(&json!({
                "executable": executable, "socket_path": root.join("fake.sock"), "deadline_ms": 1000,
                "bindings": [{ "principal_key": "local", "workspace_root": root,
                    "targets": { "writer": "terminal-1" } }],
            }))?,
        )?;
        fs::set_permissions(&config, fs::Permissions::from_mode(0o600))?;
        let adapter = HerdrAdapter::from_config_path(&config)?;
        let service = WinxService::new();
        for mutation in ["mode", "thread", "workspace", "absent"] {
            let thread = format!("stale_herdr_{}_{:032x}", mutation, rand::random::<u128>());
            let (slot, _session) = service.tool_session_for(&thread).await;
            let mut state = BashState::new();
            state.current_thread_id = thread.clone();
            state.workspace_root = root.clone();
            state.mode = Modes::Wcgw;
            *slot.lock().await = Some(state);
            let context = service.workflow_context(&thread, &root).await?;
            {
                let mut guard = slot.lock().await;
                if mutation == "absent" {
                    *guard = None;
                } else {
                    let state = guard.as_mut().context("state")?;
                    match mutation {
                        "mode" => state.mode = Modes::Architect,
                        "thread" => state.current_thread_id = "replacement-thread".into(),
                        _ => state.workspace_root = root.join("replacement-workspace"),
                    }
                }
            }
            let request = HerdrRequest {
                action: HerdrAction::Prompt,
                target: Some("writer".into()),
                text: Some("must not be sent".into()),
                ..Default::default()
            };
            let result =
                run_adapter(&adapter, &context, request, &WorkflowAuthority::default()).await;
            ensure!(
                matches!(result, Err(HerdrError::Denied(_))),
                "stale {mutation} context must be denied: {result:?}"
            );
            ensure!(!marker.exists(), "stale {mutation} context spawned an external process");
        }
        Ok(())
    }
}