weft-core 0.1.1

OpenAI-compatible AI agent runtime with WASM capability plugin system
Documentation
use serde_json::Value;
use std::path::{Component, Path, PathBuf};

pub async fn handle_core_capability(
    capability: &str,
    action: &str,
    data: &Value,
    workspace_root: Option<&Path>,
) -> Result<Value, String> {
    match capability {
        "core.files" => handle_files(action, data, workspace_root).await,
        "core.execution" => handle_execution(action, data).await,
        _ => Err(format!("Unknown core capability: {}", capability)),
    }
}

fn normalize_under_root(root: &Path, path_str: &str) -> Result<PathBuf, String> {
    if path_str.contains("..") {
        return Err("Path traversal is not allowed outside the configured workspace".into());
    }

    let candidate = if Path::new(path_str).is_absolute() {
        PathBuf::from(path_str)
    } else {
        root.join(path_str)
    };

    let mut normalized = PathBuf::new();
    for component in candidate.components() {
        match component {
            Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
            Component::RootDir => normalized.push(component.as_os_str()),
            Component::CurDir => {}
            Component::Normal(part) => normalized.push(part),
            Component::ParentDir => {
                if !normalized.pop() {
                    return Err(
                        "Path traversal is not allowed outside the configured workspace".into(),
                    );
                }
            }
        }
    }

    let root_canon = std::fs::canonicalize(root).map_err(|e| {
        format!(
            "Failed to resolve workspace root '{}': {}",
            root.display(),
            e
        )
    })?;

    if normalized.exists() {
        let candidate_canon = std::fs::canonicalize(&normalized)
            .map_err(|e| format!("Failed to resolve path '{}': {}", normalized.display(), e))?;
        if !candidate_canon.starts_with(&root_canon) {
            return Err("Path is outside the configured workspace".into());
        }
        Ok(candidate_canon)
    } else {
        let parent = normalized.parent().unwrap_or(root);
        let parent_canon = std::fs::canonicalize(parent).map_err(|e| {
            format!(
                "Failed to resolve parent path '{}': {}",
                parent.display(),
                e
            )
        })?;
        if !parent_canon.starts_with(&root_canon) {
            return Err("Path is outside the configured workspace".into());
        }
        Ok(normalized)
    }
}

fn resolve_file_path(workspace_root: Option<&Path>, path_str: &str) -> Result<PathBuf, String> {
    match workspace_root {
        Some(root) => normalize_under_root(root, path_str),
        None => Ok(PathBuf::from(path_str)),
    }
}

async fn handle_files(
    action: &str,
    data: &Value,
    workspace_root: Option<&Path>,
) -> Result<Value, String> {
    match action {
        "list" => {
            let path_str = data["path"].as_str().unwrap_or(".");
            let path_buf = resolve_file_path(workspace_root, path_str)?;
            let path = path_buf.as_path();
            if !path.exists() {
                return Err(format!("Path does not exist: {}", path_str));
            }
            let entries: Vec<Value> = match std::fs::read_dir(path) {
                Ok(reader) => reader
                    .filter_map(|e| e.ok())
                    .map(|entry| {
                        let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
                        serde_json::json!({
                            "name": entry.file_name().to_string_lossy(),
                            "is_dir": is_dir,
                        })
                    })
                    .collect(),
                Err(e) => return Err(format!("Failed to list directory: {}", e)),
            };
            Ok(serde_json::json!({ "entries": entries }))
        }
        "read" => {
            let path_str = data["path"]
                .as_str()
                .ok_or_else(|| "Missing 'path' in data".to_string())?;
            let path_buf = resolve_file_path(workspace_root, path_str)?;
            match std::fs::read_to_string(&path_buf) {
                Ok(content) => Ok(serde_json::json!({ "content": content })),
                Err(e) => Err(format!("Failed to read file: {}", e)),
            }
        }
        "write" => {
            let path_str = data["path"]
                .as_str()
                .ok_or_else(|| "Missing 'path' in data".to_string())?;
            let content = data["content"]
                .as_str()
                .ok_or_else(|| "Missing 'content' in data".to_string())?;
            let path_buf = resolve_file_path(workspace_root, path_str)?;
            if let Some(parent) = path_buf.parent() {
                std::fs::create_dir_all(parent)
                    .map_err(|e| format!("Failed to create parent directory: {}", e))?;
            }
            match std::fs::write(&path_buf, content) {
                Ok(()) => Ok(serde_json::json!({ "written": true, "path": path_str })),
                Err(e) => Err(format!("Failed to write file: {}", e)),
            }
        }
        "delete" => {
            let path_str = data["path"]
                .as_str()
                .ok_or_else(|| "Missing 'path' in data".to_string())?;
            let path_buf = resolve_file_path(workspace_root, path_str)?;
            let path = path_buf.as_path();
            let result = if path.is_dir() {
                std::fs::remove_dir_all(path)
            } else {
                std::fs::remove_file(path)
            };
            match result {
                Ok(()) => Ok(serde_json::json!({ "deleted": true, "path": path_str })),
                Err(e) => Err(format!("Failed to delete: {}", e)),
            }
        }
        "metadata" => {
            let path_str = data["path"]
                .as_str()
                .ok_or_else(|| "Missing 'path' in data".to_string())?;
            let path_buf = resolve_file_path(workspace_root, path_str)?;
            match std::fs::metadata(&path_buf) {
                Ok(meta) => Ok(serde_json::json!({
                    "path": path_str,
                    "is_file": meta.is_file(),
                    "is_dir": meta.is_dir(),
                    "len": meta.len(),
                    "readonly": meta.permissions().readonly(),
                })),
                Err(e) => Err(format!("Failed to read metadata: {}", e)),
            }
        }
        _ => Err(format!("Unknown files action: {}", action)),
    }
}

