use super::*;
pub(crate) struct CommandMetadataInput {
pub(crate) command: String,
pub(crate) working_dir: Option<String>,
pub(crate) exit_code: Option<i32>,
pub(crate) timed_out: bool,
pub(crate) background: bool,
pub(crate) stdout_lines: usize,
pub(crate) stderr_lines: usize,
pub(crate) detected_urls: Vec<String>,
pub(crate) pid: Option<u32>,
pub(crate) log_path: Option<String>,
pub(crate) byte_count: Option<usize>,
}
pub(crate) fn command_metadata(input: CommandMetadataInput) -> ToolRunMetadata {
ToolRunMetadata {
detail: ToolMetadata::ExecuteCommand {
command: input.command,
working_dir: input.working_dir,
exit_code: input.exit_code,
timed_out: input.timed_out,
background: input.background,
stdout_lines: input.stdout_lines,
stderr_lines: input.stderr_lines,
detected_urls: input.detected_urls,
pid: input.pid,
log_path: input.log_path,
denied_by_sandbox: false,
},
line_count: Some(input.stdout_lines + input.stderr_lines),
byte_count: input.byte_count,
..ToolRunMetadata::default()
}
}
pub(crate) fn sandbox_probes() -> (bool, bool) {
static PROBES: std::sync::OnceLock<(bool, bool)> = std::sync::OnceLock::new();
*PROBES.get_or_init(|| {
(
mermaid_runtime::network_killswitch_available(),
mermaid_runtime::fs_confinement_available(),
)
})
}
pub(crate) const SANDBOX_KILL_SIGNAL: i32 = 31;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DenialKind {
Network,
Filesystem,
Ambiguous,
}
pub(crate) fn detect_denial(
run: &CommandRunOutput,
sandbox_network: bool,
sandbox_fs: bool,
) -> Option<DenialKind> {
if cfg!(target_os = "linux") {
if sandbox_network && is_sigsys_denial(run) {
return Some(DenialKind::Network);
}
if sandbox_fs && is_permission_denial(run) {
return Some(DenialKind::Filesystem);
}
return None;
}
if !is_permission_denial(run) {
return None;
}
match (sandbox_network, sandbox_fs) {
(true, true) => Some(DenialKind::Ambiguous),
(true, false) => Some(DenialKind::Network),
(false, true) => Some(DenialKind::Filesystem),
(false, false) => None,
}
}
pub(crate) const NETWORK_DENIED_MESSAGE: &str = "Blocked by the network sandbox: this command tried to open an internet socket, which is denied because network access is off (safety.network = \"deny\" / --no-network). Re-run without --no-network, approve the command, or use full-access mode to allow network access.";
pub(crate) const HEDGED_NETWORK_DENIED_MESSAGE: &str = "Command failed with a permission error while the network sandbox was active (safety.network = \"deny\" / --no-network); a network access was likely denied. Re-run without --no-network, approve the command, or use full-access mode to allow network access.";
pub(crate) const FS_DENIED_MESSAGE: &str = "Command failed with a permission error while the filesystem sandbox was active (safety.filesystem = \"project\" / --confine-fs); a write outside the project directory, the system temp directory, or /dev was likely denied. Write inside the project, or re-run without --confine-fs to allow it.";
pub(crate) const AMBIGUOUS_DENIED_MESSAGE: &str = "Command failed with a permission error while the network and filesystem sandboxes were active (--no-network / --confine-fs); a network access or a write outside the allowed directories was likely denied. Write inside the project, or re-run without the sandbox flags to allow it.";
pub(crate) fn is_sigsys_denial(run: &CommandRunOutput) -> bool {
run.signal == Some(SANDBOX_KILL_SIGNAL) || run.exit_code == Some(128 + SANDBOX_KILL_SIGNAL)
}
pub(crate) fn is_permission_denial(run: &CommandRunOutput) -> bool {
let failed = matches!(run.exit_code, Some(code) if code != 0);
failed
&& (run.output.contains("Permission denied")
|| run.output.contains("Operation not permitted"))
}