use core::fmt;
pub const ISOLATED_DEFAULT_MODEL : &str = "claude-opus-4-6";
pub const ISOLATED_CLAUDE_MD : &str = "\
# Isolated subprocess\n\n\
Execute the given task immediately and exit.\n\n\
- Do not use extended thinking — respond directly and concisely.\n\
- Do not ask clarifying questions — act on the message as given.\n\
- Do not request human confirmation for any operation.\n\
- Do not explain your reasoning or narrate your steps.\n\
- Do not use tool calls — produce the answer from your own knowledge.\n\
- Output only the direct result of the task; no preamble, no summary.\n\
- If the input is a single character or whitespace only, reply with a single period.\n";
pub const REFRESH_DEFAULT_MODEL : &str = "claude-sonnet-4-6";
#[ derive( Debug, Clone ) ]
pub enum IsolatedModel
{
Default,
KeepCurrent,
Specific( String ),
}
impl IsolatedModel
{
#[ inline ]
#[ must_use ]
pub fn model_id( &self ) -> Option< &str >
{
match self
{
IsolatedModel::Default => Some( ISOLATED_DEFAULT_MODEL ),
IsolatedModel::KeepCurrent => None,
IsolatedModel::Specific( id ) => Some( id.as_str() ),
}
}
}
#[ derive( Debug ) ]
pub struct IsolatedRunResult
{
pub exit_code : i32,
pub stdout : String,
pub stderr : String,
pub credentials : Option< String >,
}
#[ derive( Debug ) ]
pub enum RunnerError
{
ClaudeNotFound,
TempDirFailed( String ),
Timeout
{
secs : u64,
},
TimeoutWithOutput
{
secs : u64,
partial_stdout : String,
},
Io( String ),
}
impl fmt::Display for RunnerError
{
#[ inline ]
fn fmt( &self, f : &mut fmt::Formatter< '_ > ) -> fmt::Result
{
match self
{
RunnerError::ClaudeNotFound => write!( f, "claude binary not found in PATH" ),
RunnerError::TempDirFailed( reason ) =>
write!( f, "failed to create temp dir: {reason}" ),
RunnerError::Timeout { secs } =>
write!( f, "claude timed out after {secs} seconds" ),
RunnerError::TimeoutWithOutput { secs, partial_stdout } =>
{
if partial_stdout.is_empty()
{
write!( f, "claude timed out after {secs} seconds (no output captured)" )
}
else
{
write!( f, "claude timed out after {secs} seconds; partial output:\n{partial_stdout}" )
}
}
RunnerError::Io( reason ) =>
write!( f, "{reason}" ),
}
}
}
impl core::error::Error for RunnerError {}
#[ cfg( feature = "enabled" ) ]
#[ inline ]
#[ allow( clippy::too_many_lines ) ]
pub fn run_isolated
(
credentials_json : &str,
args : Vec< String >,
timeout_secs : u64,
model : IsolatedModel,
) -> Result< IsolatedRunResult, RunnerError >
{
use core::time::Duration;
let temp_dir = std::env::temp_dir()
.join( format!( "claude_isolated_{}", std::process::id() ) );
let claude_dir = temp_dir.join( ".claude" );
std::fs::create_dir_all( &claude_dir )
.map_err( |e| RunnerError::TempDirFailed( e.to_string() ) )?;
let creds_path = claude_dir.join( ".credentials.json" );
std::fs::write( &creds_path, credentials_json )
.map_err( |e| RunnerError::Io( e.to_string() ) )?;
std::fs::write( claude_dir.join( "CLAUDE.md" ), ISOLATED_CLAUDE_MD )
.map_err( |e| RunnerError::Io( e.to_string() ) )?;
let mut full_args = Vec::with_capacity( args.len() + 2 );
if let Some( id ) = model.model_id()
{
full_args.push( "--model".to_string() );
full_args.push( id.to_string() );
}
full_args.extend( args );
let cmd = crate::ClaudeCommand::new()
.with_home( &temp_dir )
.with_home_isolation()
.with_args( full_args );
let mut child = cmd.spawn_piped().map_err( |e|
{
if e.kind() == std::io::ErrorKind::NotFound
{
RunnerError::ClaudeNotFound
}
else
{
RunnerError::Io( e.to_string() )
}
} )?;
let deadline : Option< std::time::Instant > = if timeout_secs > 0
{
Some( std::time::Instant::now() + Duration::from_secs( timeout_secs ) )
}
else
{
None
};
let mut timed_out = false;
let subprocess_output : Result< std::process::Output, RunnerError > = loop
{
match child.try_wait()
{
Ok( Some( _ ) ) =>
{
break child.wait_with_output()
.map_err( |e| RunnerError::Io( e.to_string() ) );
}
Ok( None ) =>
{
if deadline.is_some_and( |d| std::time::Instant::now() >= d )
{
timed_out = true;
let _ = child.kill();
break child.wait_with_output()
.map_err( |e| RunnerError::Io( e.to_string() ) );
}
std::thread::sleep( Duration::from_millis( 50 ) );
}
Err( e ) => break Err( RunnerError::Io( e.to_string() ) ),
}
};
let credentials = std::fs::read_to_string( &creds_path )
.ok()
.and_then( |new|
{
if new.as_bytes() == credentials_json.as_bytes() { None } else { Some( new ) }
} );
let _ = std::fs::remove_dir_all( &temp_dir );
let output = subprocess_output?;
let stdout = String::from_utf8_lossy( &output.stdout ).to_string();
let stderr = String::from_utf8_lossy( &output.stderr ).to_string();
if timed_out
{
if credentials.is_some()
{
return Ok( IsolatedRunResult
{
exit_code : -1,
stdout : String::new(),
stderr : String::new(),
credentials,
} );
}
return Err( RunnerError::TimeoutWithOutput
{
secs : timeout_secs,
partial_stdout : stdout,
} );
}
let exit_code = crate::signal_exit_code( &output.status );
Ok( IsolatedRunResult { exit_code, stdout, stderr, credentials } )
}