feat(mcp): synchronise les permissions MCP avec les tools réellement exposés
Les permissions déclarées ne reflétaient pas toujours les tools effectivement exposés aux agents (contexte requester/projet lié après coup). Ajoute ToolInvoker::tools_for_bound_context/tools_for_context pour exposer la liste effective au moment de l'injection dans une requête OpenAI-compatible, et propage le calcul dans la policy MCP, le serveur, la factory de session et l'adapter openai_compat. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -1,10 +1,12 @@
|
||||
//! In-memory MCP tool policy registry.
|
||||
//! MCP tool policy helpers and in-memory live policy registry.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use domain::AgentToolPolicy;
|
||||
use domain::AgentToolPolicyStore;
|
||||
use domain::ports::McpToolPermissionStore;
|
||||
use domain::{AgentId, AgentToolPolicy, AgentToolPolicyStore, McpToolPolicy, Project, StoreError};
|
||||
|
||||
use super::tools::{self, ToolDef};
|
||||
|
||||
/// Stores per-requester MCP tool policies for live assistant sessions.
|
||||
#[derive(Default)]
|
||||
@ -53,6 +55,106 @@ impl AgentToolPolicyStore for ToolPolicyRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Effective MCP tool surface resolved for one requester.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EffectiveToolSurface {
|
||||
/// Durable MCP policy when one applies.
|
||||
pub durable_policy: Option<McpToolPolicy>,
|
||||
/// Whether the read-only fallback was used because the requester could not be
|
||||
/// resolved to an agent policy.
|
||||
pub used_read_only_fallback: bool,
|
||||
}
|
||||
|
||||
impl EffectiveToolSurface {
|
||||
/// Returns whether `tool` is allowed by the durable policy, when present.
|
||||
#[must_use]
|
||||
pub fn permits_durable(&self, tool: &str) -> bool {
|
||||
self.durable_policy
|
||||
.as_ref()
|
||||
.map_or(true, |policy| policy.permits(tool))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the durable MCP tool policy for the given requester.
|
||||
///
|
||||
/// This is the shared source of truth for the surfaces that expose IdeA tools to
|
||||
/// agents. Agent UUID requesters use `.ideai/mcp-tool-permissions.json`;
|
||||
/// anonymous/legacy requesters and non-agent requesters without an ephemeral
|
||||
/// policy fall back to the canonical read-only policy. Non-agent requesters with
|
||||
/// an ephemeral policy (ticket assistants) are intentionally governed only by
|
||||
/// that narrower session policy.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`StoreError`] on store load failure or invalid persisted policy.
|
||||
pub async fn resolve_effective_tool_surface(
|
||||
store: Option<&dyn McpToolPermissionStore>,
|
||||
project: &Project,
|
||||
requester: &str,
|
||||
ephemeral_policy: Option<&AgentToolPolicy>,
|
||||
) -> Result<EffectiveToolSurface, StoreError> {
|
||||
let Some(store) = store else {
|
||||
return Ok(EffectiveToolSurface {
|
||||
durable_policy: None,
|
||||
used_read_only_fallback: false,
|
||||
});
|
||||
};
|
||||
|
||||
let known_tools = tools::classified_tool_names();
|
||||
if let Some(agent_id) = requester_agent_id(requester) {
|
||||
let doc = store.load_mcp_tool_permissions(project).await?;
|
||||
let policy = doc
|
||||
.effective_policy(agent_id, tools::READ_ONLY_TOOLS, &known_tools)
|
||||
.map_err(|err| StoreError::Invalid(err.to_string()))?;
|
||||
return Ok(EffectiveToolSurface {
|
||||
durable_policy: Some(policy),
|
||||
used_read_only_fallback: false,
|
||||
});
|
||||
}
|
||||
|
||||
if requester.is_empty() || requester == "mcp" || ephemeral_policy.is_none() {
|
||||
application::diag!(
|
||||
"[mcp] unresolved requester `{}` uses read-only tool fallback",
|
||||
if requester.is_empty() {
|
||||
"mcp"
|
||||
} else {
|
||||
requester
|
||||
},
|
||||
);
|
||||
let policy = McpToolPolicy::read_only(tools::READ_ONLY_TOOLS, &known_tools)
|
||||
.map_err(|err| StoreError::Invalid(err.to_string()))?;
|
||||
return Ok(EffectiveToolSurface {
|
||||
durable_policy: Some(policy),
|
||||
used_read_only_fallback: true,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(EffectiveToolSurface {
|
||||
durable_policy: None,
|
||||
used_read_only_fallback: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Filters the canonical catalogue by the effective ephemeral + durable policies.
|
||||
#[must_use]
|
||||
pub fn effective_tool_catalogue(
|
||||
ephemeral_policy: Option<&AgentToolPolicy>,
|
||||
surface: &EffectiveToolSurface,
|
||||
) -> Vec<ToolDef> {
|
||||
tools::catalogue()
|
||||
.into_iter()
|
||||
.filter(|tool| {
|
||||
ephemeral_policy.map_or(true, |policy| policy.permits(tool.name))
|
||||
&& surface.permits_durable(tool.name)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn requester_agent_id(requester: &str) -> Option<AgentId> {
|
||||
uuid::Uuid::parse_str(requester)
|
||||
.ok()
|
||||
.map(AgentId::from_uuid)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use domain::IssueRef;
|
||||
|
||||
Reference in New Issue
Block a user