malvin 0.2.4

Non-interactive research and coding agent
use std::path::Path;
use std::process::Stdio;
use std::time::Duration;

pub fn process_alive(pid: u32) -> bool {
    std::process::Command::new("kill")
        .arg("-0")
        .arg(pid.to_string())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .is_ok_and(|s| s.success())
}

pub(crate) fn hostile_script_delay_ms(default_ms: u64) -> u64 {
    if crate::acp::test_no_real_agent_enabled() {
        10
    } else {
        default_ms
    }
}

fn hostile_test_poll_interval() -> Duration {
    if crate::acp::test_no_real_agent_enabled() {
        Duration::from_millis(2)
    } else {
        Duration::from_millis(20)
    }
}

fn hostile_test_wait_budget() -> Duration {
    if crate::acp::test_no_real_agent_enabled() {
        Duration::from_millis(100)
    } else {
        Duration::from_millis(500)
    }
}

pub fn spawn_hostile_double_fork_daemon(
    cwd: &Path,
    orphan_pid_file: &Path,
) -> (std::process::Child, u32) {
    use std::os::unix::process::CommandExt;
    let script = format!(
        r#"python3 -c 'import os,sys,time
pid=os.fork()
if pid==0:
 os.setsid()
 g=os.fork()
 if g==0:
  open(sys.argv[1],"w").write(str(os.getpid()))
  os.execvp("sleep",["sleep","120"])
 time.sleep({child_delay} / 1000)
 os._exit(0)
os.waitpid(pid,0)' "{orphan_pid_file}"
exec sleep 60
"#,
        child_delay = hostile_script_delay_ms(200),
        orphan_pid_file = orphan_pid_file.display(),
    );
    let mut cmd = std::process::Command::new("sh");
    cmd.arg("-c").arg(script);
    cmd.current_dir(cwd);
    cmd.stdin(Stdio::null());
    cmd.stdout(Stdio::null());
    cmd.stderr(Stdio::null());
    cmd.process_group(0);
    let child = cmd.spawn().expect("spawn hostile double-fork agent");
    let pgid = child.id();
    (child, pgid)
}

pub fn spawn_hostile_agent_exits_after_orphan_fork(
    cwd: &Path,
    orphan_pid_file: &Path,
) -> (std::process::Child, u32) {
    use std::os::unix::process::CommandExt;
    let script = format!(
        r#"python3 -c 'import os,sys,time
pid=os.fork()
if pid==0:
 open(sys.argv[1],"w").write(str(os.getpid()))
 time.sleep({orphan_delay} / 1000)
 os.setsid()
 os.execvp("sleep",["sleep","120"])
else:
 time.sleep({parent_delay} / 1000)
 os._exit(0)' "{orphan_pid_file}"
"#,
        orphan_delay = hostile_script_delay_ms(200),
        parent_delay = hostile_script_delay_ms(500),
        orphan_pid_file = orphan_pid_file.display(),
    );
    let mut cmd = std::process::Command::new("sh");
    cmd.arg("-c").arg(script);
    cmd.current_dir(cwd);
    cmd.stdin(Stdio::null());
    cmd.stdout(Stdio::null());
    cmd.stderr(Stdio::null());
    cmd.process_group(0);
    let child = cmd.spawn().expect("spawn hostile agent");
    let pgid = child.id();
    (child, pgid)
}

pub fn spawn_hostile_agent(cwd: &Path, orphan_pid_file: &Path) -> (std::process::Child, u32) {
    use std::os::unix::process::CommandExt;
    let script = format!(
        r#"python3 -c 'import os,sys,time
pid=os.fork()
if pid==0:
 open(sys.argv[1],"w").write(str(os.getpid()))
 time.sleep({orphan_delay} / 1000)
 os.setsid()
 os.execvp("sleep",["sleep","120"])' "{orphan_pid_file}"
exec sleep 60
"#,
        orphan_delay = hostile_script_delay_ms(200),
        orphan_pid_file = orphan_pid_file.display(),
    );
    let mut cmd = std::process::Command::new("sh");
    cmd.arg("-c").arg(script);
    cmd.current_dir(cwd);
    cmd.stdin(Stdio::null());
    cmd.stdout(Stdio::null());
    cmd.stderr(Stdio::null());
    cmd.process_group(0);
    let child = cmd.spawn().expect("spawn hostile agent");
    let pgid = child.id();
    (child, pgid)
}

