agent-core-runtime 0.6.0

Core runtime for agent-core - LLM orchestration, tools, and permissions (no TUI dependencies)
Documentation
//! Grant structure combining target and permission level.
//!
//! A Grant is the fundamental unit of the permission system, representing
//! the tuple `(Target, Level)` - what resource is being accessed and how.

use super::{GrantTarget, PermissionLevel};
use serde::{Deserialize, Serialize};
use std::time::Instant;

/// A permission grant combining a target and permission level.
///
/// Grants are the core building blocks of the permission system. Each grant
/// specifies:
/// - What can be accessed (the target)
/// - What level of access is permitted (the level)
/// - When the grant expires (optional)
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Grant {
    /// What this grant applies to (path, domain, or command).
    pub target: GrantTarget,
    /// The permission level granted.
    pub level: PermissionLevel,
    /// Optional expiration time. If `None`, grant lasts for the session.
    #[serde(skip)]
    pub expires: Option<Instant>,
}

impl Grant {
    /// Creates a new grant with the given target and level.
    ///
    /// The grant has no expiration (session-scoped).
    pub fn new(target: GrantTarget, level: PermissionLevel) -> Self {
        Self {
            target,
            level,
            expires: None,
        }
    }

    /// Creates a new grant with an expiration time.
    pub fn with_expiration(target: GrantTarget, level: PermissionLevel, expires: Instant) -> Self {
        Self {
            target,
            level,
            expires: Some(expires),
        }
    }

    /// Creates a read grant for a path.
    pub fn read_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
        Self::new(GrantTarget::path(path, recursive), PermissionLevel::Read)
    }

    /// Creates a write grant for a path.
    pub fn write_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
        Self::new(GrantTarget::path(path, recursive), PermissionLevel::Write)
    }

    /// Creates an execute grant for a path.
    pub fn execute_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
        Self::new(GrantTarget::path(path, recursive), PermissionLevel::Execute)
    }

    /// Creates an admin grant for a path.
    pub fn admin_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
        Self::new(GrantTarget::path(path, recursive), PermissionLevel::Admin)
    }

    /// Creates a grant for a network domain.
    pub fn domain(pattern: impl Into<String>, level: PermissionLevel) -> Self {
        Self::new(GrantTarget::domain(pattern), level)
    }

    /// Creates a grant for a shell command pattern.
    pub fn command(pattern: impl Into<String>, level: PermissionLevel) -> Self {
        Self::new(GrantTarget::command(pattern), level)
    }

    /// Checks if this grant satisfies a permission request.
    ///
    /// A grant satisfies a request if:
    /// 1. The grant's target covers the request's target
    /// 2. The grant's level satisfies the request's required level
    /// 3. The grant has not expired
    ///
    /// # Arguments
    /// * `request` - The permission request to check against
    ///
    /// # Returns
    /// `true` if this grant satisfies the request
    pub fn satisfies(&self, request: &PermissionRequest) -> bool {
        // Check expiration
        if let Some(expires) = self.expires {
            if Instant::now() >= expires {
                return false;
            }
        }

        // Check target coverage
        if !self.target.covers(&request.target) {
            return false;
        }

        // Check level hierarchy
        self.level.satisfies(request.required_level)
    }

    /// Checks if this grant has expired.
    pub fn is_expired(&self) -> bool {
        self.expires.map_or(false, |e| Instant::now() >= e)
    }

    /// Returns a display-friendly description of this grant.
    pub fn description(&self) -> String {
        format!("[{}] {}", self.level, self.target)
    }
}

impl std::fmt::Display for Grant {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.description())
    }
}

/// A request for permission to perform an operation.
///
/// Permission requests are generated by tools when they need access to
/// resources. The request specifies:
/// - What target is being accessed
/// - What level of access is needed
/// - A human-readable description of the operation
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PermissionRequest {
    /// Unique identifier for this request.
    pub id: String,
    /// The target being accessed.
    pub target: GrantTarget,
    /// The required permission level.
    pub required_level: PermissionLevel,
    /// Human-readable description of the operation.
    pub description: String,
    /// Optional reason explaining why this access is needed.
    pub reason: Option<String>,
    /// The tool that generated this request.
    pub tool_name: Option<String>,
}

impl PermissionRequest {
    /// Creates a new permission request.
    pub fn new(
        id: impl Into<String>,
        target: GrantTarget,
        required_level: PermissionLevel,
        description: impl Into<String>,
    ) -> Self {
        Self {
            id: id.into(),
            target,
            required_level,
            description: description.into(),
            reason: None,
            tool_name: None,
        }
    }

    /// Sets the reason for this request.
    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
        self.reason = Some(reason.into());
        self
    }

    /// Sets the tool name for this request.
    pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
        self.tool_name = Some(tool_name.into());
        self
    }

    /// Creates a file read request.
    pub fn file_read(id: impl Into<String>, path: impl Into<std::path::PathBuf>) -> Self {
        let path = path.into();
        let description = format!("Read file: {}", path.display());
        Self::new(
            id,
            GrantTarget::path(path, false),
            PermissionLevel::Read,
            description,
        )
    }

    /// Creates a file write request.
    pub fn file_write(id: impl Into<String>, path: impl Into<std::path::PathBuf>) -> Self {
        let path = path.into();
        let description = format!("Write file: {}", path.display());
        Self::new(
            id,
            GrantTarget::path(path, false),
            PermissionLevel::Write,
            description,
        )
    }

    /// Creates a directory read request.
    pub fn directory_read(
        id: impl Into<String>,
        path: impl Into<std::path::PathBuf>,
        recursive: bool,
    ) -> Self {
        let path = path.into();
        let description = if recursive {
            format!("Read directory (recursive): {}", path.display())
        } else {
            format!("Read directory: {}", path.display())
        };
        Self::new(
            id,
            GrantTarget::path(path, recursive),
            PermissionLevel::Read,
            description,
        )
    }

    /// Creates a command execution request.
    pub fn command_execute(id: impl Into<String>, command: impl Into<String>) -> Self {
        let command = command.into();
        let description = format!("Execute command: {}", command);
        Self::new(
            id,
            GrantTarget::command(command),
            PermissionLevel::Execute,
            description,
        )
    }

    /// Creates a network request.
    pub fn network_access(
        id: impl Into<String>,
        domain: impl Into<String>,
        level: PermissionLevel,
    ) -> Self {
        let domain = domain.into();
        let description = format!("Access domain: {}", domain);
        Self::new(id, GrantTarget::domain(domain), level, description)
    }
}

