feat: add background sandbox reaper for orphaned Docker containers (#634)

* feat: add background sandbox reaper for orphaned Docker containers

* add tests

* review fixes

* linter fix

* review fixes

* style: format test assertion in reaper

Apply rustfmt to improve code formatting consistency.

Co-Authored-By: Claude Haiku 4.5 <[email protected]>

* fix: revert assertion to single-line format for CI compatibility

The assertion should remain on a single line to match CI's
rustfmt expectations.

Co-Authored-By: Claude Haiku 4.5 <[email protected]>

* fix: format assertion to multi-line for CI rustfmt

Use multi-line format for the assert macro to comply with
CI's rustfmt line length limit (100 chars).

Co-Authored-By: Claude Haiku 4.5 <[email protected]>

---------

Co-authored-by: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
Nick Pismenkov
2026-03-09 12:46:04 -07:00
committed by GitHub
co-authored by Claude Haiku 4.5
parent fcb152e408
commit 45923ef360
6 changed files with 1036 additions and 2 deletions
+23 -2
View File
@@ -1,6 +1,7 @@
//! IronClaw - Main entry point.
use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
use tracing_subscriber::EnvFilter;
@@ -25,8 +26,8 @@ use ironclaw::{
hooks::bootstrap_hooks,
llm::create_session_manager,
orchestrator::{
ContainerJobConfig, ContainerJobManager, OrchestratorApi, TokenStore,
api::OrchestratorState,
ContainerJobConfig, ContainerJobManager, OrchestratorApi, ReaperConfig, SandboxReaper,
TokenStore, api::OrchestratorState,
},
pairing::PairingStore,
secrets::SecretsStore,
@@ -676,6 +677,9 @@ async fn async_main() -> anyhow::Result<()> {
.recording_handle
.as_ref()
.map(|r| r.http_interceptor());
// Clone context_manager for the reaper before it's moved into Agent::new()
let reaper_context_manager = Arc::clone(&components.context_manager);
let deps = AgentDeps {
store: components.db,
llm: components.llm,
@@ -714,6 +718,23 @@ async fn async_main() -> anyhow::Result<()> {
// Fill the scheduler slot now that Agent (and its Scheduler) exist.
*scheduler_slot.write().await = Some(agent.scheduler());
// Spawn sandbox reaper for orphaned container cleanup
if let Some(ref jm) = container_job_manager {
let reaper_jm = Arc::clone(jm);
let reaper_config = ReaperConfig {
scan_interval: Duration::from_secs(config.sandbox.reaper_interval_secs),
orphan_threshold: Duration::from_secs(config.sandbox.orphan_threshold_secs),
..ReaperConfig::default()
};
let reaper_ctx = Arc::clone(&reaper_context_manager);
tokio::spawn(async move {
match SandboxReaper::new(reaper_jm, reaper_ctx, reaper_config).await {
Ok(reaper) => reaper.run().await,
Err(e) => tracing::error!("Sandbox reaper failed to initialize: {}", e),
}
});
}
// Give the agent the routine engine slot so it can expose the engine to the gateway.
if let Some(slot) = routine_engine_slot {
agent.set_routine_engine_slot(slot);