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 fn read_only_probe() -> bool {
static PROBE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*PROBE.get_or_init(mermaid_runtime::read_only_containment_available)
}
pub(crate) const SANDBOX_KILL_SIGNAL: i32 = 31;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DenialKind {
Network,
Filesystem,
Ambiguous,
ReadOnlyNetwork,
ReadOnly,
}
pub(crate) fn detect_denial(run: &CommandRunOutput, sandbox: &SandboxPlan) -> Option<DenialKind> {
if sandbox.read_only {
if cfg!(target_os = "linux") && is_sigsys_denial(run) {
return Some(DenialKind::ReadOnlyNetwork);
}
return is_permission_denial(run).then_some(DenialKind::ReadOnly);
}
let (sandbox_network, sandbox_fs) = (sandbox.network, sandbox.fs);
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 const READ_ONLY_NETWORK_DENIED_MESSAGE: &str = "Blocked by the read-only sandbox: this command tried to open an internet socket, and read_only mode runs every command with network access off. Switch to another safety mode (Shift+Tab or /safety) to allow network access.";
pub const READ_ONLY_DENIED_MESSAGE: &str = "Command failed with a permission error inside the read-only sandbox, so it likely tried to change something. read_only mode runs every command with file writes, metadata changes, sockets (network and local daemons), IPC, and signals to other processes denied by the OS. Reading works; to make changes, switch to another safety mode (Shift+Tab or /safety).";
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")
|| run.output.contains("Access is denied")
|| run.output.contains("UnauthorizedAccessException")
|| run.output.contains("10013")
|| run.output.contains("WSAEACCES"))
}