magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
//! Read-only observations, never permission to write. The writer lock remains authoritative.
use super::{Session, SessionWriterLease};
use crate::persistence::{CrossProcessFileLock, atomic_write_with_permissions};
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::{
    fs,
    io::{self, Read},
    path::{Path, PathBuf},
    time::Duration,
};

const OWNER_READ_BYTES: u64 = 256;
// A stopped renewal or delayed reader must not advertise a known owner.
const OWNER_FRESHNESS: Duration = Duration::from_secs(5);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum SessionOwnershipHint {
    DaemonOwned,
    Busy,
    Unowned,
    Unknown,
}

impl SessionOwnershipHint {
    pub(crate) fn label(self) -> &'static str {
        match self {
            Self::DaemonOwned => "daemon-owned",
            Self::Busy => "busy",
            Self::Unowned => "unowned",
            Self::Unknown => "unknown",
        }
    }
}

impl Session {
    pub(super) fn acquire_frontend_writer(
        &self,
        daemon: bool,
    ) -> anyhow::Result<Option<SessionWriterLease>> {
        let root = self
            .path
            .parent()
            .ok_or_else(|| anyhow::anyhow!("missing root"))?;
        super::super::store::prepare_session_root(root)?;
        let writer = CrossProcessFileLock::try_acquire(&self.path.with_extension("writer"))?;
        if writer.is_some() && daemon {
            // Failure to publish navigation metadata must not lose a valid writer lease.
            if let Ok((contents, _)) = read_owner_file(&self.writer_lock_path()) {
                let marker = daemon_marker(&contents);
                let _ = atomic_write_with_permissions(
                    &self.path.with_extension("writer-owner"),
                    marker.as_bytes(),
                    Some(super::super::store::SESSION_FILE_MODE),
                );
            }
        }
        Ok(writer)
    }

    /// Bounded local observation only. Never acquires, renews, removes, or repairs locks.
    /// Missing locks mean unowned at observation time, not guaranteed availability.
    pub(crate) fn frontend_ownership_hint(&self) -> SessionOwnershipHint {
        let Some(root) = self.path.parent() else {
            return SessionOwnershipHint::Unknown;
        };
        if super::super::store::validate_session_root(root).is_err() {
            return SessionOwnershipHint::Unknown;
        }
        let (contents, metadata) = match read_owner_file(&self.writer_lock_path()) {
            Ok(value) => value,
            Err(error) if error.kind() == io::ErrorKind::NotFound => {
                return SessionOwnershipHint::Unowned;
            }
            Err(_) => return SessionOwnershipHint::Unknown,
        };
        if !valid_fresh_lock(&contents, &metadata) {
            return SessionOwnershipHint::Unknown;
        }
        match read_owner_file(&self.path.with_extension("writer-owner")) {
            Ok((marker, _)) if marker == daemon_marker(&contents).as_bytes() => {
                SessionOwnershipHint::DaemonOwned
            }
            // A previous owner's marker cannot identify this lock's owner.
            Ok(_) => SessionOwnershipHint::Busy,
            Err(error) if error.kind() == io::ErrorKind::NotFound => SessionOwnershipHint::Busy,
            Err(_) => SessionOwnershipHint::Unknown,
        }
    }

    fn writer_lock_path(&self) -> PathBuf {
        // Mirrors CrossProcessFileLock's target naming; no independent lock authority.
        self.path
            .with_file_name(format!(".{}.writer.lock", self.id()))
    }
}

fn daemon_marker(contents: &[u8]) -> String {
    // No PID, endpoint, path, credential, or user text is published in the marker.
    use base64::Engine;
    format!(
        "daemon-v1:{}\n",
        base64::engine::general_purpose::STANDARD.encode(Sha256::digest(contents))
    )
}

fn valid_fresh_lock(contents: &[u8], metadata: &fs::Metadata) -> bool {
    let Ok(contents) = std::str::from_utf8(contents) else {
        return false;
    };
    let mut lines = contents.lines();
    let valid_pid = lines
        .next()
        .and_then(|line| line.strip_prefix("pid="))
        .and_then(|pid| pid.parse::<u32>().ok())
        .is_some_and(|pid| pid > 0);
    let valid_token = lines
        .next()
        .and_then(|line| line.strip_prefix("token="))
        .is_some_and(|token| !token.is_empty());
    valid_pid
        && valid_token
        && lines.next().is_none()
        && metadata
            .modified()
            .ok()
            .and_then(|time| time.elapsed().ok())
            .is_some_and(|age| age <= OWNER_FRESHNESS)
}

