fix(cron): persist delivery for api-created cron jobs (#4087)

Resolves merge conflicts from PR #4064. Uses typed DeliveryConfig in
CronAddBody and passes delivery directly to add_shell_job_with_approval
and add_agent_job instead of post-creation patching. Preserves master's
richer API fields (session_target, model, allowed_tools, delete_after_run).
This commit is contained in:
Argenis
2026-03-20 15:42:00 -04:00
committed by Roman Tataurov
parent a22cec8adf
commit a3dbf19d72
6 changed files with 515 additions and 77 deletions
+51 -15
View File
@@ -235,6 +235,19 @@ impl Tool for CronAddTool {
.get("approved")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let delivery = match args.get("delivery") {
Some(v) => match serde_json::from_value::<DeliveryConfig>(v.clone()) {
Ok(cfg) => Some(cfg),
Err(e) => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!("Invalid delivery config: {e}")),
});
}
},
None => None,
};
let result = match job_type {
JobType::Shell => {
@@ -261,7 +274,14 @@ impl Tool for CronAddTool {
return Ok(blocked);
}
cron::add_shell_job_with_approval(&self.config, name, schedule, command, approved)
cron::add_shell_job_with_approval(
&self.config,
name,
schedule,
command,
delivery,
approved,
)
}
JobType::Agent => {
let prompt = match args.get("prompt").and_then(serde_json::Value::as_str) {
@@ -307,20 +327,6 @@ impl Tool for CronAddTool {
None => None,
};
let delivery = match args.get("delivery") {
Some(v) => match serde_json::from_value::<DeliveryConfig>(v.clone()) {
Ok(cfg) => Some(cfg),
Err(e) => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!("Invalid delivery config: {e}")),
});
}
},
None => None,
};
if let Some(blocked) = self.enforce_mutation_allowed("cron_add") {
return Ok(blocked);
}
@@ -406,6 +412,36 @@ mod tests {
assert!(result.output.contains("next_run"));
}
#[tokio::test]
async fn shell_job_persists_delivery() {
let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp).await;
let tool = CronAddTool::new(cfg.clone(), test_security(&cfg));
let result = tool
.execute(json!({
"schedule": { "kind": "cron", "expr": "*/5 * * * *" },
"job_type": "shell",
"command": "echo ok",
"delivery": {
"mode": "announce",
"channel": "discord",
"to": "1234567890",
"best_effort": true
}
}))
.await
.unwrap();
assert!(result.success, "{:?}", result.error);
let jobs = cron::list_jobs(&cfg).unwrap();
assert_eq!(jobs.len(), 1);
assert_eq!(jobs[0].delivery.mode, "announce");
assert_eq!(jobs[0].delivery.channel.as_deref(), Some("discord"));
assert_eq!(jobs[0].delivery.to.as_deref(), Some("1234567890"));
assert!(jobs[0].delivery.best_effort);
}
#[tokio::test]
async fn blocks_disallowed_shell_command() {
let tmp = TempDir::new().unwrap();
+1
View File
@@ -244,6 +244,7 @@ mod tests {
tz: None,
},
"touch cron-run-approval",
None,
true,
)
.unwrap();
+1
View File
@@ -315,6 +315,7 @@ impl ScheduleTool {
tz: None,
},
command,
None,
approved,
) {
Ok(job) => job,