418 lines
13 KiB
Rust
418 lines
13 KiB
Rust
//! Code execution tools using Docker sandboxing
|
|
|
|
use crate::sandbox::Sandbox;
|
|
use async_trait::async_trait;
|
|
use owlen_core::Result;
|
|
use owlen_core::tools::{Tool, ToolResult};
|
|
use serde_json::{Value, json};
|
|
use std::path::PathBuf;
|
|
|
|
/// Tool for compiling projects (Rust, Node.js, Python)
|
|
pub struct CompileProjectTool {
|
|
sandbox: Sandbox,
|
|
}
|
|
|
|
impl Default for CompileProjectTool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl CompileProjectTool {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
sandbox: Sandbox::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Tool for CompileProjectTool {
|
|
fn name(&self) -> &'static str {
|
|
"compile_project"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Compile a project (Rust, Node.js, Python). Detects project type automatically."
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"project_path": {
|
|
"type": "string",
|
|
"description": "Path to the project root"
|
|
},
|
|
"project_type": {
|
|
"type": "string",
|
|
"enum": ["rust", "node", "python"],
|
|
"description": "Project type (auto-detected if not specified)"
|
|
}
|
|
},
|
|
"required": ["project_path"]
|
|
})
|
|
}
|
|
|
|
async fn execute(&self, args: Value) -> Result<ToolResult> {
|
|
let project_path = args
|
|
.get("project_path")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| owlen_core::Error::InvalidInput("Missing project_path".into()))?;
|
|
|
|
let path = PathBuf::from(project_path);
|
|
if !path.exists() {
|
|
return Ok(ToolResult::error("Project path does not exist"));
|
|
}
|
|
|
|
// Detect project type
|
|
let project_type = if let Some(pt) = args.get("project_type").and_then(|v| v.as_str()) {
|
|
pt.to_string()
|
|
} else if path.join("Cargo.toml").exists() {
|
|
"rust".to_string()
|
|
} else if path.join("package.json").exists() {
|
|
"node".to_string()
|
|
} else if path.join("setup.py").exists() || path.join("pyproject.toml").exists() {
|
|
"python".to_string()
|
|
} else {
|
|
return Ok(ToolResult::error("Could not detect project type"));
|
|
};
|
|
|
|
// Execute compilation
|
|
let result = match project_type.as_str() {
|
|
"rust" => self.sandbox.execute_rust(&path, &["cargo", "build"]).await,
|
|
"node" => {
|
|
self.sandbox
|
|
.execute_node(&path, &["npm", "run", "build"])
|
|
.await
|
|
}
|
|
"python" => {
|
|
// Python typically doesn't need compilation, but we can check syntax
|
|
self.sandbox
|
|
.execute_python(&path, &["python", "-m", "compileall", "."])
|
|
.await
|
|
}
|
|
_ => return Ok(ToolResult::error("Unsupported project type")),
|
|
};
|
|
|
|
match result {
|
|
Ok(exec_result) => {
|
|
if exec_result.timed_out {
|
|
Ok(ToolResult::error("Compilation timed out"))
|
|
} else if exec_result.exit_code == 0 {
|
|
Ok(ToolResult::success(json!({
|
|
"success": true,
|
|
"stdout": exec_result.stdout,
|
|
"stderr": exec_result.stderr,
|
|
"project_type": project_type
|
|
})))
|
|
} else {
|
|
Ok(ToolResult::success(json!({
|
|
"success": false,
|
|
"exit_code": exec_result.exit_code,
|
|
"stdout": exec_result.stdout,
|
|
"stderr": exec_result.stderr,
|
|
"project_type": project_type
|
|
})))
|
|
}
|
|
}
|
|
Err(e) => Ok(ToolResult::error(&format!("Compilation failed: {}", e))),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tool for running test suites
|
|
pub struct RunTestsTool {
|
|
sandbox: Sandbox,
|
|
}
|
|
|
|
impl Default for RunTestsTool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl RunTestsTool {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
sandbox: Sandbox::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Tool for RunTestsTool {
|
|
fn name(&self) -> &'static str {
|
|
"run_tests"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Run tests for a project (Rust, Node.js, Python)"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"project_path": {
|
|
"type": "string",
|
|
"description": "Path to the project root"
|
|
},
|
|
"test_filter": {
|
|
"type": "string",
|
|
"description": "Optional test filter/pattern"
|
|
}
|
|
},
|
|
"required": ["project_path"]
|
|
})
|
|
}
|
|
|
|
async fn execute(&self, args: Value) -> Result<ToolResult> {
|
|
let project_path = args
|
|
.get("project_path")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| owlen_core::Error::InvalidInput("Missing project_path".into()))?;
|
|
|
|
let path = PathBuf::from(project_path);
|
|
if !path.exists() {
|
|
return Ok(ToolResult::error("Project path does not exist"));
|
|
}
|
|
|
|
let test_filter = args.get("test_filter").and_then(|v| v.as_str());
|
|
|
|
// Detect project type and run tests
|
|
let result = if path.join("Cargo.toml").exists() {
|
|
let cmd = if let Some(filter) = test_filter {
|
|
vec!["cargo", "test", filter]
|
|
} else {
|
|
vec!["cargo", "test"]
|
|
};
|
|
self.sandbox.execute_rust(&path, &cmd).await
|
|
} else if path.join("package.json").exists() {
|
|
self.sandbox.execute_node(&path, &["npm", "test"]).await
|
|
} else if path.join("pytest.ini").exists()
|
|
|| path.join("setup.py").exists()
|
|
|| path.join("pyproject.toml").exists()
|
|
{
|
|
let cmd = if let Some(filter) = test_filter {
|
|
vec!["pytest", "-k", filter]
|
|
} else {
|
|
vec!["pytest"]
|
|
};
|
|
self.sandbox.execute_python(&path, &cmd).await
|
|
} else {
|
|
return Ok(ToolResult::error("Could not detect test framework"));
|
|
};
|
|
|
|
match result {
|
|
Ok(exec_result) => Ok(ToolResult::success(json!({
|
|
"success": exec_result.exit_code == 0 && !exec_result.timed_out,
|
|
"exit_code": exec_result.exit_code,
|
|
"stdout": exec_result.stdout,
|
|
"stderr": exec_result.stderr,
|
|
"timed_out": exec_result.timed_out
|
|
}))),
|
|
Err(e) => Ok(ToolResult::error(&format!("Tests failed to run: {}", e))),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tool for formatting code
|
|
pub struct FormatCodeTool {
|
|
sandbox: Sandbox,
|
|
}
|
|
|
|
impl Default for FormatCodeTool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl FormatCodeTool {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
sandbox: Sandbox::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Tool for FormatCodeTool {
|
|
fn name(&self) -> &'static str {
|
|
"format_code"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Format code using project-appropriate formatter (rustfmt, prettier, black)"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"project_path": {
|
|
"type": "string",
|
|
"description": "Path to the project root"
|
|
},
|
|
"check_only": {
|
|
"type": "boolean",
|
|
"description": "Only check formatting without modifying files",
|
|
"default": false
|
|
}
|
|
},
|
|
"required": ["project_path"]
|
|
})
|
|
}
|
|
|
|
async fn execute(&self, args: Value) -> Result<ToolResult> {
|
|
let project_path = args
|
|
.get("project_path")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| owlen_core::Error::InvalidInput("Missing project_path".into()))?;
|
|
|
|
let path = PathBuf::from(project_path);
|
|
if !path.exists() {
|
|
return Ok(ToolResult::error("Project path does not exist"));
|
|
}
|
|
|
|
let check_only = args
|
|
.get("check_only")
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false);
|
|
|
|
// Detect project type and run formatter
|
|
let result = if path.join("Cargo.toml").exists() {
|
|
let cmd = if check_only {
|
|
vec!["cargo", "fmt", "--", "--check"]
|
|
} else {
|
|
vec!["cargo", "fmt"]
|
|
};
|
|
self.sandbox.execute_rust(&path, &cmd).await
|
|
} else if path.join("package.json").exists() {
|
|
let cmd = if check_only {
|
|
vec!["npx", "prettier", "--check", "."]
|
|
} else {
|
|
vec!["npx", "prettier", "--write", "."]
|
|
};
|
|
self.sandbox.execute_node(&path, &cmd).await
|
|
} else if path.join("setup.py").exists() || path.join("pyproject.toml").exists() {
|
|
let cmd = if check_only {
|
|
vec!["black", "--check", "."]
|
|
} else {
|
|
vec!["black", "."]
|
|
};
|
|
self.sandbox.execute_python(&path, &cmd).await
|
|
} else {
|
|
return Ok(ToolResult::error("Could not detect project type"));
|
|
};
|
|
|
|
match result {
|
|
Ok(exec_result) => Ok(ToolResult::success(json!({
|
|
"success": exec_result.exit_code == 0,
|
|
"formatted": !check_only && exec_result.exit_code == 0,
|
|
"stdout": exec_result.stdout,
|
|
"stderr": exec_result.stderr
|
|
}))),
|
|
Err(e) => Ok(ToolResult::error(&format!("Formatting failed: {}", e))),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tool for linting code
|
|
pub struct LintCodeTool {
|
|
sandbox: Sandbox,
|
|
}
|
|
|
|
impl Default for LintCodeTool {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl LintCodeTool {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
sandbox: Sandbox::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Tool for LintCodeTool {
|
|
fn name(&self) -> &'static str {
|
|
"lint_code"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Lint code using project-appropriate linter (clippy, eslint, pylint)"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"project_path": {
|
|
"type": "string",
|
|
"description": "Path to the project root"
|
|
},
|
|
"fix": {
|
|
"type": "boolean",
|
|
"description": "Automatically fix issues if possible",
|
|
"default": false
|
|
}
|
|
},
|
|
"required": ["project_path"]
|
|
})
|
|
}
|
|
|
|
async fn execute(&self, args: Value) -> Result<ToolResult> {
|
|
let project_path = args
|
|
.get("project_path")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| owlen_core::Error::InvalidInput("Missing project_path".into()))?;
|
|
|
|
let path = PathBuf::from(project_path);
|
|
if !path.exists() {
|
|
return Ok(ToolResult::error("Project path does not exist"));
|
|
}
|
|
|
|
let fix = args.get("fix").and_then(|v| v.as_bool()).unwrap_or(false);
|
|
|
|
// Detect project type and run linter
|
|
let result = if path.join("Cargo.toml").exists() {
|
|
let cmd = if fix {
|
|
vec!["cargo", "clippy", "--fix", "--allow-dirty"]
|
|
} else {
|
|
vec!["cargo", "clippy"]
|
|
};
|
|
self.sandbox.execute_rust(&path, &cmd).await
|
|
} else if path.join("package.json").exists() {
|
|
let cmd = if fix {
|
|
vec!["npx", "eslint", ".", "--fix"]
|
|
} else {
|
|
vec!["npx", "eslint", "."]
|
|
};
|
|
self.sandbox.execute_node(&path, &cmd).await
|
|
} else if path.join("setup.py").exists() || path.join("pyproject.toml").exists() {
|
|
// pylint doesn't have auto-fix
|
|
self.sandbox.execute_python(&path, &["pylint", "."]).await
|
|
} else {
|
|
return Ok(ToolResult::error("Could not detect project type"));
|
|
};
|
|
|
|
match result {
|
|
Ok(exec_result) => {
|
|
let issues_found = exec_result.exit_code != 0;
|
|
Ok(ToolResult::success(json!({
|
|
"success": true,
|
|
"issues_found": issues_found,
|
|
"exit_code": exec_result.exit_code,
|
|
"stdout": exec_result.stdout,
|
|
"stderr": exec_result.stderr
|
|
})))
|
|
}
|
|
Err(e) => Ok(ToolResult::error(&format!("Linting failed: {}", e))),
|
|
}
|
|
}
|
|
}
|