agentos-native-sidecar-core 0.2.5

Shared Secure Exec sidecar logic for native and browser shells
Documentation
use agentos_bridge::ExecutionSignal;
use agentos_sidecar_protocol::protocol::{SignalDispositionAction, SignalHandlerRegistration};
use serde_json::Value;
use std::collections::BTreeMap;

pub fn execution_signal_to_kernel(signal: ExecutionSignal) -> i32 {
    match signal {
        ExecutionSignal::Terminate => 15,
        ExecutionSignal::Interrupt => 2,
        ExecutionSignal::Kill => 9,
    }
}

pub fn execution_signal_from_number(signal: i32) -> Option<ExecutionSignal> {
    match signal {
        2 => Some(ExecutionSignal::Interrupt),
        9 => Some(ExecutionSignal::Kill),
        15 => Some(ExecutionSignal::Terminate),
        _ => None,
    }
}

pub fn default_signal_exit_code(signal: i32) -> Option<i32> {
    (signal > 0).then_some(128 + signal)
}

pub fn is_valid_posix_signal_number(signal: u32) -> bool {
    signal <= 31
}

pub fn parse_posix_signal(signal: &str) -> Option<i32> {
    let trimmed = signal.trim();
    if trimmed.is_empty() {
        return None;
    }

    if let Ok(value) = trimmed.parse::<i32>() {
        return (0..=31).contains(&value).then_some(value);
    }

    let upper = trimmed.to_ascii_uppercase();
    let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
    signal_number_from_name(normalized)
}

pub fn canonical_signal_name(signal: i32) -> Option<&'static str> {
    match signal {
        1 => Some("SIGHUP"),
        2 => Some("SIGINT"),
        3 => Some("SIGQUIT"),
        4 => Some("SIGILL"),
        5 => Some("SIGTRAP"),
        6 => Some("SIGABRT"),
        7 => Some("SIGBUS"),
        8 => Some("SIGFPE"),
        9 => Some("SIGKILL"),
        10 => Some("SIGUSR1"),
        11 => Some("SIGSEGV"),
        12 => Some("SIGUSR2"),
        13 => Some("SIGPIPE"),
        14 => Some("SIGALRM"),
        15 => Some("SIGTERM"),
        16 => Some("SIGSTKFLT"),
        17 => Some("SIGCHLD"),
        18 => Some("SIGCONT"),
        19 => Some("SIGSTOP"),
        20 => Some("SIGTSTP"),
        21 => Some("SIGTTIN"),
        22 => Some("SIGTTOU"),
        23 => Some("SIGURG"),
        24 => Some("SIGXCPU"),
        25 => Some("SIGXFSZ"),
        26 => Some("SIGVTALRM"),
        27 => Some("SIGPROF"),
        28 => Some("SIGWINCH"),
        29 => Some("SIGIO"),
        30 => Some("SIGPWR"),
        31 => Some("SIGSYS"),
        _ => None,
    }
}

pub fn signal_number_from_name(signal: &str) -> Option<i32> {
    match signal {
        "0" => Some(0),
        "HUP" => Some(1),
        "INT" => Some(2),
        "QUIT" => Some(3),
        "ILL" => Some(4),
        "TRAP" => Some(5),
        "ABRT" | "IOT" => Some(6),
        "BUS" => Some(7),
        "FPE" => Some(8),
        "KILL" => Some(9),
        "USR1" => Some(10),
        "SEGV" => Some(11),
        "USR2" => Some(12),
        "PIPE" => Some(13),
        "ALRM" => Some(14),
        "TERM" => Some(15),
        "STKFLT" => Some(16),
        "CHLD" => Some(17),
        "CONT" => Some(18),
        "STOP" => Some(19),
        "TSTP" => Some(20),
        "TTIN" => Some(21),
        "TTOU" => Some(22),
        "URG" => Some(23),
        "XCPU" => Some(24),
        "XFSZ" => Some(25),
        "VTALRM" => Some(26),
        "PROF" => Some(27),
        "WINCH" => Some(28),
        "IO" | "POLL" => Some(29),
        "PWR" => Some(30),
        "SYS" => Some(31),
        _ => None,
    }
}

pub fn parse_process_signal_state_request(
    args: &[Value],
) -> Result<(u32, SignalHandlerRegistration), crate::SidecarCoreError> {
    let signal = signal_state_u32_arg(args, 0, "process.signal_state signal")?;
    validate_process_signal_number(signal, "process.signal_state signal")?;
    let action = signal_state_str_arg(args, 1, "process.signal_state action")?;
    let mask_json = signal_state_str_arg(args, 2, "process.signal_state mask")?;
    let flags = signal_state_u32_arg(args, 3, "process.signal_state flags")?;
    let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
        crate::SidecarCoreError::new(format!(
            "process.signal_state mask must be valid JSON: {error}"
        ))
    })?;
    for signal in &mask {
        validate_process_signal_number(*signal, "process.signal_state mask entries")?;
    }
    let action = match action.trim().to_ascii_lowercase().as_str() {
        "default" => SignalDispositionAction::Default,
        "ignore" => SignalDispositionAction::Ignore,
        "user" => SignalDispositionAction::User,
        other => {
            return Err(crate::SidecarCoreError::new(format!(
                "unsupported process.signal_state action {other}"
            )));
        }
    };

    Ok((
        signal,
        SignalHandlerRegistration {
            action,
            mask,
            flags,
        },
    ))
}

