fix(#108): expose les tools MCP autorisées dès le démarrage de session Codex

This commit is contained in:
2026-07-29 18:19:38 +02:00
parent 2c3a46e690
commit af6b76935c
9 changed files with 251 additions and 89 deletions

View File

@ -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":"{}"}}]}}]}"#;