Skip to main content

agentspec_provider/
ir.rs

1use serde::{Deserialize, Serialize};
2
3#[cfg(feature = "local")]
4use crate::local::LocalBackend;
5
6/// A capability that can be allowed or denied in a sandbox.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub enum Capability {
9    /// Read files from the filesystem.
10    ReadFile,
11    /// Write/edit files.
12    WriteFile,
13    /// Read-only git operations (log, diff, show, status, blame, etc.).
14    GitReadOnly,
15    /// Shell commands matching a pattern.
16    ShellCommand { pattern: String },
17    /// Web/network access.
18    Network,
19    /// Custom capability (provider-specific escape hatch).
20    Custom(String),
21}
22
23/// Sandbox configuration — what the AI agent is allowed to do.
24///
25/// Denied capabilities override allowed ones.
26/// An empty/None sandbox means no restrictions.
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct Sandbox {
29    /// Capabilities explicitly allowed.
30    pub allowed: Vec<Capability>,
31    /// Capabilities explicitly denied (overrides allowed).
32    pub denied: Vec<Capability>,
33}
34
35/// Provider-agnostic configuration for resolving an AI backend.
36#[derive(Debug, Clone, Default)]
37pub struct ProviderConfig {
38    /// Preferred backend (None = auto-detect).
39    #[cfg(feature = "local")]
40    pub backend: Option<LocalBackend>,
41    /// Model override.
42    pub model: Option<String>,
43    /// Max budget in USD (providers that support it).
44    pub budget: Option<f64>,
45    /// Sandbox restrictions.
46    pub sandbox: Option<Sandbox>,
47    /// Enable debug output.
48    pub debug: bool,
49}
50
51/// Read-only git subcommands shared across all providers.
52pub(crate) const GIT_READONLY_COMMANDS: &[&str] = &[
53    "diff", "log", "show", "status", "ls-files", "rev-parse", "branch", "cat-file", "rev-list",
54    "shortlog", "blame",
55];
56
57/// Internal trait for translating IR sandbox → provider-native arguments.
58pub(crate) trait SandboxTranslator {
59    /// Convert allowed capabilities into provider-native tool entries.
60    fn translate_allowed(&self, capabilities: &[Capability]) -> Vec<String>;
61
62    /// Build the full sandbox arguments from a Sandbox, filtering out denied capabilities.
63    fn translate_sandbox(&self, sandbox: &Sandbox) -> Vec<String> {
64        let allowed: Vec<_> = sandbox
65            .allowed
66            .iter()
67            .filter(|cap| !sandbox.denied.contains(cap))
68            .cloned()
69            .collect();
70        self.translate_allowed(&allowed)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn sandbox_default_is_empty() {
80        let sandbox = Sandbox::default();
81        assert!(sandbox.allowed.is_empty());
82        assert!(sandbox.denied.is_empty());
83    }
84
85    #[test]
86    fn provider_config_default() {
87        let config = ProviderConfig::default();
88        assert!(config.model.is_none());
89        assert!(config.budget.is_none());
90        assert!(config.sandbox.is_none());
91        assert!(!config.debug);
92    }
93
94    #[test]
95    fn capability_equality() {
96        assert_eq!(Capability::ReadFile, Capability::ReadFile);
97        assert_ne!(Capability::ReadFile, Capability::WriteFile);
98        assert_eq!(
99            Capability::ShellCommand {
100                pattern: "ls".into()
101            },
102            Capability::ShellCommand {
103                pattern: "ls".into()
104            }
105        );
106        assert_ne!(
107            Capability::ShellCommand {
108                pattern: "ls".into()
109            },
110            Capability::ShellCommand {
111                pattern: "cat".into()
112            }
113        );
114    }
115}