Align message-board tools with the multi-agent namespace (#47028)

## Why

Message-board tools hardcoded the `collaboration` namespace and used a different namespace description from the multi-agent tools. This ignored custom namespace configuration and caused conflicts under strict tool collision checking.

## What changed

- Use `multi_agent_v2.tool_namespace` and the shared multi-agent namespace description for message-board tools, resolving the namespace at runtime startup.
- Support unnamespaced tool specifications when no namespace is supplied.
- Remove hardcoded `collaboration` prefixes from message-board notifications.

## Testing

Add app-server integration coverage for the default `collaboration` namespace and a configured `agents` namespace with strict collision checking enabled. Verify that both tool families are advertised together and that `get_channels` executes through the installed extension.

GitOrigin-RevId: fef7be51ea2a84bbdb6d3491606db442ed4e6ea3
This commit is contained in:
jif
2026-09-21 13:27:39 +00:00
committed by copyberry
parent 23a6b705ba
commit 8a62cc4dc1
11 changed files with 194 additions and 43 deletions
@@ -0,0 +1,98 @@
//! Board tools share MAv2 namespace metadata through the production installation path.
use anyhow::Context;
use app_test_support::MockResponsesConfig;
use app_test_support::TestAppServer;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::UserInput;
use codex_features::Feature;
use core_test_support::responses;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use tempfile::TempDir;
use tokio::time::Duration;
use tokio::time::timeout;
#[test_case::test_case(None, "collaboration"; "default_namespace")]
#[test_case::test_case(Some("agents"), "agents"; "configured_namespace")]
#[tokio::test]
async fn board_tools_share_multi_agent_namespace(
configured_namespace: Option<&str>,
namespace: &str,
) -> anyhow::Result<()> {
let server = responses::start_mock_server().await;
let mock = responses::mount_sse_sequence(
&server,
vec![
responses::sse(vec![
responses::ev_function_call_with_namespace(
"list-channels",
namespace,
"get_channels",
"{}",
),
responses::ev_completed("response-1"),
]),
responses::sse(vec![
responses::ev_assistant_message("done", "Done."),
responses::ev_completed("response-2"),
]),
],
)
.await;
let codex_home = TempDir::new()?;
let namespace_config = configured_namespace
.map(|name| format!("tool_namespace = {name:?}"))
.unwrap_or_default();
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::AgentMessageBoard)
.disable_feature(Feature::CodeMode)
.disable_feature(Feature::CodeModeOnly)
.with_extra_config(&format!(
"[features.multi_agent_v2]\nenabled = true\n{namespace_config}\n\
[features.tool_registry]\nerror_on_tool_collisions = true"
))
.write(codex_home.path())?;
let mut app = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
let thread = app.start_thread(ThreadStartParams::default()).await?.thread;
let request = app
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
input: vec![UserInput::Text {
text: "List the shared board channels.".into(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
let _: TurnStartResponse = app.read_response(request).await?;
timeout(
Duration::from_secs(60),
app.read_stream_until_notification_message("turn/completed"),
)
.await??;
let requests = mock.requests();
assert_eq!(requests.len(), 2);
// Strict collision checking rejects different descriptions before sampling.
// Both tool families must also be advertised under the configured name.
for name in ["spawn_agent", "get_channels", "post"] {
assert!(requests[0].tool_by_name(namespace, name).is_some());
}
let result: Value = serde_json::from_str(
&requests[1]
.function_call_output_text("list-channels")
.context("board tool must execute through the installed extension")?,
)?;
assert_eq!(
result,
json!({"results": [], "n_returned": 0, "has_more": false, "next_cursor": null})
);
Ok(())
}
@@ -2,6 +2,7 @@ mod account;
#[path = "account_system_proxy_tests.rs"]
mod account_system_proxy;
mod account_thread_usage;
mod agent_message_board;
mod analytics;
mod app_installed;
mod app_list;