feat: add plugin execution

This commit is contained in:
2025-09-24 14:05:26 +03:00
parent 67d62aa479
commit b6181315a9
4 changed files with 450 additions and 8 deletions

View File

@@ -1,18 +1,18 @@
use std::fs::read_to_string;
use color_eyre::eyre::Result;
use glob::Pattern;
use serde::{Deserialize, Serialize};
use std::{fs::read_to_string, path::Path};
#[derive(Debug, Serialize, Deserialize)]
pub struct FsCapability {
pub read: Vec<String>, // Glob patter for read
pub write: Vec<String>, // Stub for now
pub read: Option<Vec<String>>, // Glob patter for read
pub write: Option<Vec<String>>, // Stub for now
}
#[derive(Debug, Serialize, Deserialize)]
pub enum Capability {
Fs(FsCapability),
// TODO: add Net, Cpu
// TODO: add Net, Cpu, etc
}
#[derive(Debug, Serialize, Deserialize)]
@@ -29,6 +29,44 @@ pub struct CapabilityManifest {
// TODO: add signature
}
pub struct HostState {
pub manifest: CapabilityManifest,
}
impl HostState {
#[inline]
#[must_use]
pub const fn new(manifest: CapabilityManifest) -> Self {
Self { manifest }
}
/// Simulate "plugin execution": check if path is allowed via FS read cap.
/// Returns `true` if allowed, `false` if denied.
#[must_use]
pub fn execute_plugin<P: AsRef<Path>>(&self, path: P) -> bool {
let Some(fs_cap) = &self.manifest.capabilities.fs else {
return false;
};
let Some(read_patterns) = &fs_cap.read else {
return false;
};
let path_str = path.as_ref().to_string_lossy();
read_patterns.iter().any(|pattern| {
Pattern::new(pattern)
.map(|p| p.matches(&path_str))
.unwrap_or(false)
})
}
}
/// Loads a capability manifest from a JSON file.
///
/// # Errors
///
/// - bubbles up `std::fs::read_to_string` and `serde_json::from_str` errors;
pub fn load_manifest(path: &str) -> Result<CapabilityManifest> {
let json_str = read_to_string(path)?;
let manifest = serde_json::from_str(&json_str)?;
@@ -49,7 +87,20 @@ mod tests {
let caps = manifest.capabilities;
let fs_cap = assert_some!(caps.fs);
assert_eq!(fs_cap.read, vec!["./workspace/*"]);
assert!(fs_cap.write.is_empty());
let read_patterns = assert_some!(fs_cap.read);
assert_eq!(read_patterns, vec!["./workspace/*"]);
let write_patterns = assert_some!(fs_cap.write);
assert!(write_patterns.is_empty());
}
#[test]
fn host_enforcement() {
let manifest = load_manifest("examples/manifest.json").expect("Load failed");
let host = HostState::new(manifest);
// Allowed: matches ./workspace/*
assert!(host.execute_plugin("./workspace/config.toml"));
// Disallowed: outside pattern
assert!(!host.execute_plugin("/etc/passwd"))
}
}