impl std::fmt::Display for PermissionRequest {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[{}] {}", self.required_level, self.description)
    }
}

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

    mod grant_tests {
        use super::*;

        #[test]
        fn test_grant_satisfies_same_level() {
            let grant = Grant::read_path("/project/src", true);
            let request = PermissionRequest::file_read("1", "/project/src/main.rs");
            assert!(grant.satisfies(&request));
        }

        #[test]
        fn test_grant_satisfies_higher_level() {
            let grant = Grant::write_path("/project/src", true);
            let request = PermissionRequest::file_read("1", "/project/src/main.rs");
            assert!(grant.satisfies(&request));
        }

        #[test]
        fn test_grant_fails_lower_level() {
            let grant = Grant::read_path("/project/src", true);
            let request = PermissionRequest::file_write("1", "/project/src/main.rs");
            assert!(!grant.satisfies(&request));
        }

        #[test]
        fn test_grant_fails_wrong_path() {
            let grant = Grant::write_path("/project/src", true);
            let request = PermissionRequest::file_write("1", "/other/file.rs");
            assert!(!grant.satisfies(&request));
        }

        #[test]
        fn test_grant_fails_non_recursive() {
            let grant = Grant::read_path("/project/src", false);
            let request = PermissionRequest::file_read("1", "/project/src/utils/mod.rs");
            assert!(!grant.satisfies(&request));
        }

        #[test]
        fn test_admin_grant_satisfies_all_levels() {
            let grant = Grant::admin_path("/project", true);

            let read_request = PermissionRequest::file_read("1", "/project/src/main.rs");
            let write_request = PermissionRequest::file_write("2", "/project/src/main.rs");

            assert!(grant.satisfies(&read_request));
            assert!(grant.satisfies(&write_request));
        }

        #[test]
        fn test_domain_grant() {
            let grant = Grant::domain("*.github.com", PermissionLevel::Read);
            let request =
                PermissionRequest::network_access("1", "api.github.com", PermissionLevel::Read);
            assert!(grant.satisfies(&request));
        }

        #[test]
        fn test_command_grant() {
            let grant = Grant::command("git *", PermissionLevel::Execute);
            let request = PermissionRequest::command_execute("1", "git status");
            assert!(grant.satisfies(&request));
        }

        #[test]
        fn test_expired_grant() {
            use std::time::Duration;
            let expired = Instant::now() - Duration::from_secs(1);
            let grant =
                Grant::with_expiration(GrantTarget::path("/project", true), PermissionLevel::Read, expired);

            let request = PermissionRequest::file_read("1", "/project/file.rs");
            assert!(!grant.satisfies(&request));
        }

        #[test]
        fn test_grant_description() {
            let grant = Grant::write_path("/project/src", true);
            let desc = grant.description();
            assert!(desc.contains("Write"));
            assert!(desc.contains("/project/src"));
        }
    }

    mod request_tests {
        use super::*;

        #[test]
        fn test_file_read_request() {
            let request = PermissionRequest::file_read("test-id", "/path/to/file.rs");
            assert_eq!(request.id, "test-id");
            assert_eq!(request.required_level, PermissionLevel::Read);
            assert!(request.description.contains("Read file"));
        }

        #[test]
        fn test_file_write_request() {
            let request = PermissionRequest::file_write("test-id", "/path/to/file.rs");
            assert_eq!(request.required_level, PermissionLevel::Write);
            assert!(request.description.contains("Write file"));
        }

        #[test]
        fn test_request_with_reason() {
            let request = PermissionRequest::file_read("1", "/file.rs")
                .with_reason("Need to analyze the code");
            assert_eq!(request.reason, Some("Need to analyze the code".to_string()));
        }

        #[test]
        fn test_request_with_tool() {
            let request = PermissionRequest::file_read("1", "/file.rs").with_tool("read_file");
            assert_eq!(request.tool_name, Some("read_file".to_string()));
        }
    }

    mod serialization_tests {
        use super::*;

        #[test]
        fn test_grant_serialization() {
            let grant = Grant::write_path("/project/src", true);
            let json = serde_json::to_string(&grant).unwrap();

            let deserialized: Grant = serde_json::from_str(&json).unwrap();
            assert_eq!(deserialized.target, grant.target);
            assert_eq!(deserialized.level, grant.level);
        }

        #[test]
        fn test_request_serialization() {
            let request =
                PermissionRequest::file_read("test-id", "/path/to/file.rs").with_reason("testing");
            let json = serde_json::to_string(&request).unwrap();

            let deserialized: PermissionRequest = serde_json::from_str(&json).unwrap();
            assert_eq!(deserialized.id, request.id);
            assert_eq!(deserialized.reason, request.reason);
        }
    }
}