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:
@ -13,7 +13,7 @@ use domain::ports::{
|
|||||||
AgentToolPolicyStore, McpToolPermissionStore, ProjectStore, ToolInvocationError, ToolInvoker,
|
AgentToolPolicyStore, McpToolPermissionStore, ProjectStore, ToolInvocationError, ToolInvoker,
|
||||||
ToolSpec,
|
ToolSpec,
|
||||||
};
|
};
|
||||||
use domain::{AgentId, AgentToolPolicy, IssueRef, McpToolPolicy, Project};
|
use domain::{AgentToolPolicy, IssueRef, McpToolPolicy, Project};
|
||||||
use infrastructure::{TemplateToolProvider, TicketToolProvider};
|
use infrastructure::{TemplateToolProvider, TicketToolProvider};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@ -62,6 +62,22 @@ impl ToolInvoker for LateBoundOpenAiToolInvoker {
|
|||||||
.map_or_else(Vec::new, |inner| inner.tools())
|
.map_or_else(Vec::new, |inner| inner.tools())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn tools_for_context(
|
||||||
|
&self,
|
||||||
|
project_root: &str,
|
||||||
|
requester: &str,
|
||||||
|
) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||||
|
let inner = self
|
||||||
|
.inner
|
||||||
|
.lock()
|
||||||
|
.expect("mutex sain")
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ToolInvocationError::Execution("ToolInvoker OpenAI non initialisé".to_owned())
|
||||||
|
})?;
|
||||||
|
inner.tools_for_context(project_root, requester).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
||||||
let inner = self
|
let inner = self
|
||||||
.inner
|
.inner
|
||||||
@ -102,14 +118,38 @@ impl ToolInvoker for AppOpenAiToolInvoker {
|
|||||||
fn tools(&self) -> Vec<ToolSpec> {
|
fn tools(&self) -> Vec<ToolSpec> {
|
||||||
infrastructure::orchestrator::mcp::catalogue()
|
infrastructure::orchestrator::mcp::catalogue()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tool| ToolSpec {
|
.map(tool_def_to_spec)
|
||||||
name: tool.name.to_owned(),
|
|
||||||
description: tool.description.to_owned(),
|
|
||||||
input_schema: tool.input_schema,
|
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn tools_for_context(
|
||||||
|
&self,
|
||||||
|
project_root: &str,
|
||||||
|
requester: &str,
|
||||||
|
) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||||
|
let ephemeral_policy = self.policies.get_policy(requester);
|
||||||
|
let project = self.find_project(project_root).await?;
|
||||||
|
let surface = infrastructure::orchestrator::mcp::policy::resolve_effective_tool_surface(
|
||||||
|
Some(self.mcp_tool_permissions.as_ref()),
|
||||||
|
&project,
|
||||||
|
requester,
|
||||||
|
ephemeral_policy.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
ToolInvocationError::Execution(format!("failed to resolve MCP tool permissions: {e}"))
|
||||||
|
})?;
|
||||||
|
Ok(
|
||||||
|
infrastructure::orchestrator::mcp::policy::effective_tool_catalogue(
|
||||||
|
ephemeral_policy.as_ref(),
|
||||||
|
&surface,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(tool_def_to_spec)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
||||||
let value: Value = serde_json::from_str(args_json)
|
let value: Value = serde_json::from_str(args_json)
|
||||||
.map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?;
|
.map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?;
|
||||||
@ -140,18 +180,7 @@ impl ToolInvoker for AppOpenAiToolInvoker {
|
|||||||
if let Some(policy) = &ephemeral_policy {
|
if let Some(policy) = &ephemeral_policy {
|
||||||
enforce_policy(policy, &requester, name, &value)?;
|
enforce_policy(policy, &requester, name, &value)?;
|
||||||
}
|
}
|
||||||
let project = self
|
let project = self.find_project(&project_root).await?;
|
||||||
.projects
|
|
||||||
.list_projects()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ToolInvocationError::Execution(e.to_string()))?
|
|
||||||
.into_iter()
|
|
||||||
.find(|project| project.root.as_str() == project_root)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ToolInvocationError::Execution(format!(
|
|
||||||
"projet introuvable pour root `{project_root}`"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
enforce_durable_tool_policy(
|
enforce_durable_tool_policy(
|
||||||
self.mcp_tool_permissions.as_ref(),
|
self.mcp_tool_permissions.as_ref(),
|
||||||
&project,
|
&project,
|
||||||
@ -209,6 +238,30 @@ impl ToolInvoker for AppOpenAiToolInvoker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AppOpenAiToolInvoker {
|
||||||
|
async fn find_project(&self, project_root: &str) -> Result<Project, ToolInvocationError> {
|
||||||
|
self.projects
|
||||||
|
.list_projects()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ToolInvocationError::Execution(e.to_string()))?
|
||||||
|
.into_iter()
|
||||||
|
.find(|project| project.root.as_str() == project_root)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ToolInvocationError::Execution(format!(
|
||||||
|
"projet introuvable pour root `{project_root}`"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_def_to_spec(tool: infrastructure::orchestrator::mcp::ToolDef) -> ToolSpec {
|
||||||
|
ToolSpec {
|
||||||
|
name: tool.name.to_owned(),
|
||||||
|
description: tool.description.to_owned(),
|
||||||
|
input_schema: tool.input_schema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn enforce_durable_tool_policy(
|
async fn enforce_durable_tool_policy(
|
||||||
store: &dyn McpToolPermissionStore,
|
store: &dyn McpToolPermissionStore,
|
||||||
project: &Project,
|
project: &Project,
|
||||||
@ -239,38 +292,17 @@ async fn durable_tool_policy(
|
|||||||
ephemeral_policy: Option<&AgentToolPolicy>,
|
ephemeral_policy: Option<&AgentToolPolicy>,
|
||||||
requester: &str,
|
requester: &str,
|
||||||
) -> Result<Option<McpToolPolicy>, ToolInvocationError> {
|
) -> Result<Option<McpToolPolicy>, ToolInvocationError> {
|
||||||
let known_tools = infrastructure::orchestrator::mcp::tools::classified_tool_names();
|
infrastructure::orchestrator::mcp::policy::resolve_effective_tool_surface(
|
||||||
let policy = if let Some(agent_id) = requester_agent_id(requester) {
|
Some(store),
|
||||||
let doc = store
|
project,
|
||||||
.load_mcp_tool_permissions(project)
|
requester,
|
||||||
.await
|
ephemeral_policy,
|
||||||
.map_err(|e| {
|
)
|
||||||
ToolInvocationError::Execution(format!("failed to load MCP tool permissions: {e}"))
|
.await
|
||||||
})?;
|
.map(|surface| surface.durable_policy)
|
||||||
doc.effective_policy(
|
.map_err(|e| {
|
||||||
agent_id,
|
ToolInvocationError::Execution(format!("failed to resolve MCP tool permissions: {e}"))
|
||||||
infrastructure::orchestrator::mcp::tools::READ_ONLY_TOOLS,
|
})
|
||||||
&known_tools,
|
|
||||||
)
|
|
||||||
.map_err(|e| ToolInvocationError::Execution(format!("invalid MCP tool permissions: {e}")))?
|
|
||||||
} else if requester.is_empty() || requester == "mcp" || ephemeral_policy.is_none() {
|
|
||||||
McpToolPolicy::read_only(
|
|
||||||
infrastructure::orchestrator::mcp::tools::READ_ONLY_TOOLS,
|
|
||||||
&known_tools,
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
ToolInvocationError::Execution(format!("invalid read-only MCP tool fallback: {e}"))
|
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
Ok(Some(policy))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requester_agent_id(requester: &str) -> Option<AgentId> {
|
|
||||||
uuid::Uuid::parse_str(requester)
|
|
||||||
.ok()
|
|
||||||
.map(AgentId::from_uuid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enforce_policy(
|
fn enforce_policy(
|
||||||
@ -643,6 +675,46 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(temp);
|
let _ = std::fs::remove_dir_all(temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn openai_tools_list_matches_durable_agent_policy() {
|
||||||
|
let temp = std::env::temp_dir().join(format!(
|
||||||
|
"idea-app-tauri-openai-mcp-tools-list-{}",
|
||||||
|
Uuid::new_v4()
|
||||||
|
));
|
||||||
|
let core = backend::BackendCore::build(temp.clone());
|
||||||
|
let agent = AgentId::from_uuid(Uuid::from_u128(84));
|
||||||
|
let invoker = AppOpenAiToolInvoker::new(
|
||||||
|
Arc::clone(&core.orchestrator_service),
|
||||||
|
Arc::new(FakeProjects::with(project())) as Arc<dyn ProjectStore>,
|
||||||
|
Arc::new(FakePolicies::default()),
|
||||||
|
mcp_permissions(allow_doc(
|
||||||
|
agent,
|
||||||
|
&["idea_memory_read", "idea_skill_read", "idea_create_skill"],
|
||||||
|
)),
|
||||||
|
Arc::new(FakeTicketTools::default()),
|
||||||
|
Arc::new(FakeTemplateTools::default()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let names = invoker
|
||||||
|
.tools_for_context("/tmp/project", &agent.to_string())
|
||||||
|
.await
|
||||||
|
.expect("tools list resolves")
|
||||||
|
.into_iter()
|
||||||
|
.map(|tool| tool.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
names,
|
||||||
|
vec![
|
||||||
|
"idea_memory_read".to_owned(),
|
||||||
|
"idea_skill_read".to_owned(),
|
||||||
|
"idea_create_skill".to_owned(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(temp);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn openai_durable_agent_override_allows_explicit_write_tool() {
|
async fn openai_durable_agent_override_allows_explicit_write_tool() {
|
||||||
let temp = std::env::temp_dir().join(format!(
|
let temp = std::env::temp_dir().join(format!(
|
||||||
|
|||||||
@ -4967,59 +4967,14 @@ mod mcp_serve_peer_tests {
|
|||||||
assert_eq!(resp["id"], json!(1));
|
assert_eq!(resp["id"], json!(1));
|
||||||
let tools = resp["result"]["tools"].as_array().expect("tools array");
|
let tools = resp["result"]["tools"].as_array().expect("tools array");
|
||||||
let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
|
let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
|
||||||
for expected in [
|
let expected_names: Vec<&str> = infrastructure::orchestrator::mcp::catalogue()
|
||||||
"idea_list_agents",
|
.into_iter()
|
||||||
"idea_ask_agent",
|
.map(|tool| tool.name)
|
||||||
"idea_ask_agents",
|
.collect();
|
||||||
"idea_run_in_background",
|
|
||||||
"idea_launch_agent",
|
|
||||||
"idea_stop_agent",
|
|
||||||
"idea_update_context",
|
|
||||||
"idea_create_skill",
|
|
||||||
// FileGuard-mediated context/memory tools (cadrage C7).
|
|
||||||
"idea_context_read",
|
|
||||||
"idea_context_propose",
|
|
||||||
"idea_memory_read",
|
|
||||||
"idea_memory_write",
|
|
||||||
// Skill-awareness : lecture à la demande du corps d'un skill.
|
|
||||||
"idea_skill_read",
|
|
||||||
// Conversation inter-agent headless : réponse inline capturée depuis le Final.
|
|
||||||
"idea_ask_agent",
|
|
||||||
// Live-state (programme live-state, lot LS4).
|
|
||||||
"idea_workstate_read",
|
|
||||||
"idea_workstate_set",
|
|
||||||
// Public ticket tools (Issue domain).
|
|
||||||
"idea_ticket_create",
|
|
||||||
"idea_ticket_read",
|
|
||||||
"idea_ticket_list",
|
|
||||||
"idea_ticket_update",
|
|
||||||
"idea_ticket_update_status",
|
|
||||||
"idea_ticket_update_priority",
|
|
||||||
"idea_ticket_bulk_update_status",
|
|
||||||
"idea_ticket_bulk_update_priority",
|
|
||||||
"idea_ticket_bulk_delete",
|
|
||||||
"idea_ticket_read_carnet",
|
|
||||||
"idea_ticket_update_carnet",
|
|
||||||
"idea_ticket_link",
|
|
||||||
"idea_ticket_unlink",
|
|
||||||
"idea_sprint_list",
|
|
||||||
// Public template tools.
|
|
||||||
"idea_template_list",
|
|
||||||
"idea_template_read",
|
|
||||||
"idea_template_create",
|
|
||||||
"idea_template_update",
|
|
||||||
"idea_template_delete",
|
|
||||||
] {
|
|
||||||
assert!(
|
|
||||||
names.contains(&expected),
|
|
||||||
"missing tool {expected}; got {names:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert!(!names.contains(&"idea_reply"));
|
assert!(!names.contains(&"idea_reply"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tools.len(),
|
names, expected_names,
|
||||||
34,
|
"tools/list must expose exactly the canonical MCP catalogue"
|
||||||
"exactly the thirty-four exposed idea_* tools; got {names:?}"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
drop(client); // EOF ⇒ serve loop ends
|
drop(client); // EOF ⇒ serve loop ends
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use domain::ports::{
|
|||||||
AgentToolPolicyStore, McpToolPermissionStore, ProjectStore, ToolInvocationError, ToolInvoker,
|
AgentToolPolicyStore, McpToolPermissionStore, ProjectStore, ToolInvocationError, ToolInvoker,
|
||||||
ToolSpec,
|
ToolSpec,
|
||||||
};
|
};
|
||||||
use domain::{AgentId, AgentToolPolicy, IssueRef, McpToolPolicy, Project};
|
use domain::{AgentToolPolicy, IssueRef, McpToolPolicy, Project};
|
||||||
use infrastructure::{TemplateToolProvider, TicketToolProvider};
|
use infrastructure::{TemplateToolProvider, TicketToolProvider};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@ -62,6 +62,22 @@ impl ToolInvoker for LateBoundOpenAiToolInvoker {
|
|||||||
.map_or_else(Vec::new, |inner| inner.tools())
|
.map_or_else(Vec::new, |inner| inner.tools())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn tools_for_context(
|
||||||
|
&self,
|
||||||
|
project_root: &str,
|
||||||
|
requester: &str,
|
||||||
|
) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||||
|
let inner = self
|
||||||
|
.inner
|
||||||
|
.lock()
|
||||||
|
.expect("mutex sain")
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ToolInvocationError::Execution("ToolInvoker OpenAI non initialisé".to_owned())
|
||||||
|
})?;
|
||||||
|
inner.tools_for_context(project_root, requester).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
||||||
let inner = self
|
let inner = self
|
||||||
.inner
|
.inner
|
||||||
@ -102,14 +118,38 @@ impl ToolInvoker for AppOpenAiToolInvoker {
|
|||||||
fn tools(&self) -> Vec<ToolSpec> {
|
fn tools(&self) -> Vec<ToolSpec> {
|
||||||
infrastructure::orchestrator::mcp::catalogue()
|
infrastructure::orchestrator::mcp::catalogue()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tool| ToolSpec {
|
.map(tool_def_to_spec)
|
||||||
name: tool.name.to_owned(),
|
|
||||||
description: tool.description.to_owned(),
|
|
||||||
input_schema: tool.input_schema,
|
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn tools_for_context(
|
||||||
|
&self,
|
||||||
|
project_root: &str,
|
||||||
|
requester: &str,
|
||||||
|
) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||||
|
let ephemeral_policy = self.policies.get_policy(requester);
|
||||||
|
let project = self.find_project(project_root).await?;
|
||||||
|
let surface = infrastructure::orchestrator::mcp::policy::resolve_effective_tool_surface(
|
||||||
|
Some(self.mcp_tool_permissions.as_ref()),
|
||||||
|
&project,
|
||||||
|
requester,
|
||||||
|
ephemeral_policy.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
ToolInvocationError::Execution(format!("failed to resolve MCP tool permissions: {e}"))
|
||||||
|
})?;
|
||||||
|
Ok(
|
||||||
|
infrastructure::orchestrator::mcp::policy::effective_tool_catalogue(
|
||||||
|
ephemeral_policy.as_ref(),
|
||||||
|
&surface,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(tool_def_to_spec)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
||||||
let value: Value = serde_json::from_str(args_json)
|
let value: Value = serde_json::from_str(args_json)
|
||||||
.map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?;
|
.map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?;
|
||||||
@ -140,18 +180,7 @@ impl ToolInvoker for AppOpenAiToolInvoker {
|
|||||||
if let Some(policy) = &ephemeral_policy {
|
if let Some(policy) = &ephemeral_policy {
|
||||||
enforce_policy(policy, &requester, name, &value)?;
|
enforce_policy(policy, &requester, name, &value)?;
|
||||||
}
|
}
|
||||||
let project = self
|
let project = self.find_project(&project_root).await?;
|
||||||
.projects
|
|
||||||
.list_projects()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ToolInvocationError::Execution(e.to_string()))?
|
|
||||||
.into_iter()
|
|
||||||
.find(|project| project.root.as_str() == project_root)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ToolInvocationError::Execution(format!(
|
|
||||||
"projet introuvable pour root `{project_root}`"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
enforce_durable_tool_policy(
|
enforce_durable_tool_policy(
|
||||||
self.mcp_tool_permissions.as_ref(),
|
self.mcp_tool_permissions.as_ref(),
|
||||||
&project,
|
&project,
|
||||||
@ -209,6 +238,30 @@ impl ToolInvoker for AppOpenAiToolInvoker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AppOpenAiToolInvoker {
|
||||||
|
async fn find_project(&self, project_root: &str) -> Result<Project, ToolInvocationError> {
|
||||||
|
self.projects
|
||||||
|
.list_projects()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ToolInvocationError::Execution(e.to_string()))?
|
||||||
|
.into_iter()
|
||||||
|
.find(|project| project.root.as_str() == project_root)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ToolInvocationError::Execution(format!(
|
||||||
|
"projet introuvable pour root `{project_root}`"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_def_to_spec(tool: infrastructure::orchestrator::mcp::ToolDef) -> ToolSpec {
|
||||||
|
ToolSpec {
|
||||||
|
name: tool.name.to_owned(),
|
||||||
|
description: tool.description.to_owned(),
|
||||||
|
input_schema: tool.input_schema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn enforce_durable_tool_policy(
|
async fn enforce_durable_tool_policy(
|
||||||
store: &dyn McpToolPermissionStore,
|
store: &dyn McpToolPermissionStore,
|
||||||
project: &Project,
|
project: &Project,
|
||||||
@ -239,38 +292,17 @@ async fn durable_tool_policy(
|
|||||||
ephemeral_policy: Option<&AgentToolPolicy>,
|
ephemeral_policy: Option<&AgentToolPolicy>,
|
||||||
requester: &str,
|
requester: &str,
|
||||||
) -> Result<Option<McpToolPolicy>, ToolInvocationError> {
|
) -> Result<Option<McpToolPolicy>, ToolInvocationError> {
|
||||||
let known_tools = infrastructure::orchestrator::mcp::tools::classified_tool_names();
|
infrastructure::orchestrator::mcp::policy::resolve_effective_tool_surface(
|
||||||
let policy = if let Some(agent_id) = requester_agent_id(requester) {
|
Some(store),
|
||||||
let doc = store
|
project,
|
||||||
.load_mcp_tool_permissions(project)
|
requester,
|
||||||
.await
|
ephemeral_policy,
|
||||||
.map_err(|e| {
|
)
|
||||||
ToolInvocationError::Execution(format!("failed to load MCP tool permissions: {e}"))
|
.await
|
||||||
})?;
|
.map(|surface| surface.durable_policy)
|
||||||
doc.effective_policy(
|
.map_err(|e| {
|
||||||
agent_id,
|
ToolInvocationError::Execution(format!("failed to resolve MCP tool permissions: {e}"))
|
||||||
infrastructure::orchestrator::mcp::tools::READ_ONLY_TOOLS,
|
})
|
||||||
&known_tools,
|
|
||||||
)
|
|
||||||
.map_err(|e| ToolInvocationError::Execution(format!("invalid MCP tool permissions: {e}")))?
|
|
||||||
} else if requester.is_empty() || requester == "mcp" || ephemeral_policy.is_none() {
|
|
||||||
McpToolPolicy::read_only(
|
|
||||||
infrastructure::orchestrator::mcp::tools::READ_ONLY_TOOLS,
|
|
||||||
&known_tools,
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
ToolInvocationError::Execution(format!("invalid read-only MCP tool fallback: {e}"))
|
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
Ok(Some(policy))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requester_agent_id(requester: &str) -> Option<AgentId> {
|
|
||||||
uuid::Uuid::parse_str(requester)
|
|
||||||
.ok()
|
|
||||||
.map(AgentId::from_uuid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -796,6 +828,42 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(temp);
|
let _ = std::fs::remove_dir_all(temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn openai_tools_list_matches_durable_agent_policy() {
|
||||||
|
let temp =
|
||||||
|
std::env::temp_dir().join(format!("idea-openai-mcp-tools-list-{}", Uuid::new_v4()));
|
||||||
|
let core = crate::BackendCore::build(temp.clone());
|
||||||
|
let agent = AgentId::from_uuid(Uuid::from_u128(182));
|
||||||
|
let invoker = AppOpenAiToolInvoker::new(
|
||||||
|
Arc::clone(&core.orchestrator_service),
|
||||||
|
Arc::new(FakeProjects::with(project())) as Arc<dyn ProjectStore>,
|
||||||
|
Arc::new(FakePolicies::default()),
|
||||||
|
mcp_permissions(allow_doc(
|
||||||
|
agent,
|
||||||
|
&["idea_ticket_list", "idea_ticket_update_carnet"],
|
||||||
|
)),
|
||||||
|
Arc::new(FakeTicketTools::default()),
|
||||||
|
Arc::new(FakeTemplateTools::default()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let tools = invoker
|
||||||
|
.tools_for_context("/tmp/project", &agent.to_string())
|
||||||
|
.await
|
||||||
|
.expect("tools list should resolve durable policy");
|
||||||
|
let names = tools
|
||||||
|
.iter()
|
||||||
|
.map(|tool| tool.name.as_str())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert!(names.contains(&"idea_ticket_list"));
|
||||||
|
assert!(names.contains(&"idea_ticket_update_carnet"));
|
||||||
|
assert!(!names.contains(&"idea_ask_agent"));
|
||||||
|
assert!(!names.contains(&"idea_ticket_update"));
|
||||||
|
assert!(!names.contains(&"idea_run_in_background"));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(temp);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn openai_durable_agent_override_allows_explicit_write_tool() {
|
async fn openai_durable_agent_override_allows_explicit_write_tool() {
|
||||||
let temp =
|
let temp =
|
||||||
|
|||||||
@ -660,6 +660,29 @@ pub trait ToolInvoker: Send + Sync {
|
|||||||
/// Liste des outils exposés au modèle.
|
/// Liste des outils exposés au modèle.
|
||||||
fn tools(&self) -> Vec<ToolSpec>;
|
fn tools(&self) -> Vec<ToolSpec>;
|
||||||
|
|
||||||
|
/// Liste des outils exposés dans le contexte déjà lié par l'adapter appelant.
|
||||||
|
///
|
||||||
|
/// Les invokers simples peuvent conserver le comportement historique via
|
||||||
|
/// [`Self::tools`]. Les wrappers qui portent déjà l'identité requester et le
|
||||||
|
/// projet peuvent surcharger cette méthode pour refléter les permissions
|
||||||
|
/// effectives au moment où les outils sont exposés au modèle.
|
||||||
|
async fn tools_for_bound_context(&self) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||||
|
Ok(self.tools())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liste des outils exposés pour un projet et un requester explicites.
|
||||||
|
///
|
||||||
|
/// Sert aux adapters qui savent résoudre la policy durable à partir du root
|
||||||
|
/// projet et de l'identité agent avant d'injecter les tools dans une requête
|
||||||
|
/// OpenAI-compatible.
|
||||||
|
async fn tools_for_context(
|
||||||
|
&self,
|
||||||
|
_project_root: &str,
|
||||||
|
_requester: &str,
|
||||||
|
) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||||
|
Ok(self.tools())
|
||||||
|
}
|
||||||
|
|
||||||
/// Appelle un outil avec ses arguments JSON bruts.
|
/// Appelle un outil avec ses arguments JSON bruts.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|||||||
@ -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::collections::HashMap;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use domain::AgentToolPolicy;
|
use domain::ports::McpToolPermissionStore;
|
||||||
use domain::AgentToolPolicyStore;
|
use domain::{AgentId, AgentToolPolicy, AgentToolPolicyStore, McpToolPolicy, Project, StoreError};
|
||||||
|
|
||||||
|
use super::tools::{self, ToolDef};
|
||||||
|
|
||||||
/// Stores per-requester MCP tool policies for live assistant sessions.
|
/// Stores per-requester MCP tool policies for live assistant sessions.
|
||||||
#[derive(Default)]
|
#[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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use domain::IssueRef;
|
use domain::IssueRef;
|
||||||
|
|||||||
@ -347,25 +347,20 @@ impl McpServer {
|
|||||||
/// The `tools/list` result: the catalogue as MCP tool descriptors.
|
/// The `tools/list` result: the catalogue as MCP tool descriptors.
|
||||||
async fn tools_list_result(&self) -> Result<Value, JsonRpcError> {
|
async fn tools_list_result(&self) -> Result<Value, JsonRpcError> {
|
||||||
let ephemeral_policy = self.ephemeral_tool_policy();
|
let ephemeral_policy = self.ephemeral_tool_policy();
|
||||||
let durable_policy = self.durable_tool_policy().await?;
|
let surface = self
|
||||||
let tools: Vec<Value> = tools::catalogue()
|
.effective_tool_surface(ephemeral_policy.as_ref())
|
||||||
.into_iter()
|
.await?;
|
||||||
.filter(|t| {
|
let tools: Vec<Value> =
|
||||||
ephemeral_policy
|
super::policy::effective_tool_catalogue(ephemeral_policy.as_ref(), &surface)
|
||||||
.as_ref()
|
.into_iter()
|
||||||
.map_or(true, |policy| policy.permits(t.name))
|
.map(|t| {
|
||||||
&& durable_policy
|
json!({
|
||||||
.as_ref()
|
"name": t.name,
|
||||||
.map_or(true, |policy| policy.permits(t.name))
|
"description": t.description,
|
||||||
})
|
"inputSchema": t.input_schema,
|
||||||
.map(|t| {
|
})
|
||||||
json!({
|
|
||||||
"name": t.name,
|
|
||||||
"description": t.description,
|
|
||||||
"inputSchema": t.input_schema,
|
|
||||||
})
|
})
|
||||||
})
|
.collect();
|
||||||
.collect();
|
|
||||||
Ok(json!({ "tools": tools }))
|
Ok(json!({ "tools": tools }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,53 +371,30 @@ impl McpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn durable_tool_policy(&self) -> Result<Option<McpToolPolicy>, JsonRpcError> {
|
async fn durable_tool_policy(&self) -> Result<Option<McpToolPolicy>, JsonRpcError> {
|
||||||
let Some(store) = &self.mcp_tool_permissions else {
|
let ephemeral_policy = self.ephemeral_tool_policy();
|
||||||
return Ok(None);
|
Ok(self
|
||||||
};
|
.effective_tool_surface(ephemeral_policy.as_ref())
|
||||||
|
.await?
|
||||||
let known_tools = tools::classified_tool_names();
|
.durable_policy)
|
||||||
let policy = if let Some(agent_id) = self.requester_agent_id() {
|
|
||||||
let doc = store
|
|
||||||
.load_mcp_tool_permissions(&self.project)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
JsonRpcError::new(
|
|
||||||
error_codes::INTERNAL_ERROR,
|
|
||||||
format!("failed to load MCP tool permissions: {e}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
doc.effective_policy(agent_id, tools::READ_ONLY_TOOLS, &known_tools)
|
|
||||||
.map_err(|e| {
|
|
||||||
JsonRpcError::new(
|
|
||||||
error_codes::INTERNAL_ERROR,
|
|
||||||
format!("invalid MCP tool permissions: {e}"),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
} else if self.requester.is_empty()
|
|
||||||
|| self.requester == "mcp"
|
|
||||||
|| self.ephemeral_tool_policy().is_none()
|
|
||||||
{
|
|
||||||
// Anonymous/legacy peers cannot be mapped to an agent override. Fail
|
|
||||||
// closed to the canonical read-only policy. Non-agent requesters with an
|
|
||||||
// ephemeral policy (ticket assistants) are governed by that narrower,
|
|
||||||
// session-scoped policy instead of the durable per-agent store.
|
|
||||||
McpToolPolicy::read_only(tools::READ_ONLY_TOOLS, &known_tools).map_err(|e| {
|
|
||||||
JsonRpcError::new(
|
|
||||||
error_codes::INTERNAL_ERROR,
|
|
||||||
format!("invalid read-only MCP tool fallback: {e}"),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(policy))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn requester_agent_id(&self) -> Option<AgentId> {
|
async fn effective_tool_surface(
|
||||||
uuid::Uuid::parse_str(&self.requester)
|
&self,
|
||||||
.ok()
|
ephemeral_policy: Option<&AgentToolPolicy>,
|
||||||
.map(AgentId::from_uuid)
|
) -> Result<super::policy::EffectiveToolSurface, JsonRpcError> {
|
||||||
|
super::policy::resolve_effective_tool_surface(
|
||||||
|
self.mcp_tool_permissions.as_deref(),
|
||||||
|
&self.project,
|
||||||
|
&self.requester,
|
||||||
|
ephemeral_policy,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
JsonRpcError::new(
|
||||||
|
error_codes::INTERNAL_ERROR,
|
||||||
|
format!("failed to resolve MCP tool permissions: {e}"),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enforce_durable_tool_policy(
|
fn enforce_durable_tool_policy(
|
||||||
|
|||||||
@ -41,6 +41,12 @@ impl ToolInvoker for ProjectScopedToolInvoker {
|
|||||||
self.inner.tools()
|
self.inner.tools()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn tools_for_bound_context(&self) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||||
|
self.inner
|
||||||
|
.tools_for_context(&self.project_root, &self.requester)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
||||||
let mut value: Value = serde_json::from_str(args_json)
|
let mut value: Value = serde_json::from_str(args_json)
|
||||||
.map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?;
|
.map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?;
|
||||||
@ -257,6 +263,7 @@ mod tests {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct RecordingToolInvoker {
|
struct RecordingToolInvoker {
|
||||||
call: Mutex<Option<(String, String)>>,
|
call: Mutex<Option<(String, String)>>,
|
||||||
|
tools_context: Mutex<Option<(String, String)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -265,6 +272,20 @@ mod tests {
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn tools_for_context(
|
||||||
|
&self,
|
||||||
|
project_root: &str,
|
||||||
|
requester: &str,
|
||||||
|
) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||||
|
*self.tools_context.lock().unwrap() =
|
||||||
|
Some((project_root.to_owned(), requester.to_owned()));
|
||||||
|
Ok(vec![ToolSpec {
|
||||||
|
name: "idea_memory_read".to_owned(),
|
||||||
|
description: "Read memory".to_owned(),
|
||||||
|
input_schema: json!({"type":"object"}),
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
||||||
*self.call.lock().unwrap() = Some((name.to_owned(), args_json.to_owned()));
|
*self.call.lock().unwrap() = Some((name.to_owned(), args_json.to_owned()));
|
||||||
Ok("ok".to_owned())
|
Ok("ok".to_owned())
|
||||||
@ -299,6 +320,28 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn project_scoped_tool_invoker_filters_tools_with_bound_context() {
|
||||||
|
let recorder = Arc::new(RecordingToolInvoker::default());
|
||||||
|
let invoker = ProjectScopedToolInvoker {
|
||||||
|
inner: recorder.clone(),
|
||||||
|
project_root: "/project/root".to_owned(),
|
||||||
|
requester: "agent-1".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tools = invoker
|
||||||
|
.tools_for_bound_context()
|
||||||
|
.await
|
||||||
|
.expect("tools resolve");
|
||||||
|
|
||||||
|
assert_eq!(tools.len(), 1);
|
||||||
|
assert_eq!(tools[0].name, "idea_memory_read");
|
||||||
|
assert_eq!(
|
||||||
|
recorder.tools_context.lock().unwrap().clone(),
|
||||||
|
Some(("/project/root".to_owned(), "agent-1".to_owned()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fallback_requester_uses_cwd_file_name_only_when_explicit_identity_absent() {
|
fn fallback_requester_uses_cwd_file_name_only_when_explicit_identity_absent() {
|
||||||
assert_eq!(fallback_requester_from_cwd("/tmp/run/7"), "7");
|
assert_eq!(fallback_requester_from_cwd("/tmp/run/7"), "7");
|
||||||
|
|||||||
@ -281,7 +281,7 @@ impl OpenAiCompatibleSession {
|
|||||||
send_tap(&tap, &ReplyEvent::Heartbeat);
|
send_tap(&tap, &ReplyEvent::Heartbeat);
|
||||||
for iteration in 0..=self.config.effective_max_tool_iterations() {
|
for iteration in 0..=self.config.effective_max_tool_iterations() {
|
||||||
let transcript = self.transcript.lock().expect("mutex sain").clone();
|
let transcript = self.transcript.lock().expect("mutex sain").clone();
|
||||||
let tools = self.effective_tools();
|
let tools = self.effective_tools().await?;
|
||||||
let response = self.post_chat(&transcript, &tools, true).await;
|
let response = self.post_chat(&transcript, &tools, true).await;
|
||||||
let response = match response {
|
let response = match response {
|
||||||
Ok(response) => response,
|
Ok(response) => response,
|
||||||
@ -330,13 +330,17 @@ impl OpenAiCompatibleSession {
|
|||||||
unreachable!("loop returns at max_tool_iterations");
|
unreachable!("loop returns at max_tool_iterations");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn effective_tools(&self) -> Vec<ToolSpec> {
|
async fn effective_tools(&self) -> Result<Vec<ToolSpec>, AgentSessionError> {
|
||||||
if *self.tools_disabled.lock().expect("mutex sain") {
|
if *self.tools_disabled.lock().expect("mutex sain") {
|
||||||
return Vec::new();
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
match &self.tool_invoker {
|
||||||
|
Some(invoker) => invoker
|
||||||
|
.tools_for_bound_context()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AgentSessionError::Start(format!("résolution tools: {e}"))),
|
||||||
|
None => Ok(Vec::new()),
|
||||||
}
|
}
|
||||||
self.tool_invoker
|
|
||||||
.as_ref()
|
|
||||||
.map_or_else(Vec::new, |invoker| invoker.tools())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_chat(
|
async fn post_chat(
|
||||||
|
|||||||
Reference in New Issue
Block a user