Files
IdeA/crates/infrastructure/src/store/mcp_tool_permission.rs
Blomios 5175589aa5 feat(backend): domaine + catalogue et store durable des permissions tools MCP (#82 lot B1)
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>
2026-07-18 22:45:32 +02:00

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()))
}
}