fn read_owner_file(path: &Path) -> io::Result<(Vec<u8>, fs::Metadata)> {
    let metadata = fs::symlink_metadata(path)?;
    if !metadata.is_file() || metadata.len() > OWNER_READ_BYTES {
        return Err(io::Error::other("invalid ownership metadata"));
    }
    let mut options = fs::OpenOptions::new();
    options.read(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
    }
    let file = options.open(path)?;
    let metadata = file.metadata()?;
    if !metadata.is_file() || metadata.len() > OWNER_READ_BYTES {
        return Err(io::Error::other("invalid ownership metadata"));
    }
    let mut contents = Vec::new();
    file.take(OWNER_READ_BYTES + 1).read_to_end(&mut contents)?;
    if contents.len() as u64 > OWNER_READ_BYTES {
        return Err(io::Error::other("ownership metadata exceeds read limit"));
    }
    Ok((contents, metadata))
}

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

    fn test_session(root: &Path) -> Session {
        super::super::super::store::prepare_session_root(root).unwrap();
        SessionManager::new(root.to_owned())
            .open("hint-test")
            .unwrap()
    }

    #[test]
    fn hints_never_acquire_or_override_writer_authority() {
        let temp = tempfile::tempdir().unwrap();
        let session = test_session(&temp.path().join("sessions"));
        assert_eq!(
            session.frontend_ownership_hint(),
            SessionOwnershipHint::Unowned
        );
        assert!(!session.writer_lock_path().exists());
        let daemon = session.try_daemon_writer().unwrap().unwrap();
        assert_eq!(
            session.frontend_ownership_hint(),
            SessionOwnershipHint::DaemonOwned
        );
        session
            .append(&crate::sessions::SessionEvent::new(
                "diagnostic",
                session.id().to_owned(),
                temp.path().to_owned(),
                serde_json::json!({}),
            ))
            .unwrap();
        let manager = SessionManager::new(temp.path().join("sessions"));
        let list = serde_json::to_value(manager.frontend_list(None, 1).unwrap()).unwrap();
        assert_eq!(list["sessions"][0]["ownership_hint"], "daemon-owned");
        assert_eq!(list["sessions"][0].as_object().unwrap().len(), 4);
        assert!(session.try_frontend_writer().unwrap().is_none());
        drop(daemon);
        // The intentionally retained marker is not ownership evidence by itself.
        assert_eq!(
            session.frontend_ownership_hint(),
            SessionOwnershipHint::Unowned
        );
        let local = session.try_frontend_writer().unwrap().unwrap();
        assert_eq!(
            session.frontend_ownership_hint(),
            SessionOwnershipHint::Busy
        );
        assert!(session.try_daemon_writer().unwrap().is_none());
        drop(local);
    }

    #[test]
    fn stale_corrupt_and_oversized_evidence_is_read_only() {
        let temp = tempfile::tempdir().unwrap();
        let session = test_session(&temp.path().join("sessions"));
        let path = session.writer_lock_path();
        for contents in [b"bad".to_vec(), vec![b'x'; 257]] {
            fs::write(&path, &contents).unwrap();
            assert_eq!(
                session.frontend_ownership_hint(),
                SessionOwnershipHint::Unknown
            );
            assert_eq!(fs::read(&path).unwrap(), contents);
        }
        let contents = b"pid=1\ntoken=old-owner\n";
        fs::write(&path, contents).unwrap();
        fs::write(
            session.path.with_extension("writer-owner"),
            daemon_marker(contents),
        )
        .unwrap();
        fs::File::options()
            .write(true)
            .open(&path)
            .unwrap()
            .set_modified(std::time::SystemTime::UNIX_EPOCH)
            .unwrap();
        assert_eq!(
            session.frontend_ownership_hint(),
            SessionOwnershipHint::Unknown
        );
        assert_eq!(fs::read(&path).unwrap(), contents);
    }

    #[cfg(unix)]
    #[test]
    fn ownership_reads_reject_symlinks_and_special_files() {
        let temp = tempfile::tempdir().unwrap();
        let session = test_session(&temp.path().join("sessions"));
        let path = session.writer_lock_path();
        std::os::unix::fs::symlink("/dev/zero", &path).unwrap();
        assert_eq!(
            session.frontend_ownership_hint(),
            SessionOwnershipHint::Unknown
        );
        fs::remove_file(&path).unwrap();
        fs::create_dir(&path).unwrap();
        assert_eq!(
            session.frontend_ownership_hint(),
            SessionOwnershipHint::Unknown
        );
    }
}