use crate::capability::AccessMode;
use crate::diagnostic::{
NonoDiagnostic, NonoDiagnosticCode, NonoDiagnosticDetail, NonoDiagnosticSeverity,
NonoRemediation, StderrObservationKind,
};
use std::path::PathBuf;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SessionObservationInput {
pub likely_sandbox_paths: Vec<(PathBuf, AccessMode)>,
pub missing_paths: Vec<PathBuf>,
pub application_failure: Option<String>,
pub blocked_protected_file: Option<String>,
pub network_blocked_hint: bool,
}
impl SessionObservationInput {
#[must_use]
pub fn into_diagnostics(self) -> Vec<NonoDiagnostic> {
let mut diagnostics = Vec::new();
if let Some(file) = self.blocked_protected_file {
diagnostics.push(diagnostic_protected_file_write(file));
}
for path in self.missing_paths {
diagnostics.push(diagnostic_missing_path(path));
}
if let Some(message) = self.application_failure {
diagnostics.push(diagnostic_application_failure(message));
}
if self.network_blocked_hint {
diagnostics.push(diagnostic_network_blocked());
}
for (path, access) in self.likely_sandbox_paths {
let remediation = grant_path_remediation(path.clone(), access);
diagnostics.push(diagnostic_likely_sandbox_path(path, access, remediation));
}
diagnostics
}
}
#[must_use]
pub fn follow_up_diagnostics() -> Vec<NonoDiagnostic> {
vec![
NonoDiagnostic::new(
NonoDiagnosticCode::CommandFailedLikelySandbox,
NonoDiagnosticSeverity::Info,
"discover additional paths required by the command",
)
.with_remediation(NonoRemediation::RunDiscovery),
NonoDiagnostic::new(
NonoDiagnosticCode::CommandFailedApplication,
NonoDiagnosticSeverity::Info,
"inspect sandbox policy for a specific path",
)
.with_remediation(NonoRemediation::CheckPolicy),
]
}
#[must_use]
pub fn diagnostic_likely_sandbox_path(
path: PathBuf,
access: AccessMode,
remediation: NonoRemediation,
) -> NonoDiagnostic {
NonoDiagnostic::new(
NonoDiagnosticCode::CommandFailedLikelySandbox,
NonoDiagnosticSeverity::Warning,
format!(
"command output suggests {} ({access}) may be sandbox-related",
path.display()
),
)
.with_path_access(path, access)
.with_remediation(remediation)
.with_detail(NonoDiagnosticDetail::StderrObservation {
observation_kind: StderrObservationKind::LikelySandboxPath,
})
}
#[must_use]
pub fn diagnostic_missing_path(path: PathBuf) -> NonoDiagnostic {
NonoDiagnostic::new(
NonoDiagnosticCode::CommandFailedApplication,
NonoDiagnosticSeverity::Warning,
format!("command reported missing path {}", path.display()),
)
.with_path_access(path.clone(), AccessMode::Read)
.with_detail(NonoDiagnosticDetail::StderrObservation {
observation_kind: StderrObservationKind::MissingPath,
})
}
#[must_use]
pub fn diagnostic_application_failure(message: String) -> NonoDiagnostic {
NonoDiagnostic::new(
NonoDiagnosticCode::CommandFailedApplication,
NonoDiagnosticSeverity::Warning,
format!("command reported application error: {message}"),
)
.with_detail(NonoDiagnosticDetail::StderrObservation {
observation_kind: StderrObservationKind::ApplicationFailure,
})
}
#[must_use]
pub fn diagnostic_protected_file_write(file: String) -> NonoDiagnostic {
NonoDiagnostic::new(
NonoDiagnosticCode::TrustVerificationFailed,
NonoDiagnosticSeverity::Warning,
format!("Write to '{file}' blocked: file is a signed instruction file."),
)
.with_detail(NonoDiagnosticDetail::StderrObservation {
observation_kind: StderrObservationKind::ProtectedFileWrite,
})
}
#[must_use]
pub fn diagnostic_network_blocked() -> NonoDiagnostic {
NonoDiagnostic::new(
NonoDiagnosticCode::SandboxDeniedNetwork,
NonoDiagnosticSeverity::Warning,
"command output contains a network error; if a required host is unreachable, check whether network access is blocked",
)
.with_detail(NonoDiagnosticDetail::StderrObservation {
observation_kind: StderrObservationKind::NetworkBlocked,
})
}
#[must_use]
fn grant_path_remediation(path: PathBuf, access: AccessMode) -> NonoRemediation {
NonoRemediation::GrantPath {
is_file: grant_path_is_file(&path),
path,
access,
}
}
#[must_use]
fn grant_path_is_file(path: &std::path::Path) -> bool {
if path.is_file() {
return true;
}
if path.is_dir() {
return false;
}
path.file_name().is_some()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn observation_input_builds_likely_sandbox_diagnostic() {
let input = SessionObservationInput {
likely_sandbox_paths: vec![(PathBuf::from("/tmp/x"), AccessMode::Read)],
..SessionObservationInput::default()
};
let diagnostics = input.into_diagnostics();
assert_eq!(diagnostics.len(), 1);
assert_eq!(
diagnostics[0].code,
NonoDiagnosticCode::CommandFailedLikelySandbox
);
}
#[test]
fn network_hint_emits_sandbox_denied_network() {
let input = SessionObservationInput {
network_blocked_hint: true,
..SessionObservationInput::default()
};
let diagnostics = input.into_diagnostics();
assert!(
diagnostics
.iter()
.any(|d| d.code == NonoDiagnosticCode::SandboxDeniedNetwork)
);
}
}