Files
owlen/crates/owlen-core/tests/file_server.rs
vikingowl 0728262a9e fix(core,mcp,security)!: resolve critical P0/P1 issues
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>
2025-10-29 12:31:20 +01:00

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
}