use async_trait::async_trait;
use std::time::Duration;
use crate::context::JobContext;
#[allow(unused_imports)]
use crate::tools::tool::{ApprovalRequirement, Tool, ToolError, ToolOutput};
pub struct RestartTool;
#[async_trait]
impl Tool for RestartTool {
fn name(&self) -> &str {
"restart"
}
fn description(&self) -> &str {
"Restart the IronClaw agent process. The process exits cleanly (code 0) and the \
container entrypoint loop restarts it automatically within a few seconds."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"delay_secs": {
"type": "integer",
"description": "Seconds to wait before exiting (default: 2, min: 1, max: 30)",
"minimum": 1,
"maximum": 30
}
}
})
}
async fn execute(
&self,
params: serde_json::Value,
_ctx: &JobContext,
) -> Result<ToolOutput, ToolError> {
tracing::info!("[RestartTool::execute] Restart tool invoked");
let start = std::time::Instant::now();
let in_docker = std::env::var("IRONCLAW_IN_DOCKER")
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false);
tracing::debug!("[RestartTool::execute] IRONCLAW_IN_DOCKER={}", in_docker);
if !in_docker {
tracing::error!("[RestartTool::execute] Not in Docker, rejecting restart");
return Err(ToolError::ExecutionFailed(
"Restart is only available when running inside the Docker container. \
For local development, please restart IronClaw manually."
.to_string(),
));
}
let delay = params
.get("delay_secs")
.and_then(|v| v.as_u64())
.unwrap_or(2)
.clamp(1, 30);
tracing::info!("[RestartTool::execute] Delay set to {} seconds", delay);
let restart_disabled = std::env::var("IRONCLAW_DISABLE_RESTART")
.map(|v| {
let v = v.to_lowercase();
v == "1" || v == "true"
})
.unwrap_or(false);
tracing::info!(
"[RestartTool::execute] Spawning background task to exit in {} seconds (disabled={})",
delay,
restart_disabled
);
tokio::spawn(async move {
tracing::info!("[RestartTool] Sleeping for {} seconds before exit", delay);
tokio::time::sleep(Duration::from_secs(delay)).await;
if !restart_disabled {
tracing::warn!("[RestartTool] Calling std::process::exit(0) NOW");
std::process::exit(0);
} else {
tracing::info!(
"[RestartTool] Exit disabled (IRONCLAW_DISABLE_RESTART set), skipping std::process::exit(0)"
);
}
});
let msg = format!(
"Restarting in {delay} second(s). The process will exit cleanly and the \
entrypoint restart loop will bring IronClaw back online."
);
tracing::info!("[RestartTool::execute] Returning success response: {}", msg);
Ok(ToolOutput::text(msg, start.elapsed()))
}
fn requires_sanitization(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn enable_docker_env() {
unsafe {
std::env::set_var("IRONCLAW_IN_DOCKER", "true");
}
}
#[test]
fn test_restart_tool_approval_handled_at_command_level() {
let tool = RestartTool;
let approval = tool.requires_approval(&serde_json::json!({}));
assert!(matches!(approval, ApprovalRequirement::Never));
}
#[test]
fn test_restart_tool_name() {
let tool = RestartTool;
assert_eq!(tool.name(), "restart");
}
#[test]
fn test_restart_tool_parameters_schema() {
let tool = RestartTool;
let schema = tool.parameters_schema();
let props = schema.get("properties").unwrap();
assert!(props.get("delay_secs").is_some());
let delay_schema = props.get("delay_secs").unwrap();
assert_eq!(delay_schema.get("minimum").unwrap().as_u64().unwrap(), 1);
assert_eq!(delay_schema.get("maximum").unwrap().as_u64().unwrap(), 30);
}
#[test]
fn test_restart_tool_requires_sanitization() {
let tool = RestartTool;
assert!(!tool.requires_sanitization());
}
#[tokio::test]
async fn test_restart_tool_delay_parameter_validation() {
enable_docker_env();
let tool = RestartTool;
let ctx = crate::context::JobContext::new("test", "test restart");
let result = tool
.execute(serde_json::json!({"delay_secs": 5}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().expect("result should be a string");
assert!(text.contains("Restarting in 5 second(s)"));
let result = tool.execute(serde_json::json!({}), &ctx).await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().expect("result should be a string");
assert!(text.contains("Restarting in 2 second(s)"));
}
#[tokio::test]
async fn test_restart_tool_delay_clamping() {
enable_docker_env();
let tool = RestartTool;
let ctx = crate::context::JobContext::new("test", "test restart");
let result = tool
.execute(serde_json::json!({"delay_secs": 0}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().expect("result should be a string");
assert!(text.contains("Restarting in 1 second(s)"));
let result = tool
.execute(serde_json::json!({"delay_secs": 100}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().expect("result should be a string");
assert!(text.contains("Restarting in 30 second(s)"));
}
#[test]
fn test_restart_tool_description() {
let tool = RestartTool;
let desc = tool.description();
assert!(desc.contains("Restart"));
assert!(desc.contains("IronClaw"));
assert!(desc.contains("exits cleanly"));
assert!(desc.contains("code 0"));
}
#[test]
fn test_restart_tool_schema_completeness() {
let tool = RestartTool;
let schema = tool.parameters_schema();
assert_eq!(schema.get("type").unwrap().as_str().unwrap(), "object");
let props = schema.get("properties").unwrap();
assert!(props.is_object());
let delay_schema = props.get("delay_secs").unwrap();
assert_eq!(
delay_schema.get("type").unwrap().as_str().unwrap(),
"integer"
);
assert!(delay_schema.get("description").is_some());
}
#[tokio::test]
async fn test_restart_tool_boundary_values() {
enable_docker_env();
let tool = RestartTool;
let ctx = crate::context::JobContext::new("test", "test restart");
let result = tool
.execute(serde_json::json!({"delay_secs": 1}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 1 second(s)"));
let result = tool
.execute(serde_json::json!({"delay_secs": 30}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 30 second(s)"));
let result = tool
.execute(serde_json::json!({"delay_secs": 15}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 15 second(s)"));
}
#[tokio::test]
async fn test_restart_tool_invalid_parameter_types() {
enable_docker_env();
let tool = RestartTool;
let ctx = crate::context::JobContext::new("test", "test restart");
let result = tool
.execute(serde_json::json!({"delay_secs": "5"}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 2 second(s)"));
let result = tool
.execute(serde_json::json!({"delay_secs": null}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 2 second(s)"));
let result = tool
.execute(serde_json::json!({"delay_secs": 5.5}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 2 second(s)"));
}
#[tokio::test]
async fn test_restart_tool_output_structure() {
enable_docker_env();
let tool = RestartTool;
let ctx = crate::context::JobContext::new("test", "test restart");
let result = tool
.execute(serde_json::json!({"delay_secs": 5}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.result.is_string());
assert!(output.duration.as_secs() == 0); assert!(output.cost.is_none()); assert!(output.raw.is_none()); }
#[tokio::test]
async fn test_restart_tool_extra_parameters_ignored() {
enable_docker_env();
let tool = RestartTool;
let ctx = crate::context::JobContext::new("test", "test restart");
let result = tool
.execute(
serde_json::json!({
"delay_secs": 5,
"extra_field": "should be ignored",
"another": 123
}),
&ctx,
)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 5 second(s)"));
}
#[tokio::test]
async fn test_restart_tool_negative_numbers() {
enable_docker_env();
let tool = RestartTool;
let ctx = crate::context::JobContext::new("test", "test restart");
let result = tool
.execute(serde_json::json!({"delay_secs": -5}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 2 second(s)"));
}
#[tokio::test]
async fn test_restart_tool_very_large_numbers() {
enable_docker_env();
let tool = RestartTool;
let ctx = crate::context::JobContext::new("test", "test restart");
let result = tool
.execute(serde_json::json!({"delay_secs": u64::MAX}), &ctx)
.await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 30 second(s)"));
}
#[tokio::test]
async fn test_restart_tool_empty_object() {
enable_docker_env();
let tool = RestartTool;
let ctx = crate::context::JobContext::new("test", "test restart");
let result = tool.execute(serde_json::json!({}), &ctx).await;
assert!(result.is_ok());
let output = result.unwrap();
let text = output.result.as_str().unwrap();
assert!(text.contains("Restarting in 2 second(s)"));
assert!(text.contains("exit cleanly"));
assert!(text.contains("entrypoint restart loop"));
}
#[test]
fn test_restart_tool_approval_consistent_regardless_of_params() {
let tool = RestartTool;
let approval1 = tool.requires_approval(&serde_json::json!({"delay_secs": 5}));
let approval2 = tool.requires_approval(&serde_json::json!({"delay_secs": 100}));
let approval3 = tool.requires_approval(&serde_json::json!({}));
assert!(matches!(approval1, ApprovalRequirement::Never));
assert!(matches!(approval2, ApprovalRequirement::Never));
assert!(matches!(approval3, ApprovalRequirement::Never));
}
#[test]
fn test_restart_tool_requires_docker_environment() {
let in_docker = std::env::var("IRONCLAW_IN_DOCKER")
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false);
if !in_docker {
assert!(
!in_docker,
"Test environment should have IRONCLAW_IN_DOCKER unset or false"
);
}
}
}