agents-kit 0.0.2

Rust agent control Kit for Codex, OpenCode, and provider-backed Custom runtimes
Documentation
use std::path::PathBuf;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::path::{Component, Path};

use crate::workspace::BackendHostPathFlavor;
use crate::{FileRead, KitError, WorkspaceRoot};

pub(crate) struct CustomFileAdapter {
    relative_path: PathBuf,
    root_path: PathBuf,
    root_relative_path: PathBuf,
}

impl CustomFileAdapter {
    #[cfg(any(target_os = "linux", target_os = "macos"))]
    pub(crate) fn validate_workspace_path_flavor(
        flavor: BackendHostPathFlavor,
    ) -> Result<(), KitError> {
        if flavor == BackendHostPathFlavor::Posix {
            Ok(())
        } else {
            Err(KitError::unsupported(
                "workspace path flavor is unsupported on this platform",
            ))
        }
    }

    #[cfg(target_os = "windows")]
    pub(crate) fn validate_workspace_path_flavor(
        flavor: BackendHostPathFlavor,
    ) -> Result<(), KitError> {
        if flavor == BackendHostPathFlavor::Windows {
            Ok(())
        } else {
            Err(KitError::unsupported(
                "workspace path flavor is unsupported on this platform",
            ))
        }
    }

    pub(crate) fn from_resolved(
        relative_path: PathBuf,
        absolute_path: PathBuf,
        roots: &[WorkspaceRoot],
    ) -> Result<Self, KitError> {
        let (root_path, root_relative_path) = roots
            .iter()
            .filter_map(|root| {
                absolute_path
                    .strip_prefix(root.path())
                    .ok()
                    .map(|root_relative_path| (root.path(), root_relative_path))
            })
            .max_by_key(|(root_path, _)| root_path.components().count())
            .ok_or_else(|| {
                KitError::invalid_state(
                    "file admission did not identify a containing workspace root",
                )
            })?;
        if root_relative_path.as_os_str().is_empty() {
            return Err(KitError::integration("file target is not a regular file"));
        }
        Ok(Self {
            relative_path,
            root_path: root_path.to_path_buf(),
            root_relative_path: root_relative_path.to_path_buf(),
        })
    }

    #[cfg(any(target_os = "linux", target_os = "macos"))]
    pub(crate) async fn read(self) -> Result<FileRead, KitError> {
        tokio::task::spawn_blocking(move || self.read_rooted())
            .await
            .map_err(|_| {
                KitError::integration("contained file read task failed")
                    .with_detail(crate::error::ErrorDetail::TaskTerminated)
            })?
    }

    #[cfg(target_os = "windows")]
    pub(crate) async fn read(self) -> Result<FileRead, KitError> {
        let Self {
            relative_path: _,
            root_path: _,
            root_relative_path: _,
        } = self;
        Err(KitError::unsupported(
            "contained file reads are unsupported on this platform",
        ))
    }

    #[cfg(any(target_os = "linux", target_os = "macos"))]
    fn read_rooted(self) -> Result<FileRead, KitError> {
        use rustix::fs::{FileType, Mode, OFlags};

        let directory_flags =
            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
        let mut root_components = self.root_path.components();
        if !matches!(root_components.next(), Some(Component::RootDir)) {
            return Err(KitError::invalid_state(
                "workspace root is unavailable to the current process",
            ));
        }
        let mut directory = rustix::fs::open(Path::new("/"), directory_flags, Mode::empty())
            .map_err(|error| self.filesystem_error(error))?;
        for component in root_components {
            let Component::Normal(segment) = component else {
                return Err(KitError::invalid_state(
                    "workspace root is not a normalized local path",
                ));
            };
            directory = rustix::fs::openat(
                &directory,
                Path::new(segment),
                directory_flags,
                Mode::empty(),
            )
            .map_err(|error| self.descendant_error(error))?;
        }
        let parent = self
            .root_relative_path
            .parent()
            .ok_or_else(|| KitError::invalid_state("file target has no rooted parent"))?;
        for component in parent.components() {
            let Component::Normal(segment) = component else {
                return Err(KitError::invalid_state(
                    "file target is not a normalized rooted path",
                ));
            };
            directory = rustix::fs::openat(
                &directory,
                Path::new(segment),
                directory_flags,
                Mode::empty(),
            )
            .map_err(|error| self.descendant_error(error))?;
        }

        let file_name = self
            .root_relative_path
            .file_name()
            .ok_or_else(|| KitError::invalid_state("file target has no final path component"))?;
        let file_flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
        let file = rustix::fs::openat(&directory, Path::new(file_name), file_flags, Mode::empty())
            .map_err(|error| self.descendant_error(error))?;
        let metadata = rustix::fs::fstat(&file).map_err(|error| self.filesystem_error(error))?;
        if !FileType::from_raw_mode(metadata.st_mode).is_file() {
            return Err(KitError::integration("file target is not a regular file"));
        }

        let mut bytes = Vec::new();
        loop {
            let mut chunk = [0_u8; 8192];
            match rustix::io::read(&file, &mut chunk) {
                Ok(0) => break,
                Ok(count) => {
                    bytes.try_reserve(count).map_err(|_| {
                        KitError::integration("file content could not be allocated")
                    })?;
                    bytes.extend_from_slice(&chunk[..count]);
                }
                Err(error) if error == rustix::io::Errno::INTR => {}
                Err(error) => return Err(self.filesystem_error(error)),
            }
        }
        FileRead::from_bytes(self.relative_path, bytes)
    }

    #[cfg(any(target_os = "linux", target_os = "macos"))]
    fn filesystem_error(&self, error: rustix::io::Errno) -> KitError {
        if error == rustix::io::Errno::NOENT {
            KitError::not_found("file path was not found")
        } else if error == rustix::io::Errno::LOOP {
            KitError::invalid_input("file path contains a symbolic link")
        } else {
            KitError::integration("contained file read failed")
        }
    }

    #[cfg(any(target_os = "linux", target_os = "macos"))]
    fn descendant_error(&self, error: rustix::io::Errno) -> KitError {
        if error == rustix::io::Errno::LOOP || error == rustix::io::Errno::NOTDIR {
            KitError::invalid_input("file path contains an invalid path component")
        } else {
            self.filesystem_error(error)
        }
    }
}