Merge origin/main into dev/ssetty/explicit-plugin-app-startup

This commit is contained in:
Sama Setty
2026-04-16 20:15:34 -07:00
759 changed files with 52454 additions and 18028 deletions

View File

@@ -22,6 +22,8 @@ const SERVER_VERSION: &str = "1.0.0";
const SEARCHABLE_TOOL_COUNT: usize = 100;
pub const CALENDAR_CREATE_EVENT_RESOURCE_URI: &str =
"connector://calendar/tools/calendar_create_event";
pub const CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI: &str =
"ui://widget/calendar-create-event.html";
const CALENDAR_LIST_EVENTS_RESOURCE_URI: &str = "connector://calendar/tools/calendar_list_events";
pub const DOCUMENT_EXTRACT_TEXT_RESOURCE_URI: &str =
"connector://calendar/tools/calendar_extract_text";
@@ -228,6 +230,7 @@ impl Respond for CodexAppsJsonRpcResponder {
"connector_id": CONNECTOR_ID,
"connector_name": self.connector_name.clone(),
"connector_description": self.connector_description.clone(),
"openai/outputTemplate": CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI,
"_codex_apps": {
"resource_uri": CALENDAR_CREATE_EVENT_RESOURCE_URI,
"contains_mcp_source": true,

View File

@@ -125,6 +125,10 @@ impl ResponsesRequest {
self.body_json().to_string().contains(&json_fragment)
}
pub fn tool_by_name(&self, namespace: &str, tool_name: &str) -> Option<Value> {
namespace_child_tool(&self.body_json(), namespace, tool_name).cloned()
}
pub fn instructions_text(&self) -> String {
self.body_json()["instructions"]
.as_str()
@@ -315,6 +319,31 @@ pub(crate) fn output_value_to_text(value: &Value) -> Option<String> {
}
}
pub fn namespace_child_tool<'a>(
body: &'a Value,
namespace: &str,
tool_name: &str,
) -> Option<&'a Value> {
let tools = body.get("tools")?.as_array()?;
for tool in tools {
if tool.get("name").and_then(Value::as_str) != Some(namespace)
|| tool.get("type").and_then(Value::as_str) != Some("namespace")
{
continue;
}
let child_tools = tool.get("tools")?.as_array()?;
if let Some(child_tool) = child_tools
.iter()
.find(|tool| tool.get("name").and_then(Value::as_str) == Some(tool_name))
{
return Some(child_tool);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
@@ -780,6 +809,24 @@ pub fn ev_function_call(call_id: &str, name: &str, arguments: &str) -> Value {
})
}
pub fn ev_function_call_with_namespace(
call_id: &str,
namespace: &str,
name: &str,
arguments: &str,
) -> Value {
serde_json::json!({
"type": "response.output_item.done",
"item": {
"type": "function_call",
"call_id": call_id,
"namespace": namespace,
"name": name,
"arguments": arguments
}
})
}
pub fn ev_tool_search_call(call_id: &str, arguments: &serde_json::Value) -> Value {
serde_json::json!({
"type": "response.output_item.done",

View File

@@ -7,6 +7,7 @@ use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
use tokio::sync::Mutex as TokioMutex;
use tokio::sync::Notify;
use tokio::sync::oneshot;
/// Streaming SSE chunk payload gated by a per-chunk signal.
@@ -20,6 +21,7 @@ pub struct StreamingSseChunk {
pub struct StreamingSseServer {
uri: String,
requests: Arc<TokioMutex<Vec<Vec<u8>>>>,
request_notify: Arc<Notify>,
shutdown: oneshot::Sender<()>,
task: tokio::task::JoinHandle<()>,
}
@@ -33,6 +35,15 @@ impl StreamingSseServer {
self.requests.lock().await.clone()
}
pub async fn wait_for_request_count(&self, count: usize) {
loop {
if self.requests.lock().await.len() >= count {
return;
}
self.request_notify.notified().await;
}
}
pub async fn shutdown(self) {
let _ = self.shutdown.send(());
let _ = self.task.await;
@@ -67,7 +78,9 @@ pub async fn start_streaming_sse_server(
completions: VecDeque::from(completion_senders),
}));
let requests = Arc::new(TokioMutex::new(Vec::new()));
let request_notify = Arc::new(Notify::new());
let requests_for_task = Arc::clone(&requests);
let request_notify_for_task = Arc::clone(&request_notify);
let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
let task = tokio::spawn(async move {
@@ -78,6 +91,7 @@ pub async fn start_streaming_sse_server(
let (mut stream, _) = accept_res.expect("accept streaming SSE connection");
let state = Arc::clone(&state);
let requests = Arc::clone(&requests_for_task);
let request_notify = Arc::clone(&request_notify_for_task);
tokio::spawn(async move {
let (request, body_prefix) = read_http_request(&mut stream).await;
let Some((method, path)) = parse_request_line(&request) else {
@@ -113,6 +127,7 @@ pub async fn start_streaming_sse_server(
}
};
requests.lock().await.push(body);
request_notify.notify_one();
let Some((chunks, completion)) = take_next_stream(&state).await else {
let _ = write_http_response(&mut stream, /*status*/ 500, "no responses queued", "text/plain").await;
return;
@@ -149,6 +164,7 @@ pub async fn start_streaming_sse_server(
StreamingSseServer {
uri,
requests,
request_notify,
shutdown: shutdown_tx,
task,
},

View File

@@ -8,7 +8,6 @@ use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use anyhow::Context;
use anyhow::Result;
@@ -44,7 +43,6 @@ use tempfile::TempDir;
use wiremock::MockServer;
use crate::PathBufExt;
use crate::RemoteEnvConfig;
use crate::TempDirExt;
use crate::get_remote_test_env;
use crate::load_default_config_for_test;
@@ -52,8 +50,8 @@ use crate::responses::WebSocketTestServer;
use crate::responses::output_value_to_text;
use crate::responses::start_mock_server;
use crate::streaming_sse::StreamingSseServer;
use crate::wait_for_event;
use crate::wait_for_event_match;
use crate::wait_for_event_with_timeout;
use wiremock::Match;
use wiremock::matchers::path_regex;
@@ -62,50 +60,16 @@ type PreBuildHook = dyn FnOnce(&Path) + Send + 'static;
type WorkspaceSetup = dyn FnOnce(AbsolutePathBuf, Arc<dyn ExecutorFileSystem>) -> BoxFuture<'static, Result<()>>
+ Send;
const TEST_MODEL_WITH_EXPERIMENTAL_TOOLS: &str = "test-gpt-5.1-codex";
const REMOTE_EXEC_SERVER_START_TIMEOUT: Duration = Duration::from_secs(5);
const REMOTE_EXEC_SERVER_POLL_INTERVAL: Duration = Duration::from_millis(25);
static REMOTE_EXEC_SERVER_INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)]
struct RemoteExecServerProcess {
container_name: String,
pid: u32,
remote_exec_server_path: String,
stdout_path: String,
cleanup_paths: Vec<String>,
}
impl Drop for RemoteExecServerProcess {
fn drop(&mut self) {
let cleanup_paths = self.cleanup_paths.join(" ");
let cleanup_paths_script = if cleanup_paths.is_empty() {
String::new()
} else {
format!("rm -rf {cleanup_paths}; ")
};
let script = format!(
"if kill -0 {pid} 2>/dev/null; then kill {pid}; fi; {cleanup_paths_script}rm -f {remote_exec_server_path} {stdout_path}",
pid = self.pid,
cleanup_paths_script = cleanup_paths_script,
remote_exec_server_path = self.remote_exec_server_path,
stdout_path = self.stdout_path
);
let _ = docker_command_capture_stdout(["exec", &self.container_name, "sh", "-lc", &script]);
}
}
impl RemoteExecServerProcess {
fn register_cleanup_path(&mut self, path: &Path) {
self.cleanup_paths.push(path.display().to_string());
}
}
const REMOTE_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_TEST_REMOTE_EXEC_SERVER_URL";
static REMOTE_TEST_INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0);
const SUBMIT_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub struct TestEnv {
environment: codex_exec_server::Environment,
cwd: AbsolutePathBuf,
local_cwd_temp_dir: Option<Arc<TempDir>>,
_remote_exec_server_process: Option<RemoteExecServerProcess>,
remote_container_name: Option<String>,
}
impl TestEnv {
@@ -117,7 +81,7 @@ impl TestEnv {
environment,
cwd,
local_cwd_temp_dir: Some(local_cwd_temp_dir),
_remote_exec_server_process: None,
remote_container_name: None,
})
}
@@ -138,163 +102,66 @@ impl TestEnv {
}
}
impl Drop for TestEnv {
fn drop(&mut self) {
if let Some(container_name) = &self.remote_container_name {
let script = format!("rm -rf {}", self.cwd.as_path().display());
let _ = docker_command_capture_stdout(["exec", container_name, "sh", "-lc", &script]);
}
}
}
pub async fn test_env() -> Result<TestEnv> {
match get_remote_test_env() {
Some(remote_env) => {
let mut remote_process = start_remote_exec_server(&remote_env)?;
let remote_ip = remote_container_ip(&remote_env.container_name)?;
let websocket_url = rewrite_websocket_host(&remote_process.listen_url, &remote_ip)?;
let websocket_url = remote_exec_server_url()?;
let environment = codex_exec_server::Environment::create(Some(websocket_url)).await?;
let cwd = remote_aware_cwd_path();
environment
.get_filesystem()
.create_directory(&cwd, CreateDirectoryOptions { recursive: true })
.create_directory(
&cwd,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
remote_process.process.register_cleanup_path(cwd.as_path());
Ok(TestEnv {
environment,
cwd,
local_cwd_temp_dir: None,
_remote_exec_server_process: Some(remote_process.process),
remote_container_name: Some(remote_env.container_name),
})
}
None => TestEnv::local().await,
}
}
struct RemoteExecServerStart {
process: RemoteExecServerProcess,
listen_url: String,
}
fn start_remote_exec_server(remote_env: &RemoteEnvConfig) -> Result<RemoteExecServerStart> {
let container_name = remote_env.container_name.as_str();
let instance_id = remote_exec_server_instance_id();
let remote_exec_server_path = format!("/tmp/codex-exec-server-{instance_id}");
let stdout_path = format!("/tmp/codex-exec-server-{instance_id}.stdout");
let local_binary = codex_utils_cargo_bin::cargo_bin("codex-exec-server")
.context("resolve codex-exec-server binary")?;
let local_binary = local_binary.to_string_lossy().to_string();
let remote_binary = format!("{container_name}:{remote_exec_server_path}");
docker_command_success(["cp", &local_binary, &remote_binary])?;
docker_command_success([
"exec",
container_name,
"chmod",
"+x",
&remote_exec_server_path,
])?;
let start_script = format!(
"rm -f {stdout_path}; \
nohup {remote_exec_server_path} --listen ws://0.0.0.0:0 > {stdout_path} 2>&1 & \
echo $!"
);
let pid_output =
docker_command_capture_stdout(["exec", container_name, "sh", "-lc", &start_script])?;
let pid = pid_output
.trim()
.parse::<u32>()
.with_context(|| format!("parse remote exec-server PID from {pid_output:?}"))?;
let listen_url = wait_for_remote_listen_url(container_name, &stdout_path)?;
Ok(RemoteExecServerStart {
process: RemoteExecServerProcess {
container_name: container_name.to_string(),
pid,
remote_exec_server_path,
stdout_path,
cleanup_paths: Vec::new(),
},
listen_url,
})
}
fn remote_aware_cwd_path() -> AbsolutePathBuf {
PathBuf::from(format!(
"/tmp/codex-core-test-cwd-{}",
remote_exec_server_instance_id()
remote_test_instance_id()
))
.abs()
}
fn wait_for_remote_listen_url(container_name: &str, stdout_path: &str) -> Result<String> {
let deadline = Instant::now() + REMOTE_EXEC_SERVER_START_TIMEOUT;
loop {
let line = docker_command_capture_stdout([
"exec",
container_name,
"sh",
"-lc",
&format!("head -n 1 {stdout_path} 2>/dev/null || true"),
])?;
let listen_url = line.trim();
if listen_url.starts_with("ws://") {
return Ok(listen_url.to_string());
}
if Instant::now() >= deadline {
return Err(anyhow!(
"timed out waiting for remote exec-server listen URL in container `{container_name}` after {REMOTE_EXEC_SERVER_START_TIMEOUT:?}"
));
}
std::thread::sleep(REMOTE_EXEC_SERVER_POLL_INTERVAL);
fn remote_exec_server_url() -> Result<String> {
let listen_url = std::env::var(REMOTE_EXEC_SERVER_URL_ENV_VAR).with_context(|| {
format!("{REMOTE_EXEC_SERVER_URL_ENV_VAR} must be set for remote tests")
})?;
let listen_url = listen_url.trim();
if listen_url.is_empty() {
return Err(anyhow!(
"{REMOTE_EXEC_SERVER_URL_ENV_VAR} must not be empty"
));
}
Ok(listen_url.to_string())
}
fn remote_exec_server_instance_id() -> String {
let instance = REMOTE_EXEC_SERVER_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
fn remote_test_instance_id() -> String {
let instance = REMOTE_TEST_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{}-{instance}", std::process::id())
}
fn remote_container_ip(container_name: &str) -> Result<String> {
let ip = docker_command_capture_stdout([
"inspect",
"-f",
"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}",
container_name,
])?;
let ip = ip.trim();
if ip.is_empty() {
return Err(anyhow!(
"container `{container_name}` has no IP address; cannot connect to remote exec-server"
));
}
Ok(ip.to_string())
}
fn rewrite_websocket_host(listen_url: &str, host: &str) -> Result<String> {
let Some(address) = listen_url.strip_prefix("ws://") else {
return Err(anyhow!(
"unexpected websocket listen URL `{listen_url}`; expected ws://IP:PORT"
));
};
let Some((_, port)) = address.rsplit_once(':') else {
return Err(anyhow!(
"unexpected websocket listen URL `{listen_url}`; expected ws://IP:PORT"
));
};
Ok(format!("ws://{host}:{port}"))
}
fn docker_command_success<const N: usize>(args: [&str; N]) -> Result<()> {
let output = Command::new("docker")
.args(args)
.output()
.with_context(|| format!("run docker {args:?}"))?;
if !output.status.success() {
return Err(anyhow!(
"docker {:?} failed: stdout={} stderr={}",
args,
String::from_utf8_lossy(&output.stdout).trim(),
String::from_utf8_lossy(&output.stderr).trim()
));
}
Ok(())
}
fn docker_command_capture_stdout<const N: usize>(args: [&str; N]) -> Result<String> {
let output = Command::new("docker")
.args(args)
@@ -527,7 +394,7 @@ impl TestCodexBuilder {
codex_core::test_support::thread_manager_with_models_provider_and_home(
auth.clone(),
config.model_provider.clone(),
config.codex_home.clone(),
config.codex_home.to_path_buf(),
Arc::clone(&environment_manager),
)
};
@@ -772,10 +639,14 @@ impl TestCodex {
_ => None,
})
.await;
wait_for_event(&self.codex, |event| match event {
EventMsg::TurnComplete(event) => event.turn_id == turn_id,
_ => false,
})
wait_for_event_with_timeout(
&self.codex,
|event| match event {
EventMsg::TurnComplete(event) => event.turn_id == turn_id,
_ => false,
},
SUBMIT_TURN_COMPLETE_TIMEOUT,
)
.await;
Ok(())
}
@@ -836,18 +707,26 @@ impl TestCodexHarness {
if let Some(parent) = abs_path.parent() {
self.test
.fs()
.create_directory(&parent, CreateDirectoryOptions { recursive: true })
.create_directory(
&parent,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
}
self.test
.fs()
.write_file(&abs_path, contents.as_ref().to_vec())
.write_file(&abs_path, contents.as_ref().to_vec(), /*sandbox*/ None)
.await?;
Ok(())
}
pub async fn read_file_text(&self, rel: impl AsRef<Path>) -> Result<String> {
Ok(self.test.fs().read_file_text(&self.path_abs(rel)).await?)
Ok(self
.test
.fs()
.read_file_text(&self.path_abs(rel), /*sandbox*/ None)
.await?)
}
pub async fn create_dir_all(&self, rel: impl AsRef<Path>) -> Result<()> {
@@ -856,6 +735,7 @@ impl TestCodexHarness {
.create_directory(
&self.path_abs(rel),
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
Ok(())
@@ -874,13 +754,14 @@ impl TestCodexHarness {
recursive: false,
force: true,
},
/*sandbox*/ None,
)
.await?;
Ok(())
}
pub async fn abs_path_exists(&self, path: &AbsolutePathBuf) -> Result<bool> {
match self.test.fs().get_metadata(path).await {
match self.test.fs().get_metadata(path, /*sandbox*/ None).await {
Ok(_) => Ok(true),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
Err(err) => Err(err.into()),