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",
54    "log",
55    "show",
56    "status",
57    "ls-files",
58    "rev-parse",
59    "branch",
60    "cat-file",
61    "rev-list",
62    "shortlog",
63    "blame",
64];
65
66/// Internal trait for translating IR sandbox → provider-native arguments.
67pub(crate) trait SandboxTranslator {
68    /// Convert allowed capabilities into provider-native tool entries.
69    fn translate_allowed(&self, capabilities: &[Capability]) -> Vec<String>;
70
71    /// Build the full sandbox arguments from a Sandbox, filtering out denied capabilities.
72    fn translate_sandbox(&self, sandbox: &Sandbox) -> Vec<String> {
73        let allowed: Vec<_> = sandbox
74            .allowed
75            .iter()
76            .filter(|cap| !sandbox.denied.contains(cap))
77            .cloned()
78            .collect();
79        self.translate_allowed(&allowed)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn sandbox_default_is_empty() {
89        let sandbox = Sandbox::default();
90        assert!(sandbox.allowed.is_empty());
91        assert!(sandbox.denied.is_empty());
92    }
93
94    #[test]
95    fn provider_config_default() {
96        let config = ProviderConfig::default();
97        assert!(config.model.is_none());
98        assert!(config.budget.is_none());
99        assert!(config.sandbox.is_none());
100        assert!(!config.debug);
101    }
102
103    #[test]
104    fn capability_equality() {
105        assert_eq!(Capability::ReadFile, Capability::ReadFile);
106        assert_ne!(Capability::ReadFile, Capability::WriteFile);
107        assert_eq!(
108            Capability::ShellCommand {
109                pattern: "ls".into()
110            },
111            Capability::ShellCommand {
112                pattern: "ls".into()
113            }
114        );
115        assert_ne!(
116            Capability::ShellCommand {
117                pattern: "ls".into()
118            },
119            Capability::ShellCommand {
120                pattern: "cat".into()
121            }
122        );
123    }
124}