mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-29 17:09:31 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
//! Job management tools.
|
||||
//!
|
||||
//! These tools allow the LLM to manage jobs:
|
||||
//! - Create new jobs/tasks
|
||||
//! - List existing jobs
|
||||
//! - Check job status
|
||||
//! - Cancel running jobs
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{ContextManager, JobContext, JobState};
|
||||
use crate::tools::tool::{Tool, ToolError, ToolOutput};
|
||||
|
||||
/// Tool for creating a new job.
|
||||
pub struct CreateJobTool {
|
||||
context_manager: Arc<ContextManager>,
|
||||
}
|
||||
|
||||
impl CreateJobTool {
|
||||
pub fn new(context_manager: Arc<ContextManager>) -> Self {
|
||||
Self { context_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for CreateJobTool {
|
||||
fn name(&self) -> &str {
|
||||
"create_job"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Create a new job or task for the agent to work on. Use this when the user wants \
|
||||
you to do something substantial that should be tracked as a separate job."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A short title for the job (max 100 chars)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Full description of what needs to be done"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let title = params
|
||||
.get("title")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| ToolError::InvalidParameters("missing 'title' parameter".into()))?;
|
||||
|
||||
let description = params
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| {
|
||||
ToolError::InvalidParameters("missing 'description' parameter".into())
|
||||
})?;
|
||||
|
||||
match self.context_manager.create_job(title, description).await {
|
||||
Ok(job_id) => {
|
||||
let result = serde_json::json!({
|
||||
"job_id": job_id.to_string(),
|
||||
"title": title,
|
||||
"status": "pending",
|
||||
"message": format!("Created job '{}'", title)
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
Err(e) => {
|
||||
let result = serde_json::json!({
|
||||
"error": e.to_string()
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool for listing jobs.
|
||||
pub struct ListJobsTool {
|
||||
context_manager: Arc<ContextManager>,
|
||||
}
|
||||
|
||||
impl ListJobsTool {
|
||||
pub fn new(context_manager: Arc<ContextManager>) -> Self {
|
||||
Self { context_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for ListJobsTool {
|
||||
fn name(&self) -> &str {
|
||||
"list_jobs"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"List all jobs or filter by status. Shows job IDs, titles, and current status."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"type": "string",
|
||||
"description": "Filter by status: 'active', 'completed', 'failed', 'all' (default: 'all')",
|
||||
"enum": ["active", "completed", "failed", "all"]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let filter = params
|
||||
.get("filter")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("all");
|
||||
|
||||
let job_ids = match filter {
|
||||
"active" => self.context_manager.active_jobs().await,
|
||||
_ => self.context_manager.all_jobs().await,
|
||||
};
|
||||
|
||||
let mut jobs = Vec::new();
|
||||
for job_id in job_ids {
|
||||
if let Ok(ctx) = self.context_manager.get_context(job_id).await {
|
||||
let include = match filter {
|
||||
"completed" => ctx.state == JobState::Completed,
|
||||
"failed" => ctx.state == JobState::Failed,
|
||||
"active" => ctx.state.is_active(),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if include {
|
||||
jobs.push(serde_json::json!({
|
||||
"job_id": job_id.to_string(),
|
||||
"title": ctx.title,
|
||||
"status": format!("{:?}", ctx.state),
|
||||
"created_at": ctx.created_at.to_rfc3339()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let summary = self.context_manager.summary().await;
|
||||
|
||||
let result = serde_json::json!({
|
||||
"jobs": jobs,
|
||||
"summary": {
|
||||
"total": summary.total,
|
||||
"pending": summary.pending,
|
||||
"in_progress": summary.in_progress,
|
||||
"completed": summary.completed,
|
||||
"failed": summary.failed
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool for checking job status.
|
||||
pub struct JobStatusTool {
|
||||
context_manager: Arc<ContextManager>,
|
||||
}
|
||||
|
||||
impl JobStatusTool {
|
||||
pub fn new(context_manager: Arc<ContextManager>) -> Self {
|
||||
Self { context_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for JobStatusTool {
|
||||
fn name(&self) -> &str {
|
||||
"job_status"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Check the status and details of a specific job by its ID."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the job to check"
|
||||
}
|
||||
},
|
||||
"required": ["job_id"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let job_id_str = params
|
||||
.get("job_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| ToolError::InvalidParameters("missing 'job_id' parameter".into()))?;
|
||||
|
||||
let job_id = Uuid::parse_str(job_id_str).map_err(|_| {
|
||||
ToolError::InvalidParameters(format!("invalid job ID format: {}", job_id_str))
|
||||
})?;
|
||||
|
||||
match self.context_manager.get_context(job_id).await {
|
||||
Ok(ctx) => {
|
||||
let result = serde_json::json!({
|
||||
"job_id": job_id.to_string(),
|
||||
"title": ctx.title,
|
||||
"description": ctx.description,
|
||||
"status": format!("{:?}", ctx.state),
|
||||
"created_at": ctx.created_at.to_rfc3339(),
|
||||
"started_at": ctx.started_at.map(|t| t.to_rfc3339()),
|
||||
"completed_at": ctx.completed_at.map(|t| t.to_rfc3339()),
|
||||
"actual_cost": ctx.actual_cost.to_string()
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
Err(e) => {
|
||||
let result = serde_json::json!({
|
||||
"error": format!("Job not found: {}", e)
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool for canceling a job.
|
||||
pub struct CancelJobTool {
|
||||
context_manager: Arc<ContextManager>,
|
||||
}
|
||||
|
||||
impl CancelJobTool {
|
||||
pub fn new(context_manager: Arc<ContextManager>) -> Self {
|
||||
Self { context_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for CancelJobTool {
|
||||
fn name(&self) -> &str {
|
||||
"cancel_job"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Cancel a running or pending job. The job will be marked as cancelled and stopped."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the job to cancel"
|
||||
}
|
||||
},
|
||||
"required": ["job_id"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let job_id_str = params
|
||||
.get("job_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| ToolError::InvalidParameters("missing 'job_id' parameter".into()))?;
|
||||
|
||||
let job_id = Uuid::parse_str(job_id_str).map_err(|_| {
|
||||
ToolError::InvalidParameters(format!("invalid job ID format: {}", job_id_str))
|
||||
})?;
|
||||
|
||||
// Transition to cancelled state
|
||||
match self
|
||||
.context_manager
|
||||
.update_context(job_id, |ctx| {
|
||||
ctx.transition_to(JobState::Cancelled, Some("Cancelled by user".to_string()))
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {
|
||||
let result = serde_json::json!({
|
||||
"job_id": job_id.to_string(),
|
||||
"status": "cancelled",
|
||||
"message": "Job cancelled successfully"
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
Ok(Err(reason)) => {
|
||||
let result = serde_json::json!({
|
||||
"error": format!("Cannot cancel job: {}", reason)
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
Err(e) => {
|
||||
let result = serde_json::json!({
|
||||
"error": format!("Job not found: {}", e)
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn requires_approval(&self) -> bool {
|
||||
true // Canceling a job should require approval
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_job_tool() {
|
||||
let manager = Arc::new(ContextManager::new(5));
|
||||
let tool = CreateJobTool::new(manager.clone());
|
||||
|
||||
let params = serde_json::json!({
|
||||
"title": "Test Job",
|
||||
"description": "A test job description"
|
||||
});
|
||||
|
||||
let ctx = JobContext::default();
|
||||
let result = tool.execute(params, &ctx).await.unwrap();
|
||||
|
||||
let job_id = result.result.get("job_id").unwrap().as_str().unwrap();
|
||||
assert!(!job_id.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_jobs_tool() {
|
||||
let manager = Arc::new(ContextManager::new(5));
|
||||
|
||||
// Create some jobs
|
||||
manager.create_job("Job 1", "Desc 1").await.unwrap();
|
||||
manager.create_job("Job 2", "Desc 2").await.unwrap();
|
||||
|
||||
let tool = ListJobsTool::new(manager);
|
||||
|
||||
let params = serde_json::json!({});
|
||||
let ctx = JobContext::default();
|
||||
let result = tool.execute(params, &ctx).await.unwrap();
|
||||
|
||||
let jobs = result.result.get("jobs").unwrap().as_array().unwrap();
|
||||
assert_eq!(jobs.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_job_status_tool() {
|
||||
let manager = Arc::new(ContextManager::new(5));
|
||||
let job_id = manager.create_job("Test Job", "Description").await.unwrap();
|
||||
|
||||
let tool = JobStatusTool::new(manager);
|
||||
|
||||
let params = serde_json::json!({
|
||||
"job_id": job_id.to_string()
|
||||
});
|
||||
let ctx = JobContext::default();
|
||||
let result = tool.execute(params, &ctx).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.result.get("title").unwrap().as_str().unwrap(),
|
||||
"Test Job"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ mod echo;
|
||||
mod ecommerce;
|
||||
mod file;
|
||||
mod http;
|
||||
mod job;
|
||||
mod json;
|
||||
mod marketplace;
|
||||
mod memory;
|
||||
@@ -16,6 +17,7 @@ pub use echo::EchoTool;
|
||||
pub use ecommerce::EcommerceTool;
|
||||
pub use file::{ApplyPatchTool, ListDirTool, ReadFileTool, WriteFileTool};
|
||||
pub use http::HttpTool;
|
||||
pub use job::{CancelJobTool, CreateJobTool, JobStatusTool, ListJobsTool};
|
||||
pub use json::JsonTool;
|
||||
pub use marketplace::MarketplaceTool;
|
||||
pub use memory::{MemoryReadTool, MemorySearchTool, MemoryTreeTool, MemoryWriteTool};
|
||||
|
||||
+145
-30
@@ -1,15 +1,27 @@
|
||||
//! Shell execution tool for running commands in a sandboxed environment.
|
||||
//!
|
||||
//! Provides controlled command execution with:
|
||||
//! - Docker sandbox isolation (when enabled)
|
||||
//! - Working directory isolation
|
||||
//! - Timeout enforcement
|
||||
//! - Output capture and truncation
|
||||
//! - Blocked command patterns for safety
|
||||
//!
|
||||
//! # Execution Modes
|
||||
//!
|
||||
//! When sandbox is available and enabled:
|
||||
//! - Commands run inside ephemeral Docker containers
|
||||
//! - Network traffic goes through a validating proxy
|
||||
//! - Credentials are injected by the proxy, never exposed to commands
|
||||
//!
|
||||
//! When sandbox is unavailable:
|
||||
//! - Commands run directly on host with basic protections
|
||||
//! - Blocked command patterns are still enforced
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -17,6 +29,7 @@ use tokio::io::AsyncReadExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::context::JobContext;
|
||||
use crate::sandbox::{SandboxManager, SandboxPolicy};
|
||||
use crate::tools::tool::{Tool, ToolError, ToolOutput};
|
||||
|
||||
/// Maximum output size before truncation (64KB).
|
||||
@@ -62,7 +75,6 @@ static DANGEROUS_PATTERNS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
|
||||
});
|
||||
|
||||
/// Shell command execution tool.
|
||||
#[derive(Debug)]
|
||||
pub struct ShellTool {
|
||||
/// Working directory for commands (if None, uses job's working dir or cwd).
|
||||
working_dir: Option<PathBuf>,
|
||||
@@ -70,6 +82,22 @@ pub struct ShellTool {
|
||||
timeout: Duration,
|
||||
/// Whether to allow potentially dangerous commands (requires explicit approval).
|
||||
allow_dangerous: bool,
|
||||
/// Optional sandbox manager for Docker execution.
|
||||
sandbox: Option<Arc<SandboxManager>>,
|
||||
/// Sandbox policy to use when sandbox is available.
|
||||
sandbox_policy: SandboxPolicy,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ShellTool {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ShellTool")
|
||||
.field("working_dir", &self.working_dir)
|
||||
.field("timeout", &self.timeout)
|
||||
.field("allow_dangerous", &self.allow_dangerous)
|
||||
.field("sandbox", &self.sandbox.is_some())
|
||||
.field("sandbox_policy", &self.sandbox_policy)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellTool {
|
||||
@@ -79,6 +107,8 @@ impl ShellTool {
|
||||
working_dir: None,
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
allow_dangerous: false,
|
||||
sandbox: None,
|
||||
sandbox_policy: SandboxPolicy::ReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +124,18 @@ impl ShellTool {
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable sandbox execution with the given manager.
|
||||
pub fn with_sandbox(mut self, sandbox: Arc<SandboxManager>) -> Self {
|
||||
self.sandbox = Some(sandbox);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sandbox policy.
|
||||
pub fn with_sandbox_policy(mut self, policy: SandboxPolicy) -> Self {
|
||||
self.sandbox_policy = policy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if a command is blocked.
|
||||
fn is_blocked(&self, cmd: &str) -> Option<&'static str> {
|
||||
let normalized = cmd.to_lowercase();
|
||||
@@ -115,28 +157,44 @@ impl ShellTool {
|
||||
None
|
||||
}
|
||||
|
||||
/// Execute a command and capture output.
|
||||
async fn execute_command(
|
||||
/// Execute a command through the sandbox.
|
||||
async fn execute_sandboxed(
|
||||
&self,
|
||||
sandbox: &SandboxManager,
|
||||
cmd: &str,
|
||||
workdir: &Path,
|
||||
timeout: Duration,
|
||||
) -> Result<(String, i64), ToolError> {
|
||||
// Override sandbox config timeout if needed
|
||||
let result = tokio::time::timeout(timeout, async {
|
||||
sandbox
|
||||
.execute_with_policy(
|
||||
cmd,
|
||||
workdir,
|
||||
self.sandbox_policy,
|
||||
std::collections::HashMap::new(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(output)) => {
|
||||
let combined = truncate_output(&output.output);
|
||||
Ok((combined, output.exit_code))
|
||||
}
|
||||
Ok(Err(e)) => Err(ToolError::ExecutionFailed(format!("Sandbox error: {}", e))),
|
||||
Err(_) => Err(ToolError::Timeout(timeout)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command directly (fallback when sandbox unavailable).
|
||||
async fn execute_direct(
|
||||
&self,
|
||||
cmd: &str,
|
||||
workdir: Option<&str>,
|
||||
timeout: Option<u64>,
|
||||
workdir: &PathBuf,
|
||||
timeout: Duration,
|
||||
) -> Result<(String, i32), ToolError> {
|
||||
// Check for blocked commands
|
||||
if let Some(reason) = self.is_blocked(cmd) {
|
||||
return Err(ToolError::NotAuthorized(format!(
|
||||
"{}: {}",
|
||||
reason,
|
||||
truncate_for_error(cmd)
|
||||
)));
|
||||
}
|
||||
|
||||
// Determine working directory
|
||||
let cwd = workdir
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| self.working_dir.clone())
|
||||
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
|
||||
|
||||
// Build command
|
||||
let mut command = if cfg!(target_os = "windows") {
|
||||
let mut c = Command::new("cmd");
|
||||
@@ -149,7 +207,7 @@ impl ShellTool {
|
||||
};
|
||||
|
||||
command
|
||||
.current_dir(&cwd)
|
||||
.current_dir(workdir)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
@@ -159,11 +217,8 @@ impl ShellTool {
|
||||
.spawn()
|
||||
.map_err(|e| ToolError::ExecutionFailed(format!("Failed to spawn command: {}", e)))?;
|
||||
|
||||
// Determine timeout
|
||||
let timeout_duration = timeout.map(Duration::from_secs).unwrap_or(self.timeout);
|
||||
|
||||
// Wait with timeout
|
||||
let result = tokio::time::timeout(timeout_duration, async {
|
||||
let result = tokio::time::timeout(timeout, async {
|
||||
let status = child.wait().await?;
|
||||
|
||||
// Read stdout
|
||||
@@ -204,10 +259,56 @@ impl ShellTool {
|
||||
Err(_) => {
|
||||
// Timeout - try to kill the process
|
||||
let _ = child.kill().await;
|
||||
Err(ToolError::Timeout(timeout_duration))
|
||||
Err(ToolError::Timeout(timeout))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command, using sandbox if available.
|
||||
async fn execute_command(
|
||||
&self,
|
||||
cmd: &str,
|
||||
workdir: Option<&str>,
|
||||
timeout: Option<u64>,
|
||||
) -> Result<(String, i64), ToolError> {
|
||||
// Check for blocked commands
|
||||
if let Some(reason) = self.is_blocked(cmd) {
|
||||
return Err(ToolError::NotAuthorized(format!(
|
||||
"{}: {}",
|
||||
reason,
|
||||
truncate_for_error(cmd)
|
||||
)));
|
||||
}
|
||||
|
||||
// Determine working directory
|
||||
let cwd = workdir
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| self.working_dir.clone())
|
||||
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
|
||||
|
||||
// Determine timeout
|
||||
let timeout_duration = timeout.map(Duration::from_secs).unwrap_or(self.timeout);
|
||||
|
||||
// Try sandbox execution if available
|
||||
if let Some(ref sandbox) = self.sandbox {
|
||||
if sandbox.is_initialized() || sandbox.config().enabled {
|
||||
match self
|
||||
.execute_sandboxed(sandbox, cmd, &cwd, timeout_duration)
|
||||
.await
|
||||
{
|
||||
Ok((output, code)) => return Ok((output, code)),
|
||||
Err(e) => {
|
||||
// Log sandbox failure and fall through to direct execution
|
||||
tracing::warn!("Sandbox execution failed, falling back to direct: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to direct execution
|
||||
let (output, code) = self.execute_direct(cmd, &cwd, timeout_duration).await?;
|
||||
Ok((output, code as i64))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShellTool {
|
||||
@@ -224,7 +325,8 @@ impl Tool for ShellTool {
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Execute shell commands. Use for running builds, tests, git operations, and other CLI tasks. \
|
||||
Commands run in a subprocess with captured output. Long-running commands have a timeout."
|
||||
Commands run in a subprocess with captured output. Long-running commands have a timeout. \
|
||||
When Docker sandbox is enabled, commands run in isolated containers for security."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
@@ -265,10 +367,13 @@ impl Tool for ShellTool {
|
||||
let (output, exit_code) = self.execute_command(command, workdir, timeout).await?;
|
||||
let duration = start.elapsed();
|
||||
|
||||
let sandboxed = self.sandbox.is_some();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"output": output,
|
||||
"exit_code": exit_code,
|
||||
"success": exit_code == 0
|
||||
"success": exit_code == 0,
|
||||
"sandboxed": sandboxed
|
||||
});
|
||||
|
||||
Ok(ToolOutput::success(result, duration))
|
||||
@@ -348,4 +453,14 @@ mod tests {
|
||||
|
||||
assert!(matches!(result, Err(ToolError::Timeout(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sandbox_policy_builder() {
|
||||
let tool = ShellTool::new()
|
||||
.with_sandbox_policy(SandboxPolicy::WorkspaceWrite)
|
||||
.with_timeout(Duration::from_secs(60));
|
||||
|
||||
assert_eq!(tool.sandbox_policy, SandboxPolicy::WorkspaceWrite);
|
||||
assert_eq!(tool.timeout, Duration::from_secs(60));
|
||||
}
|
||||
}
|
||||
|
||||
+17
-2
@@ -5,12 +5,14 @@ use std::sync::Arc;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::context::ContextManager;
|
||||
use crate::llm::{LlmProvider, ToolDefinition};
|
||||
use crate::safety::SafetyLayer;
|
||||
use crate::tools::builder::{BuildSoftwareTool, BuilderConfig, LlmSoftwareBuilder};
|
||||
use crate::tools::builtin::{
|
||||
ApplyPatchTool, EchoTool, HttpTool, JsonTool, ListDirTool, MemoryReadTool, MemorySearchTool,
|
||||
MemoryTreeTool, MemoryWriteTool, ReadFileTool, ShellTool, TimeTool, WriteFileTool,
|
||||
ApplyPatchTool, CancelJobTool, CreateJobTool, EchoTool, HttpTool, JobStatusTool, JsonTool,
|
||||
ListDirTool, ListJobsTool, MemoryReadTool, MemorySearchTool, MemoryTreeTool, MemoryWriteTool,
|
||||
ReadFileTool, ShellTool, TimeTool, WriteFileTool,
|
||||
};
|
||||
use crate::tools::tool::Tool;
|
||||
use crate::tools::wasm::{
|
||||
@@ -144,6 +146,19 @@ impl ToolRegistry {
|
||||
tracing::info!("Registered 4 memory tools");
|
||||
}
|
||||
|
||||
/// Register job management tools.
|
||||
///
|
||||
/// Job tools allow the LLM to create, list, check status, and cancel jobs.
|
||||
/// These enable natural language job management without hardcoded intent parsing.
|
||||
pub fn register_job_tools(&self, context_manager: Arc<ContextManager>) {
|
||||
self.register_sync(Arc::new(CreateJobTool::new(Arc::clone(&context_manager))));
|
||||
self.register_sync(Arc::new(ListJobsTool::new(Arc::clone(&context_manager))));
|
||||
self.register_sync(Arc::new(JobStatusTool::new(Arc::clone(&context_manager))));
|
||||
self.register_sync(Arc::new(CancelJobTool::new(context_manager)));
|
||||
|
||||
tracing::info!("Registered 4 job management tools");
|
||||
}
|
||||
|
||||
/// Register the software builder tool.
|
||||
///
|
||||
/// The builder tool allows the agent to create new software including WASM tools,
|
||||
|
||||
Reference in New Issue
Block a user