cratesの整理

This commit is contained in:
2026-04-11 02:48:50 +09:00
parent cff082ff3a
commit 4c3f81b4fa
34 changed files with 2524 additions and 669 deletions
+168
View File
@@ -0,0 +1,168 @@
mod scope;
pub use scope::Scope;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
/// Declarative configuration for a Pod.
///
/// Parsed from a TOML manifest file. Describes the provider, model,
/// system prompt, and optional directory scope.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PodManifest {
pub pod: PodMeta,
pub provider: ProviderConfig,
pub worker: WorkerManifest,
#[serde(default)]
pub scope: Option<ScopeConfig>,
}
/// Pod metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PodMeta {
pub name: String,
}
/// LLM provider configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
pub kind: ProviderKind,
pub model: String,
/// Environment variable name holding the API key.
#[serde(default)]
pub api_key_env: Option<String>,
/// Custom base URL for the provider API.
#[serde(default)]
pub base_url: Option<String>,
}
/// Supported LLM providers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProviderKind {
Anthropic,
Openai,
Gemini,
Ollama,
}
/// Worker-level configuration embedded in the manifest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerManifest {
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub temperature: Option<f32>,
}
/// Directory scope configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScopeConfig {
pub root: PathBuf,
}
impl PodManifest {
/// Parse a manifest from a TOML string.
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
toml::from_str(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_minimal_manifest() {
let toml = r#"
[pod]
name = "test-agent"
[provider]
kind = "anthropic"
model = "claude-sonnet-4-20250514"
[worker]
"#;
let manifest = PodManifest::from_toml(toml).unwrap();
assert_eq!(manifest.pod.name, "test-agent");
assert_eq!(manifest.provider.kind, ProviderKind::Anthropic);
assert_eq!(manifest.provider.model, "claude-sonnet-4-20250514");
assert!(manifest.provider.api_key_env.is_none());
assert!(manifest.scope.is_none());
assert!(manifest.worker.system_prompt.is_none());
}
#[test]
fn parse_full_manifest() {
let toml = r#"
[pod]
name = "code-reviewer"
[provider]
kind = "anthropic"
model = "claude-sonnet-4-20250514"
api_key_env = "ANTHROPIC_API_KEY"
[worker]
system_prompt = "You are a code reviewer."
max_tokens = 4096
temperature = 0.3
[scope]
root = "./src"
"#;
let manifest = PodManifest::from_toml(toml).unwrap();
assert_eq!(manifest.pod.name, "code-reviewer");
assert_eq!(
manifest.provider.api_key_env.as_deref(),
Some("ANTHROPIC_API_KEY")
);
assert_eq!(
manifest.worker.system_prompt.as_deref(),
Some("You are a code reviewer.")
);
assert_eq!(manifest.worker.max_tokens, Some(4096));
assert_eq!(manifest.worker.temperature, Some(0.3));
assert_eq!(
manifest.scope.as_ref().unwrap().root,
PathBuf::from("./src")
);
}
#[test]
fn parse_ollama_no_api_key() {
let toml = r#"
[pod]
name = "local-agent"
[provider]
kind = "ollama"
model = "llama3"
[worker]
"#;
let manifest = PodManifest::from_toml(toml).unwrap();
assert_eq!(manifest.provider.kind, ProviderKind::Ollama);
assert!(manifest.provider.api_key_env.is_none());
}
#[test]
fn reject_unknown_provider() {
let toml = r#"
[pod]
name = "test"
[provider]
kind = "unknown_provider"
model = "x"
[worker]
"#;
assert!(PodManifest::from_toml(toml).is_err());
}
}
+101
View File
@@ -0,0 +1,101 @@
use std::path::{Path, PathBuf};
/// Directory scope constraining a Pod's write access.
///
/// Read access is unrestricted — only write operations are checked against the scope.
#[derive(Debug, Clone)]
pub struct Scope {
root: PathBuf,
}
impl Scope {
/// Create a new scope rooted at the given directory.
///
/// The path is canonicalized to resolve symlinks and relative components.
pub fn new(root: impl Into<PathBuf>) -> std::io::Result<Self> {
let root = root.into().canonicalize()?;
Ok(Self { root })
}
/// The root directory of this scope.
pub fn root(&self) -> &Path {
&self.root
}
/// Check whether `path` falls within this scope.
///
/// The path is canonicalized before comparison.
pub fn contains(&self, path: &Path) -> bool {
match path.canonicalize() {
Ok(canonical) => canonical.starts_with(&self.root),
Err(_) => {
// Path doesn't exist yet — check the parent directory instead.
// This handles write_file to a new file inside the scope.
match path.parent().and_then(|p| p.canonicalize().ok()) {
Some(parent) => parent.starts_with(&self.root),
None => false,
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn contains_file_inside_scope() {
let dir = TempDir::new().unwrap();
let scope = Scope::new(dir.path()).unwrap();
let file = dir.path().join("test.txt");
fs::write(&file, "hello").unwrap();
assert!(scope.contains(&file));
}
#[test]
fn rejects_file_outside_scope() {
let dir = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let scope = Scope::new(dir.path()).unwrap();
let file = outside.path().join("test.txt");
fs::write(&file, "hello").unwrap();
assert!(!scope.contains(&file));
}
#[test]
fn contains_new_file_in_existing_parent() {
let dir = TempDir::new().unwrap();
let scope = Scope::new(dir.path()).unwrap();
// File doesn't exist yet, but parent dir is inside scope
let new_file = dir.path().join("new.txt");
assert!(scope.contains(&new_file));
}
#[test]
fn contains_nested_directory() {
let dir = TempDir::new().unwrap();
let nested = dir.path().join("a/b/c");
fs::create_dir_all(&nested).unwrap();
let scope = Scope::new(dir.path()).unwrap();
let file = nested.join("test.txt");
assert!(scope.contains(&file));
}
#[test]
fn rejects_traversal_attack() {
let dir = TempDir::new().unwrap();
let scope = Scope::new(dir.path()).unwrap();
let traversal = dir.path().join("../../../etc/passwd");
assert!(!scope.contains(&traversal));
}
}