BREAKING CHANGES: - owlen-core no longer depends on ratatui/crossterm - RemoteMcpClient constructors are now async - MCP path validation is stricter (security hardening) This commit resolves three critical issues identified in project analysis: ## P0-1: Extract TUI dependencies from owlen-core Create owlen-ui-common crate to hold UI-agnostic color and theme abstractions, removing architectural boundary violation. Changes: - Create new owlen-ui-common crate with abstract Color enum - Move theme.rs from owlen-core to owlen-ui-common - Define Color with Rgb and Named variants (no ratatui dependency) - Create color conversion layer in owlen-tui (color_convert.rs) - Update 35+ color usages with conversion wrappers - Remove ratatui/crossterm from owlen-core dependencies Benefits: - owlen-core usable in headless/CLI contexts - Enables future GUI frontends - Reduces binary size for core library consumers ## P0-2: Fix blocking WebSocket connections Convert RemoteMcpClient constructors to async, eliminating runtime blocking that froze TUI for 30+ seconds on slow connections. Changes: - Make new_with_runtime(), new_with_config(), new() async - Remove block_in_place wrappers for I/O operations - Add 30-second connection timeout with tokio::time::timeout - Update 15+ call sites across 10 files to await constructors - Convert 4 test functions to #[tokio::test] Benefits: - TUI remains responsive during WebSocket connections - Proper async I/O follows Rust best practices - No more indefinite hangs ## P1-1: Secure path traversal vulnerabilities Implement comprehensive path validation with 7 defense layers to prevent file access outside workspace boundaries. Changes: - Create validate_safe_path() with multi-layer security: * URL decoding (prevents %2E%2E bypasses) * Absolute path rejection * Null byte protection * Windows-specific checks (UNC/device paths) * Lexical path cleaning (removes .. components) * Symlink resolution via canonicalization * Boundary verification with starts_with check - Update 4 MCP resource functions (get/list/write/delete) - Add 11 comprehensive security tests Benefits: - Blocks URL-encoded, absolute, UNC path attacks - Prevents null byte injection - Stops symlink escape attempts - Cross-platform security (Windows/Linux/macOS) ## Test Results - owlen-core: 109/109 tests pass (100%) - owlen-tui: 52/53 tests pass (98%, 1 pre-existing failure) - owlen-providers: 2/2 tests pass (100%) - Build: cargo build --all succeeds ## Verification - ✓ cargo tree -p owlen-core shows no TUI dependencies - ✓ No block_in_place calls remain in MCP I/O code - ✓ All 11 security tests pass Fixes: #P0-1, #P0-2, #P1-1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
53 lines
2.1 KiB
Rust
53 lines
2.1 KiB
Rust
use owlen_core::McpToolCall;
|
|
use owlen_core::mcp::remote_client::RemoteMcpClient;
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use tempfile::tempdir;
|
|
|
|
#[tokio::test]
|
|
async fn remote_file_server_read_and_list() {
|
|
// Create temporary directory with a file
|
|
let dir = tempdir().expect("tempdir failed");
|
|
let file_path = dir.path().join("hello.txt");
|
|
let mut file = File::create(&file_path).expect("create file");
|
|
writeln!(file, "world").expect("write file");
|
|
|
|
// Change current directory for the test process so the server sees the temp dir as its root
|
|
std::env::set_current_dir(dir.path()).expect("set cwd");
|
|
|
|
// Ensure the MCP server binary is built.
|
|
// Build the MCP server binary using the workspace manifest.
|
|
let manifest_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../..")
|
|
.join("Cargo.toml");
|
|
let build_status = std::process::Command::new("cargo")
|
|
.args(["build", "-p", "owlen-mcp-server", "--manifest-path"])
|
|
.arg(manifest_path)
|
|
.status()
|
|
.expect("failed to run cargo build for MCP server");
|
|
assert!(build_status.success(), "MCP server build failed");
|
|
|
|
// Spawn remote client after the cwd is set and binary built
|
|
let client = RemoteMcpClient::new().await.expect("remote client init");
|
|
|
|
// Read file via MCP
|
|
let call = McpToolCall {
|
|
name: "resources_get".to_string(),
|
|
arguments: serde_json::json!({"path": "hello.txt"}),
|
|
};
|
|
let resp = client.call_tool(call).await.expect("call_tool");
|
|
let content: String = serde_json::from_value(resp.output).expect("parse output");
|
|
assert!(content.trim().ends_with("world"));
|
|
|
|
// List directory via MCP
|
|
let list_call = McpToolCall {
|
|
name: "resources_list".to_string(),
|
|
arguments: serde_json::json!({"path": "."}),
|
|
};
|
|
let list_resp = client.call_tool(list_call).await.expect("list_tool");
|
|
let entries: Vec<String> = serde_json::from_value(list_resp.output).expect("parse list");
|
|
assert!(entries.contains(&"hello.txt".to_string()));
|
|
|
|
// Cleanup handled by tempdir
|
|
}
|