Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/goose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ keyring = { workspace = true, features = ["apple-native"], optional = true }

[target.'cfg(target_os = "linux")'.dependencies]
keyring = { workspace = true, features = ["sync-secret-service"], optional = true }

[target."cfg(unix)".dependencies]
libc = { version = "0.2.182", default-features = false, features = ["std"] }

[dev-dependencies]
Expand Down
107 changes: 68 additions & 39 deletions crates/goose/src/agents/schedule_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! This module contains all the handlers for the schedule management platform tool,
//! including job creation, execution, monitoring, and session management.

use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::mcp_utils::ToolResult;
Expand All @@ -11,8 +13,55 @@ use rmcp::model::{Content, ErrorCode, ErrorData};

use super::Agent;
use crate::recipe::Recipe;
use crate::scheduler::{
open_regular_schedule_recipe, ValidatedScheduleRecipe, MAX_SCHEDULE_RECIPE_BYTES,
};
use crate::scheduler_trait::SchedulerTrait;

fn recipe_file_error(message: &str) -> ErrorData {
ErrorData::new(ErrorCode::INTERNAL_ERROR, message.to_string(), None)
}

fn read_schedule_recipe(path: &Path) -> Result<(String, PathBuf), ErrorData> {
let canonical_path = path
.canonicalize()
.map_err(|_| recipe_file_error("Cannot read recipe file"))?;
let file = open_regular_schedule_recipe(&canonical_path).map_err(|error| {
if error.kind() == std::io::ErrorKind::InvalidInput {
recipe_file_error("Recipe path must reference a regular file")
} else {
recipe_file_error("Cannot read recipe file")
}
})?;
let opened_metadata = file
.metadata()
.map_err(|_| recipe_file_error("Cannot read recipe file"))?;
if !opened_metadata.is_file() {
return Err(recipe_file_error(
"Recipe path must reference a regular file",
));
}
if opened_metadata.len() > MAX_SCHEDULE_RECIPE_BYTES {
return Err(recipe_file_error(
"Recipe file exceeds the 1048576 byte limit",
));
}

let mut bytes = Vec::new();
file.take(MAX_SCHEDULE_RECIPE_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|_| recipe_file_error("Cannot read recipe file"))?;
if bytes.len() as u64 > MAX_SCHEDULE_RECIPE_BYTES {
return Err(recipe_file_error(
"Recipe file exceeds the 1048576 byte limit",
));
}

let content = String::from_utf8(bytes)
.map_err(|_| recipe_file_error("Recipe file must be valid UTF-8"))?;
Ok((content, canonical_path))
}

impl Agent {
/// Handle schedule management tool calls
pub async fn handle_schedule_management(
Expand Down Expand Up @@ -109,61 +158,41 @@ impl Agent {
.and_then(|v| v.as_str())
.unwrap_or("background");

if !std::path::Path::new(recipe_path).exists() {
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Recipe file not found: {}", recipe_path),
None,
));
}

// Validate it's a valid recipe by trying to parse it
match std::fs::read_to_string(recipe_path) {
Ok(content) => {
if recipe_path.ends_with(".json") {
serde_json::from_str::<Recipe>(&content).map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Invalid JSON recipe: {}", e),
None,
)
})?;
} else {
serde_yaml::from_str::<Recipe>(&content).map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Invalid YAML recipe: {}", e),
None,
)
})?;
}
}
Err(e) => {
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Cannot read recipe file: {}", e),
None,
))
}
let (content, canonical_recipe_path) = read_schedule_recipe(Path::new(recipe_path))?;
if recipe_path.ends_with(".json") {
serde_json::from_str::<Recipe>(&content)
.map_err(|_| recipe_file_error("Invalid JSON recipe"))?;
} else {
serde_yaml::from_str::<Recipe>(&content)
.map_err(|_| recipe_file_error("Invalid YAML recipe"))?;
}

// Generate unique job ID
let job_id = format!("agent_created_{}", Utc::now().timestamp());

let recipe_base_dir = canonical_recipe_path
.parent()
.map(|path| path.to_string_lossy().into_owned());
let job = crate::scheduler::ScheduledJob {
id: job_id.clone(),
source: recipe_path.to_string(),
source: canonical_recipe_path.to_string_lossy().into_owned(),
cron: cron_expression.to_string(),
last_run: None,
currently_running: false,
paused: false,
current_session_id: None,
process_start_time: None,
parameters: vec![],
recipe_base_dir: None,
recipe_base_dir,
};

match scheduler.add_scheduled_job(job, true).await {
match scheduler
.add_scheduled_job_with_recipe(
job,
ValidatedScheduleRecipe::new(content.into_bytes(), canonical_recipe_path),
)
.await
{
Ok(()) => Ok(vec![Content::text(format!(
"Successfully created scheduled job '{}' for recipe '{}' with cron expression '{}' in {} mode",
job_id, recipe_path, cron_expression, execution_mode
Expand Down
Loading