fix(#108): expose les tools MCP autorisées dès le démarrage de session Codex
This commit is contained in:
@ -31,82 +31,11 @@
|
||||
"idea_template_create",
|
||||
"idea_template_update",
|
||||
"idea_template_delete",
|
||||
"idea_create_skill"
|
||||
"idea_create_skill",
|
||||
"idea_ask_agents"
|
||||
]
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "a6ced819-b893-4213-b003-9e9dc79b9641",
|
||||
"policy": {
|
||||
"allowedTools": [
|
||||
"idea_list_agents",
|
||||
"idea_context_read",
|
||||
"idea_memory_read",
|
||||
"idea_skill_read",
|
||||
"idea_workstate_read",
|
||||
"idea_ticket_read",
|
||||
"idea_ticket_list",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_sprint_list",
|
||||
"idea_template_list",
|
||||
"idea_template_read",
|
||||
"idea_ask_agent",
|
||||
"idea_launch_agent",
|
||||
"idea_stop_agent",
|
||||
"idea_update_context",
|
||||
"idea_context_propose",
|
||||
"idea_memory_write",
|
||||
"idea_workstate_set",
|
||||
"idea_create_skill",
|
||||
"idea_ticket_create",
|
||||
"idea_ticket_update",
|
||||
"idea_ticket_update_status",
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
"idea_template_create",
|
||||
"idea_template_update",
|
||||
"idea_template_delete"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "dce19c75-9669-4e45-b8de-9950025157da",
|
||||
"policy": {
|
||||
"allowedTools": [
|
||||
"idea_list_agents",
|
||||
"idea_context_read",
|
||||
"idea_memory_read",
|
||||
"idea_skill_read",
|
||||
"idea_workstate_read",
|
||||
"idea_ticket_read",
|
||||
"idea_ticket_list",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_sprint_list",
|
||||
"idea_template_list",
|
||||
"idea_template_read",
|
||||
"idea_ask_agent",
|
||||
"idea_launch_agent",
|
||||
"idea_stop_agent",
|
||||
"idea_update_context",
|
||||
"idea_context_propose",
|
||||
"idea_memory_write",
|
||||
"idea_workstate_set",
|
||||
"idea_create_skill",
|
||||
"idea_ticket_create",
|
||||
"idea_ticket_update",
|
||||
"idea_ticket_update_status",
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
"idea_template_create",
|
||||
"idea_template_update",
|
||||
"idea_template_delete"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "73c853d1-c0fd-463b-ad17-1d24fefa371f",
|
||||
"policy": {
|
||||
@ -358,6 +287,80 @@
|
||||
"idea_template_delete"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "a6ced819-b893-4213-b003-9e9dc79b9641",
|
||||
"policy": {
|
||||
"allowedTools": [
|
||||
"idea_list_agents",
|
||||
"idea_context_read",
|
||||
"idea_memory_read",
|
||||
"idea_skill_read",
|
||||
"idea_workstate_read",
|
||||
"idea_ticket_read",
|
||||
"idea_ticket_list",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_sprint_list",
|
||||
"idea_template_list",
|
||||
"idea_template_read",
|
||||
"idea_ask_agent",
|
||||
"idea_launch_agent",
|
||||
"idea_stop_agent",
|
||||
"idea_update_context",
|
||||
"idea_context_propose",
|
||||
"idea_memory_write",
|
||||
"idea_workstate_set",
|
||||
"idea_create_skill",
|
||||
"idea_ticket_create",
|
||||
"idea_ticket_update",
|
||||
"idea_ticket_update_status",
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
"idea_template_create",
|
||||
"idea_template_update",
|
||||
"idea_template_delete",
|
||||
"idea_run_in_background"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "dce19c75-9669-4e45-b8de-9950025157da",
|
||||
"policy": {
|
||||
"allowedTools": [
|
||||
"idea_list_agents",
|
||||
"idea_context_read",
|
||||
"idea_memory_read",
|
||||
"idea_skill_read",
|
||||
"idea_workstate_read",
|
||||
"idea_ticket_read",
|
||||
"idea_ticket_list",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_sprint_list",
|
||||
"idea_template_list",
|
||||
"idea_template_read",
|
||||
"idea_ask_agent",
|
||||
"idea_launch_agent",
|
||||
"idea_stop_agent",
|
||||
"idea_update_context",
|
||||
"idea_context_propose",
|
||||
"idea_memory_write",
|
||||
"idea_workstate_set",
|
||||
"idea_create_skill",
|
||||
"idea_ticket_create",
|
||||
"idea_ticket_update",
|
||||
"idea_ticket_update_status",
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
"idea_template_create",
|
||||
"idea_template_update",
|
||||
"idea_template_delete",
|
||||
"idea_run_in_background"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
issueRef: "#112"
|
||||
version: 3
|
||||
version: 4
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1785332227989
|
||||
updatedAt: 1785341341049
|
||||
---
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: "ff8e11d1-98f8-4c6c-b5d0-a8a087c1dbbc"
|
||||
number: 112
|
||||
title: "Pouvoir ajouter plusieurs tickets a la fois a un sprint"
|
||||
status: "open"
|
||||
status: "closed"
|
||||
priority: "medium"
|
||||
sprint: null
|
||||
links: []
|
||||
@ -11,7 +11,7 @@ attachments: []
|
||||
createdBy: {"kind":"user"}
|
||||
updatedBy: {"kind":"user"}
|
||||
createdAt: 1785332192429
|
||||
updatedAt: 1785332227989
|
||||
version: 3
|
||||
updatedAt: 1785341341049
|
||||
version: 4
|
||||
---
|
||||
Je veux qu'on ajoute la possibilité de set le sprint des tickets selectionnés grace a la selection multiple de ticket dans la liste des tickets.
|
||||
@ -1467,7 +1467,7 @@
|
||||
"issueRef": "#112",
|
||||
"path": "112",
|
||||
"title": "Pouvoir ajouter plusieurs tickets a la fois a un sprint",
|
||||
"status": "open",
|
||||
"status": "closed",
|
||||
"priority": "medium",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
@ -1476,7 +1476,7 @@
|
||||
"createdBy": {
|
||||
"kind": "user"
|
||||
},
|
||||
"updatedAt": 1785332227989
|
||||
"updatedAt": 1785341341049
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -597,6 +597,39 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn default_and_override_doc(
|
||||
agent: AgentId,
|
||||
project_default_tools: &[&str],
|
||||
override_tools: &[&str],
|
||||
) -> ProjectMcpToolPermissions {
|
||||
let known_tools = infrastructure::orchestrator::mcp::tools::classified_tool_names();
|
||||
ProjectMcpToolPermissions::new(
|
||||
Some(
|
||||
McpToolPolicy::new(
|
||||
project_default_tools
|
||||
.iter()
|
||||
.map(|tool| (*tool).to_owned())
|
||||
.collect(),
|
||||
&known_tools,
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
vec![AgentMcpToolPolicyOverride::new(
|
||||
agent,
|
||||
McpToolPolicy::new(
|
||||
override_tools
|
||||
.iter()
|
||||
.map(|tool| (*tool).to_owned())
|
||||
.collect(),
|
||||
&known_tools,
|
||||
)
|
||||
.unwrap(),
|
||||
)],
|
||||
&known_tools,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_tool_policy_rejects_tool_outside_allowlist() {
|
||||
let policies = FakePolicies::default();
|
||||
@ -864,6 +897,41 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(temp);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn openai_tools_list_keeps_project_default_tools_with_agent_override() {
|
||||
let temp = std::env::temp_dir().join(format!(
|
||||
"idea-openai-mcp-tools-list-override-{}",
|
||||
Uuid::new_v4()
|
||||
));
|
||||
let core = crate::BackendCore::build(temp.clone());
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(183));
|
||||
let invoker = AppOpenAiToolInvoker::new(
|
||||
Arc::clone(&core.orchestrator_service),
|
||||
Arc::new(FakeProjects::with(project())) as Arc<dyn ProjectStore>,
|
||||
Arc::new(FakePolicies::default()),
|
||||
mcp_permissions(default_and_override_doc(
|
||||
agent,
|
||||
&["idea_memory_read", "idea_run_in_background"],
|
||||
&["idea_memory_read"],
|
||||
)),
|
||||
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_eq!(names, vec!["idea_run_in_background", "idea_memory_read"]);
|
||||
|
||||
let _ = std::fs::remove_dir_all(temp);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn openai_durable_agent_override_allows_explicit_write_tool() {
|
||||
let temp =
|
||||
|
||||
@ -185,8 +185,8 @@ impl ProjectMcpToolPermissions {
|
||||
|
||||
/// Resolves the effective policy for `agent_id`.
|
||||
///
|
||||
/// Resolution order: agent override > project default > canonical read-only
|
||||
/// fallback.
|
||||
/// Resolution order: project default enriched by agent override > agent
|
||||
/// override > project default > canonical read-only fallback.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`McpToolPermissionError`] if the document or fallback set is invalid.
|
||||
@ -197,12 +197,24 @@ impl ProjectMcpToolPermissions {
|
||||
known_tools: &[&str],
|
||||
) -> Result<McpToolPolicy, McpToolPermissionError> {
|
||||
self.validate(known_tools)?;
|
||||
if let Some(override_) = self
|
||||
let override_policy = self
|
||||
.agents
|
||||
.iter()
|
||||
.find(|override_| override_.agent_id == agent_id)
|
||||
.map(|override_| &override_.policy);
|
||||
if let (Some(project_default), Some(override_policy)) =
|
||||
(&self.project_default, override_policy)
|
||||
{
|
||||
return Ok(override_.policy.clone());
|
||||
let mut allowed_tools = project_default.allowed_tools.clone();
|
||||
for tool in &override_policy.allowed_tools {
|
||||
if !allowed_tools.iter().any(|allowed| allowed == tool) {
|
||||
allowed_tools.push(tool.clone());
|
||||
}
|
||||
}
|
||||
return McpToolPolicy::new(allowed_tools, known_tools);
|
||||
}
|
||||
if let Some(override_policy) = override_policy {
|
||||
return Ok(override_policy.clone());
|
||||
}
|
||||
if let Some(policy) = &self.project_default {
|
||||
return Ok(policy.clone());
|
||||
@ -236,7 +248,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_override_replaces_project_default() {
|
||||
fn agent_override_extends_project_default() {
|
||||
let project_default =
|
||||
McpToolPolicy::new(vec!["idea_memory_read".to_owned()], KNOWN).unwrap();
|
||||
let override_policy =
|
||||
@ -250,8 +262,8 @@ mod tests {
|
||||
|
||||
let effective = doc.effective_policy(agent(7), READ_ONLY, KNOWN).unwrap();
|
||||
|
||||
assert!(effective.permits("idea_memory_read"));
|
||||
assert!(effective.permits("idea_memory_write"));
|
||||
assert!(!effective.permits("idea_memory_read"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -700,11 +700,27 @@ mod tests {
|
||||
#[derive(Default)]
|
||||
struct FakeInvoker {
|
||||
calls: AtomicUsize,
|
||||
tools: Mutex<Vec<ToolSpec>>,
|
||||
bound_tools: Mutex<Option<Vec<ToolSpec>>>,
|
||||
}
|
||||
|
||||
impl FakeInvoker {
|
||||
fn with_bound_tools(tools: Vec<ToolSpec>) -> Self {
|
||||
Self {
|
||||
calls: AtomicUsize::new(0),
|
||||
tools: Mutex::new(Vec::new()),
|
||||
bound_tools: Mutex::new(Some(tools)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ToolInvoker for FakeInvoker {
|
||||
fn tools(&self) -> Vec<ToolSpec> {
|
||||
let tools = self.tools.lock().expect("mutex sain");
|
||||
if !tools.is_empty() {
|
||||
return tools.clone();
|
||||
}
|
||||
vec![ToolSpec {
|
||||
name: "idea_echo".to_owned(),
|
||||
description: "Echo".to_owned(),
|
||||
@ -712,6 +728,15 @@ mod tests {
|
||||
}]
|
||||
}
|
||||
|
||||
async fn tools_for_bound_context(&self) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||
Ok(self
|
||||
.bound_tools
|
||||
.lock()
|
||||
.expect("mutex sain")
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.tools()))
|
||||
}
|
||||
|
||||
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
||||
self.calls.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(format!("{name}:{args_json}"))
|
||||
@ -829,6 +854,18 @@ mod tests {
|
||||
path
|
||||
}
|
||||
|
||||
fn tool(name: &str) -> ToolSpec {
|
||||
ToolSpec {
|
||||
name: name.to_owned(),
|
||||
description: format!("{name} description"),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn config(endpoint: String, max_tool_iterations: Option<u16>) -> HttpChatConfig {
|
||||
config_with_timeouts(endpoint, max_tool_iterations, 10_000, 1_000)
|
||||
}
|
||||
@ -1055,6 +1092,44 @@ mod tests {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn first_openai_request_uses_bound_tool_surface() {
|
||||
let (endpoint, bodies, handle) = http_server(vec![TestHttpResponse::ok(
|
||||
r#"{"choices":[{"message":{"role":"assistant","content":"ok"}}]}"#,
|
||||
)])
|
||||
.await;
|
||||
let invoker = Arc::new(FakeInvoker::with_bound_tools(vec![
|
||||
tool("idea_memory_read"),
|
||||
tool("idea_run_in_background"),
|
||||
]));
|
||||
let session = OpenAiCompatibleSession::new(
|
||||
SessionId::new_random(),
|
||||
config(endpoint, Some(1)),
|
||||
temp_run_dir("initial-bound-tools"),
|
||||
"# system",
|
||||
Some(invoker),
|
||||
)
|
||||
.expect("session");
|
||||
|
||||
let _ = drain(&session, "hello").await;
|
||||
|
||||
let bodies = bodies.lock().expect("mutex sain");
|
||||
let body: Value = serde_json::from_str(&bodies[0]).expect("request body JSON");
|
||||
let names = body["tools"]
|
||||
.as_array()
|
||||
.expect("initial request carries tools")
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
tool["function"]["name"]
|
||||
.as_str()
|
||||
.expect("tool function name")
|
||||
.to_owned()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(names, vec!["idea_memory_read", "idea_run_in_background"]);
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn max_tool_iterations_returns_single_degraded_final() {
|
||||
let tool = r#"{"choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"idea_echo","arguments":"{}"}}]}}]}"#;
|
||||
|
||||
@ -754,6 +754,9 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
"idea_ticket_bulk_delete",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_attachment_add",
|
||||
"idea_ticket_attachment_read",
|
||||
"idea_ticket_attachment_mark_summarized",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
"idea_sprint_list",
|
||||
@ -772,8 +775,8 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
assert!(!names.contains(&"idea_reply"));
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
34,
|
||||
"exactly the thirty-four exposed idea_* tools; got {names:?}"
|
||||
37,
|
||||
"exactly the thirty-seven exposed idea_* tools; got {names:?}"
|
||||
);
|
||||
|
||||
// Every tool advertises an object input schema.
|
||||
|
||||
@ -110,7 +110,8 @@ async fn save_then_load_roundtrips_project_default_and_agent_override() {
|
||||
.effective_policy(agent, READ_ONLY_TOOLS, &classified_tool_names())
|
||||
.unwrap();
|
||||
assert!(effective.permits("idea_ticket_update"));
|
||||
assert!(!effective.permits("idea_context_read"));
|
||||
assert!(effective.permits("idea_context_read"));
|
||||
assert!(effective.permits("idea_memory_read"));
|
||||
|
||||
let path = tmp.0.join(".ideai").join("mcp-tool-permissions.json");
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user