async fn handle_execution(action: &str, data: &Value) -> Result<Value, String> {
    match action {
        "describe" => Ok(serde_json::json!({
            "capability": "core.execution",
            "actions": ["describe", "health", "run"],
            "runtime": "core",
        })),
        "health" => Ok(serde_json::json!({
            "capability": "core.execution",
            "healthy": true,
            "runtime": "core",
        })),
        "run" => {
            let mode = data["mode"]
                .as_str()
                .ok_or_else(|| "core.execution/run only supports mode=dry_run".to_string())?;
            if mode != "dry_run" {
                return Err("core.execution/run only supports mode=dry_run".into());
            }

            let command = data["command"]
                .as_str()
                .ok_or_else(|| "Missing 'command' in data".to_string())?;
            if command != "weft-core-version" {
                return Err(format!(
                    "core.execution/run dry_run does not support command: {}",
                    command
                ));
            }

            Ok(serde_json::json!({
                "mode": "dry_run",
                "command": command,
                "dry_run": true,
                "would_execute": false,
                "exit_code": 0,
                "stdout": "weft-core-version",
                "stderr": "",
            }))
        }
        _ => Err(format!("Unknown execution action: {}", action)),
    }
}

#[cfg(test)]
mod tests {
    use super::handle_core_capability;
    use serde_json::json;

    #[tokio::test]
    async fn execution_describe_and_health_are_available() {
        let describe = handle_core_capability("core.execution", "describe", &json!({}), None)
            .await
            .expect("describe should succeed");
        assert_eq!(describe["capability"], "core.execution");
        assert_eq!(describe["runtime"], "core");

        let health = handle_core_capability("core.execution", "health", &json!({}), None)
            .await
            .expect("health should succeed");
        assert_eq!(health["healthy"], true);
    }

    #[tokio::test]
    async fn execution_run_dry_run_weft_core_version_is_deterministic() {
        let result = handle_core_capability(
            "core.execution",
            "run",
            &json!({"mode":"dry_run","command":"weft-core-version"}),
            None,
        )
        .await
        .expect("dry-run weft-core-version should succeed");

        assert_eq!(result["mode"], "dry_run");
        assert_eq!(result["command"], "weft-core-version");
        assert_eq!(result["dry_run"], true);
        assert_eq!(result["would_execute"], false);
        assert_eq!(result["exit_code"], 0);
        assert_eq!(result["stdout"], "weft-core-version");
        assert_eq!(result["stderr"], "");
    }

    #[tokio::test]
    async fn execution_run_rejects_arbitrary_command_without_dry_run_mode() {
        let error =
            handle_core_capability("core.execution", "run", &json!({"command":"echo hi"}), None)
                .await
                .expect_err("run without dry_run mode should be rejected");

        assert!(error.contains("core.execution/run only supports mode=dry_run"));
    }

    #[tokio::test]
    async fn execution_run_rejects_dry_run_unknown_command() {
        let error = handle_core_capability(
            "core.execution",
            "run",
            &json!({"mode":"dry_run","command":"echo hi"}),
            None,
        )
        .await
        .expect_err("unknown dry-run command should be rejected");

        assert!(error.contains("dry_run does not support command: echo hi"));
    }

    #[tokio::test]
    async fn execution_run_real_process_path_is_not_reachable_by_default() {
        let error = handle_core_capability(
            "core.execution",
            "run",
            &json!({"mode":"execute","command":"weft-core-version"}),
            None,
        )
        .await
        .expect_err("non-dry-run execution should be rejected before process spawn");

        assert!(error.contains("core.execution/run only supports mode=dry_run"));
    }
}