#[path = "hostile_orphan_read_pid.rs"]
mod hostile_orphan_read_pid;
pub use hostile_orphan_read_pid::read_orphan_pid;

#[path = "hostile_orphan_user_shell.rs"]
mod hostile_orphan_user_shell;
pub use hostile_orphan_user_shell::{
    cleanup_user_coincidental_test, setup_user_init_reparented_daemon, spawn_isolated_agent_sleep,
    spawn_user_coincidental_daemon, spawn_user_shell_cooperator,
};

pub async fn wait_for_init_reparent(pid: u32) {
    let poll = hostile_test_poll_interval();
    let deadline = tokio::time::Instant::now() + hostile_test_wait_budget();
    while tokio::time::Instant::now() < deadline {
        if super::unix_process_group_ps::list_proc_rows()
            .unwrap_or_default()
            .iter()
            .find(|row| row.pid == pid)
            .is_some_and(|row| row.ppid == super::unix_process_group_ps::INIT_PID)
        {
            return;
        }
        tokio::time::sleep(poll).await;
    }
    panic!("orphan never reparented to init (pid={pid})");
}

pub fn spawn_hostile_agent_acp_orphan(
    cwd: &Path,
    orphan_pid_file: &Path,
) -> (std::process::Child, u32) {
    use std::os::unix::process::CommandExt;
    let script = format!(
        r#"python3 -c 'import os,sys
pid=os.fork()
if pid==0:
 open(sys.argv[1],"w").write(str(os.getpid()))
 os.setsid()
 os.environ["MALVIN_WORKSPACE"]="/tmp/malvin-hostile-orptest"
 os.execvp("sleep",["sleep","120"])
else:
 os._exit(0)' "{}"
"#,
        orphan_pid_file.display()
    );
    let mut cmd = std::process::Command::new("sh");
    cmd.arg("-c").arg(script);
    cmd.current_dir(cwd);
    cmd.stdin(Stdio::null());
    cmd.stdout(Stdio::null());
    cmd.stderr(Stdio::null());
    cmd.process_group(0);
    let child = cmd.spawn().expect("spawn hostile agent-acp orphan");
    let pgid = child.id();
    (child, pgid)
}

pub fn spawn_agent_pg_and_malvin_sibling() -> (u32, u32, std::process::Child, std::process::Child) {
    use std::os::unix::process::CommandExt;
    let mut agent = std::process::Command::new("sleep");
    agent.arg("120").process_group(0);
    let agent_child = agent.spawn().expect("spawn agent");
    let agent_pgid = agent_child.id();
    let mut sibling = std::process::Command::new("sleep");
    sibling.arg("120");
    let sibling_child = sibling.spawn().expect("spawn sibling");
    let sibling_pid = sibling_child.id();
    std::thread::sleep(std::time::Duration::from_millis(hostile_script_delay_ms(
        100,
    )));
    (agent_pgid, sibling_pid, agent_child, sibling_child)
}

pub fn assert_sibling_monitored_and_blocks_spawn(
    agent_pgid: u32,
    sibling_pid: u32,
    baseline: &std::collections::HashSet<u32>,
) {
    use crate::acp::sandbox_monitor_pids;
    use crate::malvin_sandbox::{assert_dead_before_next_spawn, note_active_sandbox_session};

    let work = std::env::temp_dir().join(format!(
        "malvin_hostile_orphan_blocks_spawn_{}",
        std::process::id()
    ));
    let _ = std::fs::remove_dir_all(&work);
    std::fs::create_dir_all(&work).expect("mkdir work");
    note_active_sandbox_session(Some(agent_pgid), baseline.clone(), &work).expect("note");
    let monitor = sandbox_monitor_pids(Some(agent_pgid), baseline);
    assert!(
        monitor.contains(&sibling_pid),
        "setup: sibling must be in sandbox monitor set"
    );
    assert!(
        assert_dead_before_next_spawn().is_err(),
        "setup: dead-before-next must block while sibling is alive"
    );
}

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

    #[test]
    fn kiss_cov_spawn_hostile_agent_exits_after_orphan_fork() {
        let _ = spawn_hostile_agent_exits_after_orphan_fork;
    }
    #[test]
    fn kiss_cov_wait_for_init_reparent() {
        let _ = wait_for_init_reparent;
    }
}