pub fn apply_process_signal_state_update(
    signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
    process_id: &str,
    signal: u32,
    registration: SignalHandlerRegistration,
) {
    if registration.action == SignalDispositionAction::Default
        && registration.mask.is_empty()
        && registration.flags == 0
    {
        let remove_process_entry = signal_states
            .get_mut(process_id)
            .map(|handlers| {
                handlers.remove(&signal);
                handlers.is_empty()
            })
            .unwrap_or(false);
        if remove_process_entry {
            signal_states.remove(process_id);
        }
        return;
    }

    signal_states
        .entry(process_id.to_owned())
        .or_default()
        .insert(signal, registration);
}

fn validate_process_signal_number(signal: u32, label: &str) -> Result<(), crate::SidecarCoreError> {
    if is_valid_posix_signal_number(signal) {
        Ok(())
    } else {
        Err(crate::SidecarCoreError::new(format!(
            "{label} must be a valid POSIX signal"
        )))
    }
}

fn signal_state_u32_arg(
    args: &[Value],
    index: usize,
    label: &str,
) -> Result<u32, crate::SidecarCoreError> {
    let value = args
        .get(index)
        .ok_or_else(|| crate::SidecarCoreError::new(format!("{label} missing")))?;
    if let Some(value) = value.as_u64() {
        return u32::try_from(value)
            .map_err(|_| crate::SidecarCoreError::new(format!("{label} must fit in u32")));
    }
    if let Some(value) = value.as_i64() {
        return u32::try_from(value)
            .map_err(|_| crate::SidecarCoreError::new(format!("{label} must fit in u32")));
    }
    if let Some(value) = value.as_str() {
        return value
            .parse::<u32>()
            .map_err(|error| crate::SidecarCoreError::new(format!("{label}: {error}")));
    }
    Err(crate::SidecarCoreError::new(format!(
        "{label} must be a u32"
    )))
}

fn signal_state_str_arg<'a>(
    args: &'a [Value],
    index: usize,
    label: &str,
) -> Result<&'a str, crate::SidecarCoreError> {
    args.get(index)
        .and_then(Value::as_str)
        .ok_or_else(|| crate::SidecarCoreError::new(format!("{label} must be a string")))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn execution_signal_mapping_matches_posix_defaults() {
        assert_eq!(execution_signal_to_kernel(ExecutionSignal::Interrupt), 2);
        assert_eq!(execution_signal_to_kernel(ExecutionSignal::Kill), 9);
        assert_eq!(execution_signal_to_kernel(ExecutionSignal::Terminate), 15);
        assert_eq!(
            execution_signal_from_number(2),
            Some(ExecutionSignal::Interrupt)
        );
        assert_eq!(execution_signal_from_number(9), Some(ExecutionSignal::Kill));
        assert_eq!(
            execution_signal_from_number(15),
            Some(ExecutionSignal::Terminate)
        );
        assert_eq!(execution_signal_from_number(10), None);
    }

    #[test]
    fn default_signal_exit_code_is_128_plus_signal() {
        assert_eq!(default_signal_exit_code(2), Some(130));
        assert_eq!(default_signal_exit_code(9), Some(137));
        assert_eq!(default_signal_exit_code(15), Some(143));
        assert_eq!(default_signal_exit_code(0), None);
    }

    #[test]
    fn validates_posix_signal_number_range() {
        assert!(is_valid_posix_signal_number(0));
        assert!(is_valid_posix_signal_number(31));
        assert!(!is_valid_posix_signal_number(32));
    }

    #[test]
    fn parses_signal_names_and_numbers() {
        assert_eq!(parse_posix_signal("SIGTERM"), Some(15));
        assert_eq!(parse_posix_signal("term"), Some(15));
        assert_eq!(canonical_signal_name(16), Some("SIGSTKFLT"));
        assert_eq!(parse_posix_signal("9"), Some(9));
        assert_eq!(parse_posix_signal("0"), Some(0));
        assert_eq!(parse_posix_signal("SIGBOGUS"), None);
        assert_eq!(parse_posix_signal("32"), None);
    }

    #[test]
    fn parses_and_applies_process_signal_state_updates() {
        let args = vec![
            Value::from(15),
            Value::from("user"),
            Value::from("[2]"),
            Value::from(0),
        ];
        let (signal, registration) =
            parse_process_signal_state_request(&args).expect("signal state");
        assert_eq!(signal, 15);
        assert_eq!(registration.action, SignalDispositionAction::User);
        assert_eq!(registration.mask, vec![2]);

        let mut states = BTreeMap::new();
        apply_process_signal_state_update(&mut states, "proc-1", signal, registration);
        assert!(states
            .get("proc-1")
            .is_some_and(|handlers| handlers.contains_key(&15)));

        apply_process_signal_state_update(
            &mut states,
            "proc-1",
            15,
            SignalHandlerRegistration {
                action: SignalDispositionAction::Default,
                mask: Vec::new(),
                flags: 0,
            },
        );
        assert!(!states.contains_key("proc-1"));
    }

    #[test]
    fn rejects_unknown_process_signal_state_values() {
        let invalid_signal = parse_process_signal_state_request(&[
            Value::from(32),
            Value::from("user"),
            Value::from("[]"),
            Value::from(0),
        ])
        .expect_err("unknown signal must fail");
        assert_eq!(
            invalid_signal.to_string(),
            "process.signal_state signal must be a valid POSIX signal"
        );

        let invalid_mask = parse_process_signal_state_request(&[
            Value::from(15),
            Value::from("user"),
            Value::from("[32]"),
            Value::from(0),
        ])
        .expect_err("unknown mask signal must fail");
        assert_eq!(
            invalid_mask.to_string(),
            "process.signal_state mask entries must be a valid POSIX signal"
        );
    }
}