Introduit le domaine mcp_tool_permissions (règles de permission par tool MCP) et étend le port de store correspondant. Le catalogue read/write des tools MCP (orchestrator/mcp/tools.rs) s'appuie désormais sur ces règles, et un store durable (mcp_tool_permission.rs) persiste les permissions au-delà d'une session. Lot B1 du ticket #82 : pose le socle domaine/store, l'enforcement suit en B2 sur la même branche. QA vert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
82 lines
2.7 KiB
Rust
82 lines
2.7 KiB
Rust
//! Filesystem-backed [`McpToolPermissionStore`] for project MCP tool permissions.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
|
|
use domain::ports::{FileSystem, FsError, McpToolPermissionStore, RemotePath, StoreError};
|
|
use domain::{Project, ProjectMcpToolPermissions};
|
|
|
|
use crate::orchestrator::mcp::tools;
|
|
|
|
const MCP_TOOL_PERMISSIONS_FILE: &str = "mcp-tool-permissions.json";
|
|
|
|
/// JSON-file implementation for `<project>/.ideai/mcp-tool-permissions.json`.
|
|
#[derive(Clone)]
|
|
pub struct FsMcpToolPermissionStore {
|
|
fs: Arc<dyn FileSystem>,
|
|
}
|
|
|
|
impl FsMcpToolPermissionStore {
|
|
/// Builds the store from an injected filesystem port.
|
|
#[must_use]
|
|
pub fn new(fs: Arc<dyn FileSystem>) -> Self {
|
|
Self { fs }
|
|
}
|
|
|
|
fn path(project: &Project) -> RemotePath {
|
|
let root = project.root.as_str().trim_end_matches(['/', '\\']);
|
|
RemotePath::new(format!("{root}/.ideai/{MCP_TOOL_PERMISSIONS_FILE}"))
|
|
}
|
|
|
|
async fn ensure_ideai(&self, project: &Project) -> Result<(), StoreError> {
|
|
let root = project.root.as_str().trim_end_matches(['/', '\\']);
|
|
self.fs
|
|
.create_dir_all(&RemotePath::new(format!("{root}/.ideai")))
|
|
.await
|
|
.map_err(|e| StoreError::Io(e.to_string()))
|
|
}
|
|
|
|
fn validate(permissions: &ProjectMcpToolPermissions) -> Result<(), StoreError> {
|
|
let known_tools = tools::classified_tool_names();
|
|
permissions
|
|
.validate(&known_tools)
|
|
.map_err(|e| StoreError::Serialization(e.to_string()))
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl McpToolPermissionStore for FsMcpToolPermissionStore {
|
|
async fn load_mcp_tool_permissions(
|
|
&self,
|
|
project: &Project,
|
|
) -> Result<ProjectMcpToolPermissions, StoreError> {
|
|
match self.fs.read(&Self::path(project)).await {
|
|
Ok(bytes) => {
|
|
let permissions = serde_json::from_slice::<ProjectMcpToolPermissions>(&bytes)
|
|
.map_err(|e| StoreError::Serialization(e.to_string()))?;
|
|
Self::validate(&permissions)?;
|
|
Ok(permissions)
|
|
}
|
|
Err(FsError::NotFound(_)) => Ok(ProjectMcpToolPermissions::default()),
|
|
Err(e) => Err(StoreError::Io(e.to_string())),
|
|
}
|
|
}
|
|
|
|
async fn save_mcp_tool_permissions(
|
|
&self,
|
|
project: &Project,
|
|
permissions: &ProjectMcpToolPermissions,
|
|
) -> Result<(), StoreError> {
|
|
Self::validate(permissions)?;
|
|
self.ensure_ideai(project).await?;
|
|
let mut bytes = serde_json::to_vec_pretty(permissions)
|
|
.map_err(|e| StoreError::Serialization(e.to_string()))?;
|
|
bytes.push(b'\n');
|
|
self.fs
|
|
.write(&Self::path(project), &bytes)
|
|
.await
|
|
.map_err(|e| StoreError::Io(e.to_string()))
|
|
}
|
|
}
|