use crate::cli::{RunArgs, SandboxArgs, ShellArgs, WrapArgs};
use crate::exec_strategy;
use crate::execution_runtime::execute_sandboxed;
use crate::launch_runtime::{
ExecutionFlags, LaunchPlan, SessionLaunchOptions, load_configured_detach_sequence,
load_configured_redaction_policy, prepare_run_launch_plan, resolve_requested_workdir,
select_exec_strategy,
};
use crate::output;
use crate::profile;
use crate::proxy_runtime::prepare_proxy_launch_options;
use crate::sandbox_prepare::{
prepare_sandbox, print_allow_gpu_warning, print_allow_launch_services_warning,
should_auto_enable_claude_launch_services, validate_external_proxy_bypass,
};
use crate::theme;
use nono::{NonoError, Result};
use std::ffi::OsString;
use std::path::PathBuf;
use tracing::warn;
fn resolve_profile_binary(
profile_name: &str,
loaded: &profile::Profile,
silent: bool,
) -> Option<String> {
let binary = loaded.binary.as_ref()?;
let is_user_profile =
profile::is_user_override(profile_name) || profile::is_file_path_ref(profile_name);
if !is_user_profile {
if !silent {
warn!(
"Profile '{profile_name}' specifies binary '{binary}' but is not a user profile; ignoring",
);
}
return None;
}
Some(binary.clone())
}
fn resolve_program_from_profile_or_cli(
cli_command: &[String],
loaded_profile: Option<(&str, &profile::Profile)>,
silent: bool,
) -> Result<(OsString, Vec<OsString>)> {
let profile_binary =
loaded_profile.and_then(|(name, prof)| resolve_profile_binary(name, prof, silent));
if let Some(binary) = profile_binary {
if !cli_command.is_empty() && !silent {
crate::output::print_warning(&format!(
"Profile specifies binary '{}'; ignoring trailing command '{}'",
binary,
cli_command.join(" ")
));
}
let program = OsString::from(&binary);
Ok((program, Vec::new()))
} else if !cli_command.is_empty() {
let mut iter = cli_command.iter();
let program = OsString::from(iter.next().ok_or(NonoError::NoCommand)?);
let cmd_args: Vec<OsString> = iter.map(OsString::from).collect();
Ok((program, cmd_args))
} else {
Err(NonoError::NoCommand)
}
}
pub(crate) fn run_sandbox(mut run_args: RunArgs, silent: bool) -> Result<()> {
let command = run_args.command.clone();
let loaded_profile = match run_args.sandbox.profile.as_ref() {
Some(name) => Some((name.clone(), profile::load_profile(name)?)),
None => None,
};
let (program, mut cmd_args) = resolve_program_from_profile_or_cli(
&command,
loaded_profile.as_ref().map(|(n, p)| (n.as_str(), p)),
silent,
)?;
if should_auto_enable_claude_launch_services(&run_args.sandbox, &program, &cmd_args) {
warn!(
"Auto-enabling --allow-launch-services for Claude Code because no refresh-capable local auth was detected"
);
run_args.sandbox.allow_launch_services = true;
}
let args = run_args.sandbox.clone();
if let Some((_, ref loaded)) = loaded_profile
&& !loaded.command_args.is_empty()
{
let all_packs_installed = loaded.packs.iter().all(|pack_ref| {
let parts: Vec<&str> = pack_ref.splitn(2, '/').collect();
if parts.len() != 2 {
return false;
}
crate::package::package_install_dir(parts[0], parts[1])
.map(|dir| dir.exists())
.unwrap_or(false)
});
if all_packs_installed || loaded.packs.is_empty() {
let workdir = args
.workdir
.clone()
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| PathBuf::from("."));
for arg in &loaded.command_args {
let expanded = profile::expand_vars(arg, &workdir)?;
cmd_args.push(OsString::from(expanded));
}
}
}
if args.dry_run {
let prepared = prepare_sandbox(&args, silent)?;
validate_external_proxy_bypass(&args, &prepared)?;
if !prepared.secrets.is_empty() && !silent {
eprintln!(
" Would inject {} credential(s) as environment variables",
prepared.secrets.len()
);
}
let redaction_policy = load_configured_redaction_policy()?;
output::print_dry_run(&program, &cmd_args, &redaction_policy, silent);
return Ok(());
}
let launch_plan = prepare_run_launch_plan(run_args, program, cmd_args, silent)?;
execute_sandboxed(launch_plan)
}
pub(crate) fn run_shell(args: ShellArgs, silent: bool) -> Result<()> {
let shell_path = args
.shell
.or_else(|| {
std::env::var("SHELL")
.ok()
.filter(|shell| !shell.is_empty())
.map(std::path::PathBuf::from)
})
.unwrap_or_else(|| std::path::PathBuf::from("/bin/sh"));
if args.sandbox.dry_run {
let prepared = prepare_sandbox(&args.sandbox, silent)?;
if !prepared.secrets.is_empty() && !silent {
eprintln!(
" Would inject {} credential(s) as environment variables",
prepared.secrets.len()
);
}
let redaction_policy = load_configured_redaction_policy()?;
output::print_dry_run(shell_path.as_os_str(), &[], &redaction_policy, silent);
return Ok(());
}
let prepared = prepare_sandbox(&args.sandbox, silent)?;
if prepared.allow_launch_services_active {
print_allow_launch_services_warning(silent);
}
if prepared.allow_gpu_active {
print_allow_gpu_warning(silent);
}
if !silent {
eprintln!("{}", {
let theme = theme::current();
theme::fg("Exit the shell with Ctrl-D or 'exit'.", theme.subtext)
});
eprintln!();
}
let proxy = prepare_proxy_launch_options(&args.sandbox, &prepared, silent)?;
let strategy = select_exec_strategy(
false,
proxy.active,
prepared.capability_elevation,
false,
false,
);
execute_sandboxed(LaunchPlan {
program: shell_path.into_os_string(),
cmd_args: vec![],
caps: prepared.caps,
loaded_secrets: prepared.secrets,
flags: ExecutionFlags {
strategy,
workdir: resolve_requested_workdir(args.sandbox.workdir.as_ref()),
no_diagnostics: true,
capability_elevation: prepared.capability_elevation,
#[cfg(target_os = "linux")]
wsl2_proxy_policy: prepared.wsl2_proxy_policy,
#[cfg(target_os = "linux")]
af_unix_mediation: prepared.af_unix_mediation,
bypass_protection_paths: prepared.bypass_protection_paths,
ignored_denial_paths: prepared.ignored_denial_paths,
allowed_env_vars: prepared.allowed_env_vars,
denied_env_vars: prepared.denied_env_vars,
startup_timeout_secs: args.startup_timeout_secs,
proxy,
redaction_policy: load_configured_redaction_policy()?,
session: SessionLaunchOptions {
session_name: args.name,
detach_sequence: load_configured_detach_sequence()?,
..SessionLaunchOptions::default()
},
..ExecutionFlags::defaults(silent)?
},
})
}
pub(crate) fn run_wrap(wrap_args: WrapArgs, silent: bool) -> Result<()> {
let args: SandboxArgs = wrap_args.sandbox.into();
let command = wrap_args.command;
let no_diagnostics = wrap_args.no_diagnostics;
if command.is_empty() {
return Err(NonoError::NoCommand);
}
let mut command_iter = command.into_iter();
let program = OsString::from(command_iter.next().ok_or(NonoError::NoCommand)?);
let cmd_args: Vec<OsString> = command_iter.map(OsString::from).collect();
if args.dry_run {
let prepared = prepare_sandbox(&args, silent)?;
if !prepared.secrets.is_empty() && !silent {
eprintln!(
" Would inject {} credential(s) as environment variables",
prepared.secrets.len()
);
}
let redaction_policy = load_configured_redaction_policy()?;
output::print_dry_run(&program, &cmd_args, &redaction_policy, silent);
return Ok(());
}
let prepared = prepare_sandbox(&args, silent)?;
if prepared.upstream_proxy.is_some()
|| matches!(
prepared.caps.network_mode(),
nono::NetworkMode::ProxyOnly { .. }
)
{
return Err(NonoError::ConfigParse(
"nono wrap does not support proxy mode (activated by profile network settings). \
Use `nono run` instead."
.to_string(),
));
}
#[cfg(target_os = "linux")]
if prepared.af_unix_mediation.is_pathname() {
return Err(NonoError::ConfigParse(
"nono wrap does not support linux.af_unix_mediation = \"pathname\" because direct \
exec cannot run the seccomp supervisor. Use `nono run` instead."
.to_string(),
));
}
if prepared.allow_launch_services_active {
print_allow_launch_services_warning(silent);
}
if prepared.allow_gpu_active {
print_allow_gpu_warning(silent);
}
execute_sandboxed(LaunchPlan {
program,
cmd_args,
caps: prepared.caps,
loaded_secrets: prepared.secrets,
flags: ExecutionFlags {
strategy: exec_strategy::ExecStrategy::Direct,
workdir: resolve_requested_workdir(args.workdir.as_ref()),
no_diagnostics,
bypass_protection_paths: prepared.bypass_protection_paths,
ignored_denial_paths: prepared.ignored_denial_paths,
allowed_env_vars: prepared.allowed_env_vars,
denied_env_vars: prepared.denied_env_vars,
..ExecutionFlags::defaults(silent)?
},
})
}