diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 707151a49e7c..d230cfff9e9a 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1420,6 +1420,7 @@ "rama-utils_0.3.0-alpha.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"const_format\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"name\":\"rama-macros\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"regex\",\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1\"},{\"features\":[\"write\",\"serde\",\"const_generics\",\"const_new\"],\"name\":\"smallvec\",\"req\":\"^1.15\"},{\"name\":\"smol_str\",\"req\":\"^0.3\"},{\"features\":[\"time\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"name\":\"wildcard\",\"req\":\"^0.3\"}],\"features\":{}}", "rand_0.10.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"rng\"],\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.1.3\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.140\"}],\"features\":{\"alloc\":[],\"chacha\":[\"dep:chacha20\"],\"default\":[\"std\",\"std_rng\",\"sys_rng\",\"thread_rng\"],\"log\":[],\"serde\":[\"dep:serde\"],\"simd_support\":[],\"std\":[\"alloc\",\"getrandom?/std\"],\"std_rng\":[\"dep:chacha20\"],\"sys_rng\":[\"dep:getrandom\",\"getrandom/sys_rng\"],\"thread_rng\":[\"std\",\"std_rng\",\"sys_rng\"],\"unbiased\":[]}}", "rand_0.8.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.22\",\"target\":\"cfg(unix)\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"features\":[\"into_bits\"],\"name\":\"packed_simd\",\"optional\":true,\"package\":\"packed_simd_2\",\"req\":\"^0.3.7\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"rand_core\",\"req\":\"^0.6.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.3.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"}],\"features\":{\"alloc\":[\"rand_core/alloc\"],\"default\":[\"std\",\"std_rng\"],\"getrandom\":[\"rand_core/getrandom\"],\"min_const_gen\":[],\"nightly\":[],\"serde1\":[\"serde\",\"rand_core/serde1\"],\"simd_support\":[\"packed_simd\"],\"small_rng\":[],\"std\":[\"rand_core/std\",\"rand_chacha/std\",\"alloc\",\"getrandom\",\"libc\"],\"std_rng\":[\"rand_chacha\"]}}", + "rand_0.8.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.22\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"rand_core\",\"req\":\"^0.6.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.3.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"}],\"features\":{\"alloc\":[\"rand_core/alloc\"],\"default\":[\"std\",\"std_rng\"],\"getrandom\":[\"rand_core/getrandom\"],\"log\":[],\"min_const_gen\":[],\"nightly\":[],\"serde1\":[\"serde\",\"rand_core/serde1\"],\"small_rng\":[],\"std\":[\"rand_core/std\",\"rand_chacha/std\",\"alloc\",\"getrandom\",\"libc\"],\"std_rng\":[\"rand_chacha\"]}}", "rand_0.9.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"optional\":true,\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.140\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"std_rng\",\"os_rng\",\"small_rng\",\"thread_rng\"],\"log\":[],\"nightly\":[],\"os_rng\":[\"rand_core/os_rng\"],\"serde\":[\"dep:serde\",\"rand_core/serde\"],\"simd_support\":[],\"small_rng\":[],\"std\":[\"rand_core/std\",\"rand_chacha?/std\",\"alloc\"],\"std_rng\":[\"dep:rand_chacha\"],\"thread_rng\":[\"std\",\"std_rng\",\"os_rng\"],\"unbiased\":[]}}", "rand_chacha_0.3.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"simd\"],\"name\":\"ppv-lite86\",\"req\":\"^0.2.8\"},{\"name\":\"rand_core\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde1\":[\"serde\"],\"simd\":[],\"std\":[\"ppv-lite86/std\"]}}", "rand_chacha_0.9.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"simd\"],\"name\":\"ppv-lite86\",\"req\":\"^0.2.14\"},{\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"features\":[\"os_rng\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"os_rng\":[\"rand_core/os_rng\"],\"serde\":[\"dep:serde\"],\"std\":[\"ppv-lite86/std\",\"rand_core/std\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index cb9caa4a6448..dfd339083fd9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -213,7 +213,7 @@ dependencies = [ "lazy_static", "nom 7.1.3", "pin-project", - "rand 0.8.5", + "rand 0.8.6", "rust-embed", "scrypt", "sha2 0.10.9", @@ -234,7 +234,7 @@ dependencies = [ "hkdf 0.12.4", "io_tee", "nom 7.1.3", - "rand 0.8.5", + "rand 0.8.6", "secrecy", "sha2 0.10.9", ] @@ -786,7 +786,7 @@ dependencies = [ "hex", "http 1.4.0", "p256", - "rand 0.8.5", + "rand 0.8.6", "ring", "sha2 0.10.9", "time", @@ -1852,7 +1852,6 @@ dependencies = [ name = "codex-agent-graph-store" version = "0.0.0" dependencies = [ - "async-trait", "codex-protocol", "codex-state", "pretty_assertions", @@ -1919,7 +1918,6 @@ dependencies = [ "anyhow", "assert_matches", "async-channel", - "async-trait", "base64 0.22.1", "bytes", "chrono", @@ -1953,7 +1951,6 @@ version = "0.0.0" dependencies = [ "anyhow", "app_test_support", - "async-trait", "axum", "base64 0.22.1", "chrono", @@ -2218,7 +2215,6 @@ dependencies = [ name = "codex-async-utils" version = "0.0.0" dependencies = [ - "async-trait", "pretty_assertions", "tokio", "tokio-util", @@ -2376,7 +2372,6 @@ dependencies = [ name = "codex-client" version = "0.0.0" dependencies = [ - "async-trait", "bytes", "codex-utils-cargo-bin", "codex-utils-rustls-provider", @@ -2431,7 +2426,6 @@ name = "codex-cloud-tasks" version = "0.0.0" dependencies = [ "anyhow", - "async-trait", "chrono", "clap", "codex-client", @@ -2463,7 +2457,6 @@ name = "codex-cloud-tasks-client" version = "0.0.0" dependencies = [ "anyhow", - "async-trait", "chrono", "codex-api", "codex-backend-client", @@ -2477,7 +2470,6 @@ dependencies = [ name = "codex-cloud-tasks-mock-client" version = "0.0.0" dependencies = [ - "async-trait", "chrono", "codex-cloud-tasks-client", "diffy", @@ -2507,7 +2499,6 @@ name = "codex-config" version = "0.0.0" dependencies = [ "anyhow", - "async-trait", "base64 0.22.1", "codex-app-server-protocol", "codex-execpolicy", @@ -2584,7 +2575,6 @@ dependencies = [ "assert_cmd", "assert_matches", "async-channel", - "async-trait", "base64 0.22.1", "bm25", "chrono", @@ -2844,7 +2834,6 @@ version = "0.0.0" dependencies = [ "anyhow", "arc-swap", - "async-trait", "axum", "base64 0.22.1", "bytes", @@ -3012,7 +3001,6 @@ dependencies = [ name = "codex-file-system" version = "0.0.0" dependencies = [ - "async-trait", "codex-protocol", "codex-utils-absolute-path", "codex-utils-path-uri", @@ -3204,7 +3192,6 @@ name = "codex-login" version = "0.0.0" dependencies = [ "anyhow", - "async-trait", "base64 0.22.1", "chrono", "codex-agent-identity", @@ -3409,7 +3396,6 @@ dependencies = [ name = "codex-model-provider" version = "0.0.0" dependencies = [ - "async-trait", "codex-agent-identity", "codex-api", "codex-aws-auth", @@ -3450,7 +3436,6 @@ dependencies = [ name = "codex-models-manager" version = "0.0.0" dependencies = [ - "async-trait", "chrono", "codex-app-server-protocol", "codex-collaboration-mode-templates", @@ -3472,7 +3457,6 @@ name = "codex-network-proxy" version = "0.0.0" dependencies = [ "anyhow", - "async-trait", "base64 0.22.1", "chrono", "clap", @@ -3707,7 +3691,6 @@ name = "codex-rollout" version = "0.0.0" dependencies = [ "anyhow", - "async-trait", "chrono", "codex-file-search", "codex-git-utils", @@ -3749,7 +3732,6 @@ name = "codex-sandboxing" version = "0.0.0" dependencies = [ "anyhow", - "async-trait", "codex-network-proxy", "codex-protocol", "codex-utils-absolute-path", @@ -3812,7 +3794,6 @@ name = "codex-shell-escalation" version = "0.0.0" dependencies = [ "anyhow", - "async-trait", "clap", "codex-protocol", "codex-utils-absolute-path", @@ -3841,7 +3822,6 @@ dependencies = [ name = "codex-skills-extension" version = "0.0.0" dependencies = [ - "async-trait", "codex-core-skills", "codex-exec-server", "codex-extension-api", @@ -3926,7 +3906,6 @@ dependencies = [ name = "codex-thread-store" version = "0.0.0" dependencies = [ - "async-trait", "chrono", "codex-git-utils", "codex-install-context", @@ -4260,7 +4239,6 @@ name = "codex-utils-readiness" version = "0.0.0" dependencies = [ "assert_matches", - "async-trait", "thiserror 2.0.18", "time", "tokio", @@ -4361,7 +4339,7 @@ dependencies = [ "dunce", "glob", "pretty_assertions", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "tempfile", @@ -9470,7 +9448,7 @@ dependencies = [ "chrono", "getrandom 0.2.17", "http 1.4.0", - "rand 0.8.5", + "rand 0.8.6", "reqwest 0.12.28", "serde", "serde_json", @@ -11109,9 +11087,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -11977,7 +11955,7 @@ dependencies = [ "hkdf 0.12.4", "num", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "serde", "sha2 0.10.9", "zbus", @@ -15642,7 +15620,7 @@ dependencies = [ "hex", "nix 0.29.0", "ordered-stream", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_repr", "sha1 0.10.6", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index dab5e82855e8..0b939c9acfae 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -265,7 +265,6 @@ assert_matches = "1.5.0" async-channel = "2.3.1" async-io = "2.6.0" async-stream = "0.3.6" -async-trait = "0.1.89" aws-config = "1" aws-credential-types = "1" aws-sigv4 = "1" diff --git a/codex-rs/agent-graph-store/Cargo.toml b/codex-rs/agent-graph-store/Cargo.toml index 9ecd827194b9..f676959add78 100644 --- a/codex-rs/agent-graph-store/Cargo.toml +++ b/codex-rs/agent-graph-store/Cargo.toml @@ -13,7 +13,6 @@ doctest = false workspace = true [dependencies] -async-trait = { workspace = true } codex-protocol = { workspace = true } codex-state = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/codex-rs/agent-graph-store/src/local.rs b/codex-rs/agent-graph-store/src/local.rs index f45874855c6c..b90aaf4f4784 100644 --- a/codex-rs/agent-graph-store/src/local.rs +++ b/codex-rs/agent-graph-store/src/local.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use codex_protocol::ThreadId; use codex_state::StateRuntime; use std::sync::Arc; @@ -29,7 +28,6 @@ impl LocalAgentGraphStore { } } -#[async_trait] impl AgentGraphStore for LocalAgentGraphStore { async fn upsert_thread_spawn_edge( &self, diff --git a/codex-rs/agent-graph-store/src/store.rs b/codex-rs/agent-graph-store/src/store.rs index c421182110f1..2e0ba20733b0 100644 --- a/codex-rs/agent-graph-store/src/store.rs +++ b/codex-rs/agent-graph-store/src/store.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use codex_protocol::ThreadId; use crate::AgentGraphStoreResult; @@ -8,38 +7,37 @@ use crate::ThreadSpawnEdgeStatus; /// /// Implementations are expected to return stable ordering for list methods so callers can merge /// persisted graph state with live in-memory state without introducing nondeterministic output. -#[async_trait] pub trait AgentGraphStore: Send + Sync { /// Insert or replace the directional parent/child edge for a spawned thread. /// /// `child_thread_id` has at most one persisted parent. Re-inserting the same child should /// update both the parent and status to match the supplied values. - async fn upsert_thread_spawn_edge( + fn upsert_thread_spawn_edge( &self, parent_thread_id: ThreadId, child_thread_id: ThreadId, status: ThreadSpawnEdgeStatus, - ) -> AgentGraphStoreResult<()>; + ) -> impl std::future::Future> + Send; /// Update the persisted lifecycle status of a spawned thread's incoming edge. /// /// Implementations should treat missing children as a successful no-op. - async fn set_thread_spawn_edge_status( + fn set_thread_spawn_edge_status( &self, child_thread_id: ThreadId, status: ThreadSpawnEdgeStatus, - ) -> AgentGraphStoreResult<()>; + ) -> impl std::future::Future> + Send; /// List direct spawned children of a parent thread. /// /// When `status_filter` is `Some`, only child edges with that exact status are returned. When /// it is `None`, all direct child edges are returned regardless of status, including statuses /// that may be added by a future store implementation. - async fn list_thread_spawn_children( + fn list_thread_spawn_children( &self, parent_thread_id: ThreadId, status_filter: Option, - ) -> AgentGraphStoreResult>; + ) -> impl std::future::Future>> + Send; /// List spawned descendants breadth-first by depth, then by thread id. /// @@ -47,9 +45,9 @@ pub trait AgentGraphStore: Send + Sync { /// For example, `Some(Open)` walks only open edges, so descendants under a closed edge are not /// included even if their own incoming edge is open. `None` walks and returns every persisted /// edge regardless of status. - async fn list_thread_spawn_descendants( + fn list_thread_spawn_descendants( &self, root_thread_id: ThreadId, status_filter: Option, - ) -> AgentGraphStoreResult>; + ) -> impl std::future::Future>> + Send; } diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 756a39484147..3d08059a63c2 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -22,7 +22,6 @@ workspace = true [dependencies] anyhow = { workspace = true } -async-trait = { workspace = true } base64 = { workspace = true } axum = { workspace = true, default-features = false, features = [ "http1", diff --git a/codex-rs/app-server/src/mcp_refresh.rs b/codex-rs/app-server/src/mcp_refresh.rs index 0fd63a5f0285..51cb6dded097 100644 --- a/codex-rs/app-server/src/mcp_refresh.rs +++ b/codex-rs/app-server/src/mcp_refresh.rs @@ -99,7 +99,6 @@ mod tests { use crate::extensions::ThreadExtensionDependencies; use crate::extensions::guardian_agent_spawner; use crate::extensions::thread_extensions; - use async_trait::async_trait; use codex_arg0::Arg0DispatchPaths; use codex_config::CloudConfigBundleLoader; use codex_config::LoaderOverrides; @@ -245,8 +244,7 @@ mod tests { bad_loads: AtomicUsize, } - #[async_trait] - impl ThreadConfigLoader for CountingThreadConfigLoader { + impl CountingThreadConfigLoader { async fn load( &self, context: ThreadConfigContext, @@ -265,4 +263,13 @@ mod tests { Ok(Vec::new()) } } + + impl ThreadConfigLoader for CountingThreadConfigLoader { + fn load( + &self, + context: ThreadConfigContext, + ) -> codex_config::ThreadConfigLoaderFuture<'_, Vec> { + Box::pin(CountingThreadConfigLoader::load(self, context)) + } + } } diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 10bbccd9e4df..d55b266dcdfa 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -46,7 +46,6 @@ use crate::thread_state::ConnectionCapabilities; use crate::thread_state::ThreadStateManager; use crate::transport::AppServerTransport; use crate::transport::RemoteControlHandle; -use async_trait::async_trait; use codex_analytics::AnalyticsEventsClient; use codex_analytics::AppServerRpcTransport; use codex_app_server_protocol::AuthMode as LoginAuthMode; @@ -106,13 +105,6 @@ impl ExternalAuthRefreshBridge { ExternalAuthRefreshReason::Unauthorized => ChatgptAuthTokensRefreshReason::Unauthorized, } } -} - -#[async_trait] -impl ExternalAuth for ExternalAuthRefreshBridge { - fn auth_mode(&self) -> LoginAuthMode { - LoginAuthMode::Chatgpt - } async fn refresh( &self, @@ -163,6 +155,19 @@ impl ExternalAuth for ExternalAuthRefreshBridge { } } +impl ExternalAuth for ExternalAuthRefreshBridge { + fn auth_mode(&self) -> LoginAuthMode { + LoginAuthMode::Chatgpt + } + + fn refresh( + &self, + context: ExternalAuthRefreshContext, + ) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> { + Box::pin(ExternalAuthRefreshBridge::refresh(self, context)) + } +} + pub(crate) struct MessageProcessor { outgoing: Arc, skills_watcher: Arc, diff --git a/codex-rs/async-utils/Cargo.toml b/codex-rs/async-utils/Cargo.toml index 9f81ff818e6f..093bbe0972b4 100644 --- a/codex-rs/async-utils/Cargo.toml +++ b/codex-rs/async-utils/Cargo.toml @@ -8,7 +8,6 @@ license.workspace = true workspace = true [dependencies] -async-trait.workspace = true tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "time"] } tokio-util.workspace = true diff --git a/codex-rs/async-utils/src/lib.rs b/codex-rs/async-utils/src/lib.rs index bd880ae1fb90..caa3479a6709 100644 --- a/codex-rs/async-utils/src/lib.rs +++ b/codex-rs/async-utils/src/lib.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use std::future::Future; use tokio_util::sync::CancellationToken; @@ -7,14 +6,15 @@ pub enum CancelErr { Cancelled, } -#[async_trait] pub trait OrCancelExt: Sized { type Output; - async fn or_cancel(self, token: &CancellationToken) -> Result; + fn or_cancel( + self, + token: &CancellationToken, + ) -> impl Future> + Send; } -#[async_trait] impl OrCancelExt for F where F: Future + Send, diff --git a/codex-rs/cloud-tasks-client/Cargo.toml b/codex-rs/cloud-tasks-client/Cargo.toml index df8ec12b206d..dc07550d086d 100644 --- a/codex-rs/cloud-tasks-client/Cargo.toml +++ b/codex-rs/cloud-tasks-client/Cargo.toml @@ -15,7 +15,6 @@ workspace = true [dependencies] anyhow = { workspace = true } -async-trait = { workspace = true } chrono = { workspace = true, features = ["serde"] } codex-api = { workspace = true } codex-backend-client = { workspace = true } diff --git a/codex-rs/cloud-tasks-client/src/api.rs b/codex-rs/cloud-tasks-client/src/api.rs index 7059bdb39fdd..a9c75dedc67e 100644 --- a/codex-rs/cloud-tasks-client/src/api.rs +++ b/codex-rs/cloud-tasks-client/src/api.rs @@ -2,8 +2,11 @@ use chrono::DateTime; use chrono::Utc; use serde::Deserialize; use serde::Serialize; +use std::future::Future; +use std::pin::Pin; pub type Result = std::result::Result; +pub type CloudBackendFuture<'a, T> = Pin> + Send + 'a>>; #[derive(Debug, thiserror::Error)] pub enum CloudTaskError { @@ -130,41 +133,44 @@ impl Default for TaskText { } } -#[async_trait::async_trait] pub trait CloudBackend: Send + Sync { - async fn list_tasks( - &self, - env: Option<&str>, + fn list_tasks<'a>( + &'a self, + env: Option<&'a str>, limit: Option, - cursor: Option<&str>, - ) -> Result; - async fn get_task_summary(&self, id: TaskId) -> Result; - async fn get_task_diff(&self, id: TaskId) -> Result>; + cursor: Option<&'a str>, + ) -> CloudBackendFuture<'a, TaskListPage>; + fn get_task_summary(&self, id: TaskId) -> CloudBackendFuture<'_, TaskSummary>; + fn get_task_diff(&self, id: TaskId) -> CloudBackendFuture<'_, Option>; /// Return assistant output messages (no diff) when available. - async fn get_task_messages(&self, id: TaskId) -> Result>; + fn get_task_messages(&self, id: TaskId) -> CloudBackendFuture<'_, Vec>; /// Return the creating prompt and assistant messages (when available). - async fn get_task_text(&self, id: TaskId) -> Result; + fn get_task_text(&self, id: TaskId) -> CloudBackendFuture<'_, TaskText>; /// Return any sibling attempts (best-of-N) for the given assistant turn. - async fn list_sibling_attempts( + fn list_sibling_attempts( &self, task: TaskId, turn_id: String, - ) -> Result>; + ) -> CloudBackendFuture<'_, Vec>; /// Dry-run apply (preflight) that validates whether the patch would apply cleanly. /// Never modifies the working tree. When `diff_override` is supplied, the provided diff is /// used instead of re-fetching the task details so callers can apply alternate attempts. - async fn apply_task_preflight( + fn apply_task_preflight( &self, id: TaskId, diff_override: Option, - ) -> Result; - async fn apply_task(&self, id: TaskId, diff_override: Option) -> Result; - async fn create_task( + ) -> CloudBackendFuture<'_, ApplyOutcome>; + fn apply_task( &self, - env_id: &str, - prompt: &str, - git_ref: &str, + id: TaskId, + diff_override: Option, + ) -> CloudBackendFuture<'_, ApplyOutcome>; + fn create_task<'a>( + &'a self, + env_id: &'a str, + prompt: &'a str, + git_ref: &'a str, qa_mode: bool, best_of_n: usize, - ) -> Result; + ) -> CloudBackendFuture<'a, CreatedTask>; } diff --git a/codex-rs/cloud-tasks-client/src/http.rs b/codex-rs/cloud-tasks-client/src/http.rs index 46fed812bac0..2f0fd613f79b 100644 --- a/codex-rs/cloud-tasks-client/src/http.rs +++ b/codex-rs/cloud-tasks-client/src/http.rs @@ -2,6 +2,7 @@ use crate::ApplyOutcome; use crate::ApplyStatus; use crate::AttemptStatus; use crate::CloudBackend; +use crate::CloudBackendFuture; use crate::CloudTaskError; use crate::DiffSummary; use crate::Result; @@ -61,68 +62,77 @@ impl HttpClient { } } -#[async_trait::async_trait] impl CloudBackend for HttpClient { - async fn list_tasks( - &self, - env: Option<&str>, + fn list_tasks<'a>( + &'a self, + env: Option<&'a str>, limit: Option, - cursor: Option<&str>, - ) -> Result { - self.tasks_api().list(env, limit, cursor).await + cursor: Option<&'a str>, + ) -> CloudBackendFuture<'a, TaskListPage> { + Box::pin(async move { self.tasks_api().list(env, limit, cursor).await }) } - async fn get_task_summary(&self, id: TaskId) -> Result { - self.tasks_api().summary(id).await + fn get_task_summary(&self, id: TaskId) -> CloudBackendFuture<'_, TaskSummary> { + Box::pin(async move { self.tasks_api().summary(id).await }) } - async fn get_task_diff(&self, id: TaskId) -> Result> { - self.tasks_api().diff(id).await + fn get_task_diff(&self, id: TaskId) -> CloudBackendFuture<'_, Option> { + Box::pin(async move { self.tasks_api().diff(id).await }) } - async fn get_task_messages(&self, id: TaskId) -> Result> { - self.tasks_api().messages(id).await + fn get_task_messages(&self, id: TaskId) -> CloudBackendFuture<'_, Vec> { + Box::pin(async move { self.tasks_api().messages(id).await }) } - async fn get_task_text(&self, id: TaskId) -> Result { - self.tasks_api().task_text(id).await + fn get_task_text(&self, id: TaskId) -> CloudBackendFuture<'_, TaskText> { + Box::pin(async move { self.tasks_api().task_text(id).await }) } - async fn list_sibling_attempts( + fn list_sibling_attempts( &self, task: TaskId, turn_id: String, - ) -> Result> { - self.attempts_api().list(task, turn_id).await + ) -> CloudBackendFuture<'_, Vec> { + Box::pin(async move { self.attempts_api().list(task, turn_id).await }) } - async fn apply_task(&self, id: TaskId, diff_override: Option) -> Result { - self.apply_api() - .run(id, diff_override, /*preflight*/ false) - .await + fn apply_task( + &self, + id: TaskId, + diff_override: Option, + ) -> CloudBackendFuture<'_, ApplyOutcome> { + Box::pin(async move { + self.apply_api() + .run(id, diff_override, /*preflight*/ false) + .await + }) } - async fn apply_task_preflight( + fn apply_task_preflight( &self, id: TaskId, diff_override: Option, - ) -> Result { - self.apply_api() - .run(id, diff_override, /*preflight*/ true) - .await + ) -> CloudBackendFuture<'_, ApplyOutcome> { + Box::pin(async move { + self.apply_api() + .run(id, diff_override, /*preflight*/ true) + .await + }) } - async fn create_task( - &self, - env_id: &str, - prompt: &str, - git_ref: &str, + fn create_task<'a>( + &'a self, + env_id: &'a str, + prompt: &'a str, + git_ref: &'a str, qa_mode: bool, best_of_n: usize, - ) -> Result { - self.tasks_api() - .create(env_id, prompt, git_ref, qa_mode, best_of_n) - .await + ) -> CloudBackendFuture<'a, crate::CreatedTask> { + Box::pin(async move { + self.tasks_api() + .create(env_id, prompt, git_ref, qa_mode, best_of_n) + .await + }) } } diff --git a/codex-rs/cloud-tasks-client/src/lib.rs b/codex-rs/cloud-tasks-client/src/lib.rs index 8ed6469a5332..a3f883403b02 100644 --- a/codex-rs/cloud-tasks-client/src/lib.rs +++ b/codex-rs/cloud-tasks-client/src/lib.rs @@ -4,6 +4,7 @@ pub use api::ApplyOutcome; pub use api::ApplyStatus; pub use api::AttemptStatus; pub use api::CloudBackend; +pub use api::CloudBackendFuture; pub use api::CloudTaskError; pub use api::CreatedTask; pub use api::DiffSummary; diff --git a/codex-rs/cloud-tasks-mock-client/Cargo.toml b/codex-rs/cloud-tasks-mock-client/Cargo.toml index b4531cff63b7..b249b654e02f 100644 --- a/codex-rs/cloud-tasks-mock-client/Cargo.toml +++ b/codex-rs/cloud-tasks-mock-client/Cargo.toml @@ -15,7 +15,6 @@ doctest = false workspace = true [dependencies] -async-trait = { workspace = true } chrono = { workspace = true } codex-cloud-tasks-client = { workspace = true } diffy = { workspace = true } diff --git a/codex-rs/cloud-tasks-mock-client/src/mock.rs b/codex-rs/cloud-tasks-mock-client/src/mock.rs index 4bde0e93b997..08fadee371c0 100644 --- a/codex-rs/cloud-tasks-mock-client/src/mock.rs +++ b/codex-rs/cloud-tasks-mock-client/src/mock.rs @@ -3,6 +3,7 @@ use codex_cloud_tasks_client::ApplyOutcome; use codex_cloud_tasks_client::ApplyStatus; use codex_cloud_tasks_client::AttemptStatus; use codex_cloud_tasks_client::CloudBackend; +use codex_cloud_tasks_client::CloudBackendFuture; use codex_cloud_tasks_client::CloudTaskError; use codex_cloud_tasks_client::CreatedTask; use codex_cloud_tasks_client::DiffSummary; @@ -17,8 +18,7 @@ use codex_cloud_tasks_client::TurnAttempt; #[derive(Clone, Default)] pub struct MockClient; -#[async_trait::async_trait] -impl CloudBackend for MockClient { +impl MockClient { async fn list_tasks( &self, _env: Option<&str>, @@ -160,6 +160,70 @@ impl CloudBackend for MockClient { } } +impl CloudBackend for MockClient { + fn list_tasks<'a>( + &'a self, + env: Option<&'a str>, + limit: Option, + cursor: Option<&'a str>, + ) -> CloudBackendFuture<'a, TaskListPage> { + Box::pin(MockClient::list_tasks(self, env, limit, cursor)) + } + + fn get_task_summary(&self, id: TaskId) -> CloudBackendFuture<'_, TaskSummary> { + Box::pin(MockClient::get_task_summary(self, id)) + } + + fn get_task_diff(&self, id: TaskId) -> CloudBackendFuture<'_, Option> { + Box::pin(MockClient::get_task_diff(self, id)) + } + + fn get_task_messages(&self, id: TaskId) -> CloudBackendFuture<'_, Vec> { + Box::pin(MockClient::get_task_messages(self, id)) + } + + fn get_task_text(&self, id: TaskId) -> CloudBackendFuture<'_, TaskText> { + Box::pin(MockClient::get_task_text(self, id)) + } + + fn apply_task( + &self, + id: TaskId, + diff_override: Option, + ) -> CloudBackendFuture<'_, ApplyOutcome> { + Box::pin(MockClient::apply_task(self, id, diff_override)) + } + + fn apply_task_preflight( + &self, + id: TaskId, + diff_override: Option, + ) -> CloudBackendFuture<'_, ApplyOutcome> { + Box::pin(MockClient::apply_task_preflight(self, id, diff_override)) + } + + fn list_sibling_attempts( + &self, + task: TaskId, + turn_id: String, + ) -> CloudBackendFuture<'_, Vec> { + Box::pin(MockClient::list_sibling_attempts(self, task, turn_id)) + } + + fn create_task<'a>( + &'a self, + env_id: &'a str, + prompt: &'a str, + git_ref: &'a str, + qa_mode: bool, + best_of_n: usize, + ) -> CloudBackendFuture<'a, CreatedTask> { + Box::pin(MockClient::create_task( + self, env_id, prompt, git_ref, qa_mode, best_of_n, + )) + } +} + fn mock_diff_for(id: &TaskId) -> String { match id.0.as_str() { "T-1000" => { diff --git a/codex-rs/cloud-tasks/Cargo.toml b/codex-rs/cloud-tasks/Cargo.toml index 7bdcaaddbaa3..e7997259a588 100644 --- a/codex-rs/cloud-tasks/Cargo.toml +++ b/codex-rs/cloud-tasks/Cargo.toml @@ -39,8 +39,5 @@ tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } unicode-width = { workspace = true } -[dependencies.async-trait] -workspace = true - [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/cloud-tasks/src/app.rs b/codex-rs/cloud-tasks/src/app.rs index aa02be97f1b7..7b227f074602 100644 --- a/codex-rs/cloud-tasks/src/app.rs +++ b/codex-rs/cloud-tasks/src/app.rs @@ -354,6 +354,7 @@ pub enum AppEvent { mod tests { use super::*; use chrono::Utc; + use codex_cloud_tasks_client::CloudBackendFuture; use codex_cloud_tasks_client::CloudTaskError; struct FakeBackend { @@ -361,14 +362,13 @@ mod tests { by_env: std::collections::HashMap, Vec<&'static str>>, } - #[async_trait::async_trait] - impl codex_cloud_tasks_client::CloudBackend for FakeBackend { + impl FakeBackend { async fn list_tasks( &self, env: Option<&str>, limit: Option, cursor: Option<&str>, - ) -> codex_cloud_tasks_client::Result { + ) -> Result { let key = env.map(str::to_string); let titles = self .by_env @@ -404,10 +404,7 @@ mod tests { }) } - async fn get_task_summary( - &self, - id: TaskId, - ) -> codex_cloud_tasks_client::Result { + async fn get_task_summary(&self, id: TaskId) -> Result { self.list_tasks(/*env*/ None, /*limit*/ None, /*cursor*/ None) .await? .tasks @@ -416,25 +413,10 @@ mod tests { .ok_or_else(|| CloudTaskError::Msg(format!("Task {} not found", id.0))) } - async fn get_task_diff( - &self, - _id: TaskId, - ) -> codex_cloud_tasks_client::Result> { - Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( - "not used in test", - )) - } - - async fn get_task_messages( - &self, - _id: TaskId, - ) -> codex_cloud_tasks_client::Result> { - Ok(vec![]) - } async fn get_task_text( &self, _id: TaskId, - ) -> codex_cloud_tasks_client::Result { + ) -> Result { Ok(codex_cloud_tasks_client::TaskText { prompt: Some("Example prompt".to_string()), messages: Vec::new(), @@ -444,46 +426,86 @@ mod tests { attempt_status: codex_cloud_tasks_client::AttemptStatus::Completed, }) } + } + + impl codex_cloud_tasks_client::CloudBackend for FakeBackend { + fn list_tasks<'a>( + &'a self, + env: Option<&'a str>, + limit: Option, + cursor: Option<&'a str>, + ) -> CloudBackendFuture<'a, codex_cloud_tasks_client::TaskListPage> { + Box::pin(FakeBackend::list_tasks(self, env, limit, cursor)) + } + + fn get_task_summary(&self, id: TaskId) -> CloudBackendFuture<'_, TaskSummary> { + Box::pin(FakeBackend::get_task_summary(self, id)) + } + + fn get_task_diff(&self, _id: TaskId) -> CloudBackendFuture<'_, Option> { + Box::pin(async { + Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( + "not used in test", + )) + }) + } + + fn get_task_messages(&self, _id: TaskId) -> CloudBackendFuture<'_, Vec> { + Box::pin(async { Ok(vec![]) }) + } - async fn list_sibling_attempts( + fn get_task_text( + &self, + id: TaskId, + ) -> CloudBackendFuture<'_, codex_cloud_tasks_client::TaskText> { + Box::pin(FakeBackend::get_task_text(self, id)) + } + + fn list_sibling_attempts( &self, _task: TaskId, _turn_id: String, - ) -> codex_cloud_tasks_client::Result> { - Ok(Vec::new()) + ) -> CloudBackendFuture<'_, Vec> { + Box::pin(async { Ok(Vec::new()) }) } - async fn apply_task( + fn apply_task( &self, _id: TaskId, _diff_override: Option, - ) -> codex_cloud_tasks_client::Result { - Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( - "not used in test", - )) + ) -> CloudBackendFuture<'_, codex_cloud_tasks_client::ApplyOutcome> { + Box::pin(async { + Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( + "not used in test", + )) + }) } - async fn apply_task_preflight( + fn apply_task_preflight( &self, _id: TaskId, _diff_override: Option, - ) -> codex_cloud_tasks_client::Result { - Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( - "not used in test", - )) + ) -> CloudBackendFuture<'_, codex_cloud_tasks_client::ApplyOutcome> { + Box::pin(async { + Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( + "not used in test", + )) + }) } - async fn create_task( - &self, - _env_id: &str, - _prompt: &str, - _git_ref: &str, + fn create_task<'a>( + &'a self, + _env_id: &'a str, + _prompt: &'a str, + _git_ref: &'a str, _qa_mode: bool, _best_of_n: usize, - ) -> codex_cloud_tasks_client::Result { - Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( - "not used in test", - )) + ) -> CloudBackendFuture<'a, codex_cloud_tasks_client::CreatedTask> { + Box::pin(async { + Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( + "not used in test", + )) + }) } } diff --git a/codex-rs/cloud-tasks/src/lib.rs b/codex-rs/cloud-tasks/src/lib.rs index e8d6b545b50d..4b99bb8c87a1 100644 --- a/codex-rs/cloud-tasks/src/lib.rs +++ b/codex-rs/cloud-tasks/src/lib.rs @@ -106,16 +106,20 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result }) } -#[async_trait::async_trait] trait GitInfoProvider { - async fn default_branch_name(&self, path: &std::path::Path) -> Option; - - async fn current_branch_name(&self, path: &std::path::Path) -> Option; + fn default_branch_name( + &self, + path: &std::path::Path, + ) -> impl std::future::Future> + Send; + + fn current_branch_name( + &self, + path: &std::path::Path, + ) -> impl std::future::Future> + Send; } struct RealGitInfo; -#[async_trait::async_trait] impl GitInfoProvider for RealGitInfo { async fn default_branch_name(&self, path: &std::path::Path) -> Option { default_branch_name(path).await @@ -2156,7 +2160,6 @@ mod tests { } } - #[async_trait::async_trait] impl super::GitInfoProvider for StubGitInfo { async fn default_branch_name(&self, _path: &std::path::Path) -> Option { self.default_branch.clone() diff --git a/codex-rs/codex-api/Cargo.toml b/codex-rs/codex-api/Cargo.toml index 07d855725a06..3b34b1569b93 100644 --- a/codex-rs/codex-api/Cargo.toml +++ b/codex-rs/codex-api/Cargo.toml @@ -6,7 +6,6 @@ license.workspace = true [dependencies] async-channel = { workspace = true } -async-trait = { workspace = true } base64 = { workspace = true } bytes = { workspace = true } chrono = { workspace = true } diff --git a/codex-rs/codex-api/src/auth.rs b/codex-rs/codex-api/src/auth.rs index 41394a22584b..2d88dd6e2d04 100644 --- a/codex-rs/codex-api/src/auth.rs +++ b/codex-rs/codex-api/src/auth.rs @@ -1,7 +1,8 @@ -use async_trait::async_trait; use codex_client::Request; use codex_client::TransportError; use http::HeaderMap; +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; /// Error returned while applying authentication to an outbound request. @@ -26,7 +27,6 @@ impl From for TransportError { /// /// Header-only providers can implement `add_auth_headers`; providers that sign /// complete requests can override `apply_auth`. -#[async_trait] pub trait AuthProvider: Send + Sync { /// Adds any auth headers that are available without request body access. /// @@ -52,13 +52,18 @@ pub trait AuthProvider: Send + Sync { /// /// Callers must always use the returned request as authoritative. /// If this returns [`AuthError`], the request should not be sent. - async fn apply_auth(&self, request: Request) -> Result { - let mut request = request; - self.add_auth_headers(&mut request.headers); - Ok(request) + fn apply_auth(&self, request: Request) -> AuthProviderFuture<'_> { + Box::pin(async move { + let mut request = request; + self.add_auth_headers(&mut request.headers); + Ok(request) + }) } } +pub type AuthProviderFuture<'a> = + Pin> + Send + 'a>>; + /// Shared auth handle passed through API clients. pub type SharedAuthProvider = Arc; diff --git a/codex-rs/codex-api/src/endpoint/compact.rs b/codex-rs/codex-api/src/endpoint/compact.rs index a3da854844cf..939bc7d8587c 100644 --- a/codex-rs/codex-api/src/endpoint/compact.rs +++ b/codex-rs/codex-api/src/endpoint/compact.rs @@ -77,7 +77,6 @@ struct CompactHistoryResponse { #[cfg(test)] mod tests { use super::*; - use async_trait::async_trait; use codex_client::Request; use codex_client::Response; use codex_client::StreamResponse; @@ -86,7 +85,6 @@ mod tests { #[derive(Clone, Default)] struct DummyTransport; - #[async_trait] impl HttpTransport for DummyTransport { async fn execute(&self, _req: Request) -> Result { Err(TransportError::Build("execute should not run".to_string())) diff --git a/codex-rs/codex-api/src/endpoint/images.rs b/codex-rs/codex-api/src/endpoint/images.rs index 9d1bd41eea3c..9f5856379427 100644 --- a/codex-rs/codex-api/src/endpoint/images.rs +++ b/codex-rs/codex-api/src/endpoint/images.rs @@ -80,7 +80,6 @@ mod tests { use crate::images::ImageQuality; use crate::images::ImageUrl; use crate::provider::RetryConfig; - use async_trait::async_trait; use codex_client::Request; use codex_client::RequestBody; use codex_client::Response; @@ -114,7 +113,6 @@ mod tests { } } - #[async_trait] impl HttpTransport for CapturingTransport { async fn execute(&self, req: Request) -> Result { *self.last_request.lock().expect("lock request store") = Some(req); diff --git a/codex-rs/codex-api/src/endpoint/memories.rs b/codex-rs/codex-api/src/endpoint/memories.rs index a6c25641f25d..122ca565f70e 100644 --- a/codex-rs/codex-api/src/endpoint/memories.rs +++ b/codex-rs/codex-api/src/endpoint/memories.rs @@ -71,7 +71,6 @@ mod tests { use crate::common::RawMemory; use crate::common::RawMemoryMetadata; use crate::provider::RetryConfig; - use async_trait::async_trait; use codex_client::Request; use codex_client::RequestBody; use codex_client::Response; @@ -89,7 +88,6 @@ mod tests { #[derive(Clone, Default)] struct DummyTransport; - #[async_trait] impl HttpTransport for DummyTransport { async fn execute(&self, _req: Request) -> Result { Err(TransportError::Build("execute should not run".to_string())) @@ -122,7 +120,6 @@ mod tests { } } - #[async_trait] impl HttpTransport for CapturingTransport { async fn execute(&self, req: Request) -> Result { *self.last_request.lock().expect("lock request store") = Some(req); diff --git a/codex-rs/codex-api/src/endpoint/models.rs b/codex-rs/codex-api/src/endpoint/models.rs index ec9ee7aac6d3..2f1b799c3ba4 100644 --- a/codex-rs/codex-api/src/endpoint/models.rs +++ b/codex-rs/codex-api/src/endpoint/models.rs @@ -78,7 +78,6 @@ mod tests { use super::*; use crate::auth::AuthProvider; use crate::provider::RetryConfig; - use async_trait::async_trait; use codex_client::Request; use codex_client::Response; use codex_client::StreamResponse; @@ -108,7 +107,6 @@ mod tests { } } - #[async_trait] impl HttpTransport for CapturingTransport { async fn execute(&self, req: Request) -> Result { *self.last_request.lock().unwrap() = Some(req); diff --git a/codex-rs/codex-api/src/endpoint/realtime_call.rs b/codex-rs/codex-api/src/endpoint/realtime_call.rs index b0342c53498d..227b85c70c05 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_call.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_call.rs @@ -226,7 +226,6 @@ mod tests { use crate::endpoint::realtime_websocket::RealtimeOutputModality; use crate::endpoint::realtime_websocket::RealtimeSessionMode; use crate::provider::RetryConfig; - use async_trait::async_trait; use codex_client::Request; use codex_client::Response; use codex_client::StreamResponse; @@ -265,7 +264,6 @@ mod tests { } } - #[async_trait] impl HttpTransport for CapturingTransport { async fn execute(&self, req: Request) -> Result { *self.last_request.lock().unwrap() = Some(req); diff --git a/codex-rs/codex-api/src/endpoint/search.rs b/codex-rs/codex-api/src/endpoint/search.rs index 143048c9545a..860f4f283e66 100644 --- a/codex-rs/codex-api/src/endpoint/search.rs +++ b/codex-rs/codex-api/src/endpoint/search.rs @@ -64,7 +64,6 @@ mod tests { use crate::search::SearchInput; use crate::search::SearchQuery; use crate::search::SearchSettings; - use async_trait::async_trait; use codex_client::Request; use codex_client::RequestBody; use codex_client::Response; @@ -100,7 +99,6 @@ mod tests { } } - #[async_trait] impl HttpTransport for CapturingTransport { async fn execute(&self, req: Request) -> Result { *self.last_request.lock().expect("lock request store") = Some(req); diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs index d2d767940647..cc9b58c0343b 100644 --- a/codex-rs/codex-api/src/lib.rs +++ b/codex-rs/codex-api/src/lib.rs @@ -21,6 +21,7 @@ pub use crate::api_bridge::map_api_error; pub use crate::auth::AuthError; pub use crate::auth::AuthHeaderTelemetry; pub use crate::auth::AuthProvider; +pub use crate::auth::AuthProviderFuture; pub use crate::auth::SharedAuthProvider; pub use crate::auth::auth_header_telemetry; pub use crate::common::CompactionInput; diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index afaf82389f00..34284634f7c8 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -3,7 +3,6 @@ use std::sync::Mutex; use std::time::Duration; use anyhow::Result; -use async_trait::async_trait; use bytes::Bytes; use codex_api::ApiError; use codex_api::AuthError; @@ -71,7 +70,6 @@ impl RecordingTransport { } } -#[async_trait] impl HttpTransport for RecordingTransport { async fn execute(&self, _req: Request) -> Result { Err(TransportError::Build("execute should not run".to_string())) @@ -195,11 +193,6 @@ impl FailsOnceAuth { .lock() .unwrap_or_else(|err| panic!("mutex poisoned: {err}")) } -} - -#[async_trait] -impl AuthProvider for FailsOnceAuth { - fn add_auth_headers(&self, _headers: &mut HeaderMap) {} async fn apply_auth(&self, request: Request) -> Result { let mut attempts = self @@ -219,7 +212,14 @@ impl AuthProvider for FailsOnceAuth { } } -#[async_trait] +impl AuthProvider for FailsOnceAuth { + fn add_auth_headers(&self, _headers: &mut HeaderMap) {} + + fn apply_auth(&self, request: Request) -> codex_api::AuthProviderFuture<'_> { + Box::pin(FailsOnceAuth::apply_auth(self, request)) + } +} + impl HttpTransport for FlakyTransport { async fn execute(&self, _req: Request) -> Result { Err(TransportError::Build("execute should not run".to_string())) diff --git a/codex-rs/codex-api/tests/sse_end_to_end.rs b/codex-rs/codex-api/tests/sse_end_to_end.rs index bf880fefcf9f..eef8c8823004 100644 --- a/codex-rs/codex-api/tests/sse_end_to_end.rs +++ b/codex-rs/codex-api/tests/sse_end_to_end.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Result; -use async_trait::async_trait; use bytes::Bytes; use codex_api::AuthProvider; use codex_api::Compression; @@ -32,7 +31,6 @@ impl FixtureSseTransport { } } -#[async_trait] impl HttpTransport for FixtureSseTransport { async fn execute(&self, _req: Request) -> Result { Err(TransportError::Build("execute should not run".to_string())) diff --git a/codex-rs/codex-client/Cargo.toml b/codex-rs/codex-client/Cargo.toml index 184505eb5591..87f95de655ec 100644 --- a/codex-rs/codex-client/Cargo.toml +++ b/codex-rs/codex-client/Cargo.toml @@ -5,7 +5,6 @@ name = "codex-client" version.workspace = true [dependencies] -async-trait = { workspace = true } bytes = { workspace = true } eventsource-stream = { workspace = true } futures = { workspace = true } diff --git a/codex-rs/codex-client/src/transport.rs b/codex-rs/codex-client/src/transport.rs index 4ed062c483bb..00b84a614b97 100644 --- a/codex-rs/codex-client/src/transport.rs +++ b/codex-rs/codex-client/src/transport.rs @@ -4,7 +4,6 @@ use crate::error::TransportError; use crate::request::Request; use crate::request::RequestBody; use crate::request::Response; -use async_trait::async_trait; use bytes::Bytes; use futures::StreamExt; use futures::stream::BoxStream; @@ -23,10 +22,15 @@ pub struct StreamResponse { pub bytes: ByteStream, } -#[async_trait] pub trait HttpTransport: Send + Sync { - async fn execute(&self, req: Request) -> Result; - async fn stream(&self, req: Request) -> Result; + fn execute( + &self, + req: Request, + ) -> impl std::future::Future> + Send; + fn stream( + &self, + req: Request, + ) -> impl std::future::Future> + Send; } #[derive(Clone, Debug)] @@ -86,7 +90,6 @@ fn request_body_for_trace(req: &Request) -> String { } } -#[async_trait] impl HttpTransport for ReqwestTransport { async fn execute(&self, req: Request) -> Result { if enabled!(Level::TRACE) { diff --git a/codex-rs/config/Cargo.toml b/codex-rs/config/Cargo.toml index e2c650dcccc4..54ad61f94d37 100644 --- a/codex-rs/config/Cargo.toml +++ b/codex-rs/config/Cargo.toml @@ -13,7 +13,6 @@ workspace = true [dependencies] anyhow = { workspace = true } -async-trait = { workspace = true } base64 = { workspace = true } codex-app-server-protocol = { workspace = true } codex-execpolicy = { workspace = true } diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index 0c79ffacffc9..ed26757715f8 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -150,6 +150,7 @@ pub use thread_config::ThreadConfigContext; pub use thread_config::ThreadConfigLoadError; pub use thread_config::ThreadConfigLoadErrorCode; pub use thread_config::ThreadConfigLoader; +pub use thread_config::ThreadConfigLoaderFuture; pub use thread_config::ThreadConfigSource; pub use thread_config::UserThreadConfig; pub use toml::Value as TomlValue; diff --git a/codex-rs/config/src/loader/tests.rs b/codex-rs/config/src/loader/tests.rs index e349ba0c6856..88c5b16522db 100644 --- a/codex-rs/config/src/loader/tests.rs +++ b/codex-rs/config/src/loader/tests.rs @@ -1,9 +1,8 @@ use super::*; -use async_trait::async_trait; use codex_file_system::CopyOptions; use codex_file_system::CreateDirectoryOptions; +use codex_file_system::ExecutorFileSystemFuture; use codex_file_system::FileMetadata; -use codex_file_system::FileSystemResult; use codex_file_system::FileSystemSandboxContext; use codex_file_system::ReadDirectoryEntry; use codex_file_system::RemoveOptions; @@ -13,78 +12,81 @@ use tempfile::tempdir; struct TestFileSystem; -#[async_trait] impl ExecutorFileSystem for TestFileSystem { - async fn canonicalize( - &self, - path: &PathUri, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult { - let path = path.to_abs_path()?; - let canonicalized = path.canonicalize()?; - PathUri::from_abs_path(&canonicalized) + fn canonicalize<'a>( + &'a self, + path: &'a PathUri, + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, PathUri> { + Box::pin(async move { + let path = path.to_abs_path()?; + let canonicalized = path.canonicalize()?; + PathUri::from_abs_path(&canonicalized) + }) } - async fn read_file( - &self, - path: &PathUri, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult> { - let path = path.to_abs_path()?; - tokio::fs::read(path.as_path()).await + fn read_file<'a>( + &'a self, + path: &'a PathUri, + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(async move { + let path = path.to_abs_path()?; + tokio::fs::read(path.as_path()).await + }) } - async fn write_file( - &self, - _path: &PathUri, + fn write_file<'a>( + &'a self, + _path: &'a PathUri, _contents: Vec, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()> { - unimplemented!("test filesystem only supports reads") + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(async move { unimplemented!("test filesystem only supports reads") }) } - async fn create_directory( - &self, - _path: &PathUri, + fn create_directory<'a>( + &'a self, + _path: &'a PathUri, _create_directory_options: CreateDirectoryOptions, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()> { - unimplemented!("test filesystem only supports reads") + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(async move { unimplemented!("test filesystem only supports reads") }) } - async fn get_metadata( - &self, - _path: &PathUri, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult { - unimplemented!("test filesystem only supports reads") + fn get_metadata<'a>( + &'a self, + _path: &'a PathUri, + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, FileMetadata> { + Box::pin(async move { unimplemented!("test filesystem only supports reads") }) } - async fn read_directory( - &self, - _path: &PathUri, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult> { - unimplemented!("test filesystem only supports reads") + fn read_directory<'a>( + &'a self, + _path: &'a PathUri, + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(async move { unimplemented!("test filesystem only supports reads") }) } - async fn remove( - &self, - _path: &PathUri, + fn remove<'a>( + &'a self, + _path: &'a PathUri, _remove_options: RemoveOptions, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()> { - unimplemented!("test filesystem only supports reads") + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(async move { unimplemented!("test filesystem only supports reads") }) } - async fn copy( - &self, - _source_path: &PathUri, - _destination_path: &PathUri, + fn copy<'a>( + &'a self, + _source_path: &'a PathUri, + _destination_path: &'a PathUri, _copy_options: CopyOptions, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()> { - unimplemented!("test filesystem only supports reads") + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(async move { unimplemented!("test filesystem only supports reads") }) } } diff --git a/codex-rs/config/src/thread_config.rs b/codex-rs/config/src/thread_config.rs index 1b3ea8fe8712..273680d5ec6e 100644 --- a/codex-rs/config/src/thread_config.rs +++ b/codex-rs/config/src/thread_config.rs @@ -1,7 +1,8 @@ use std::collections::BTreeMap; use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; -use async_trait::async_trait; use codex_app_server_protocol::ConfigLayerSource; use codex_model_provider_info::ModelProviderInfo; use codex_utils_absolute_path::AbsolutePathBuf; @@ -86,7 +87,6 @@ impl ThreadConfigLoadError { /// return typed payloads without applying precedence or merge rules. Callers /// are responsible for resolving the returned sources into the effective /// runtime config. -#[async_trait] pub trait ThreadConfigLoader: Send + Sync { /// Load source-specific typed config. /// @@ -94,24 +94,29 @@ pub trait ThreadConfigLoader: Send + Sync { /// their owned sources. Most callers should use [`Self::load_config_layers`] /// so precedence and merging continue through the ordinary config layer /// stack. - async fn load( + fn load( &self, context: ThreadConfigContext, - ) -> Result, ThreadConfigLoadError>; + ) -> ThreadConfigLoaderFuture<'_, Vec>; - async fn load_config_layers( + fn load_config_layers( &self, context: ThreadConfigContext, - ) -> Result, ThreadConfigLoadError> { - let sources = self.load(context).await?; - sources - .into_iter() - .map(thread_config_source_to_layer) - .collect::, _>>() - .map(|layers| layers.into_iter().flatten().collect()) + ) -> ThreadConfigLoaderFuture<'_, Vec> { + Box::pin(async move { + let sources = self.load(context).await?; + sources + .into_iter() + .map(thread_config_source_to_layer) + .collect::, _>>() + .map(|layers| layers.into_iter().flatten().collect()) + }) } } +pub type ThreadConfigLoaderFuture<'a, T> = + Pin> + Send + 'a>>; + /// Loader backed by a static set of typed thread config sources. #[derive(Clone, Debug, Default, PartialEq)] pub struct StaticThreadConfigLoader { @@ -124,13 +129,12 @@ impl StaticThreadConfigLoader { } } -#[async_trait] impl ThreadConfigLoader for StaticThreadConfigLoader { - async fn load( + fn load( &self, _context: ThreadConfigContext, - ) -> Result, ThreadConfigLoadError> { - Ok(self.sources.clone()) + ) -> ThreadConfigLoaderFuture<'_, Vec> { + Box::pin(async { Ok(self.sources.clone()) }) } } @@ -138,13 +142,12 @@ impl ThreadConfigLoader for StaticThreadConfigLoader { #[derive(Clone, Debug, Default)] pub struct NoopThreadConfigLoader; -#[async_trait] impl ThreadConfigLoader for NoopThreadConfigLoader { - async fn load( + fn load( &self, _context: ThreadConfigContext, - ) -> Result, ThreadConfigLoadError> { - Ok(Vec::new()) + ) -> ThreadConfigLoaderFuture<'_, Vec> { + Box::pin(async { Ok(Vec::new()) }) } } diff --git a/codex-rs/config/src/thread_config/remote.rs b/codex-rs/config/src/thread_config/remote.rs index 7b7feacec5ec..7a8e4ce88fa6 100644 --- a/codex-rs/config/src/thread_config/remote.rs +++ b/codex-rs/config/src/thread_config/remote.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use std::num::NonZeroU64; use std::time::Duration; -use async_trait::async_trait; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; use codex_protocol::config_types::ModelProviderAuthInfo; @@ -14,6 +13,7 @@ use super::ThreadConfigContext; use super::ThreadConfigLoadError; use super::ThreadConfigLoadErrorCode; use super::ThreadConfigLoader; +use super::ThreadConfigLoaderFuture; use super::ThreadConfigSource; use super::UserThreadConfig; use proto::thread_config_loader_client::ThreadConfigLoaderClient; @@ -49,10 +49,7 @@ impl RemoteThreadConfigLoader { ) }) } -} -#[async_trait] -impl ThreadConfigLoader for RemoteThreadConfigLoader { async fn load( &self, context: ThreadConfigContext, @@ -73,6 +70,15 @@ impl ThreadConfigLoader for RemoteThreadConfigLoader { } } +impl ThreadConfigLoader for RemoteThreadConfigLoader { + fn load( + &self, + context: ThreadConfigContext, + ) -> ThreadConfigLoaderFuture<'_, Vec> { + Box::pin(RemoteThreadConfigLoader::load(self, context)) + } +} + fn load_thread_config_request( context: ThreadConfigContext, ) -> tonic::Request { @@ -321,8 +327,7 @@ mod tests { expected_cwd: String, } - #[tonic::async_trait] - impl thread_config_loader_server::ThreadConfigLoader for TestServer { + impl TestServer { async fn load( &self, request: Request, @@ -341,6 +346,26 @@ mod tests { } } + impl thread_config_loader_server::ThreadConfigLoader for TestServer { + fn load<'a, 'async_trait>( + &'a self, + request: Request, + ) -> std::pin::Pin< + Box< + dyn std::future::Future< + Output = Result, Status>, + > + Send + + 'async_trait, + >, + > + where + 'a: 'async_trait, + Self: 'async_trait, + { + Box::pin(TestServer::load(self, request)) + } + } + #[tokio::test] async fn load_thread_config_calls_remote_service() { let cwd = workspace_dir().join("project"); diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 5174395e3438..d21245c63ecb 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -19,7 +19,6 @@ workspace = true anyhow = { workspace = true } arc-swap = { workspace = true } async-channel = { workspace = true } -async-trait = { workspace = true } base64 = { workspace = true } bm25 = { workspace = true } chrono = { workspace = true, features = ["serde"] } diff --git a/codex-rs/core/src/agents_md_tests.rs b/codex-rs/core/src/agents_md_tests.rs index 3788a0f6dc98..1db1f29215a8 100644 --- a/codex-rs/core/src/agents_md_tests.rs +++ b/codex-rs/core/src/agents_md_tests.rs @@ -1,12 +1,12 @@ use super::*; use crate::config::ConfigBuilder; -use async_trait::async_trait; use codex_config::ConfigLayerEntry; use codex_config::ConfigLayerStack; use codex_config::ConfigRequirements; use codex_config::ConfigRequirementsToml; use codex_exec_server::CopyOptions; use codex_exec_server::CreateDirectoryOptions; +use codex_exec_server::ExecutorFileSystemFuture; use codex_exec_server::FileMetadata; use codex_exec_server::FileSystemSandboxContext; use codex_exec_server::LOCAL_FS; @@ -39,8 +39,7 @@ struct FailingFileSystem { failure: InjectedFailure, } -#[async_trait] -impl ExecutorFileSystem for FailingFileSystem { +impl FailingFileSystem { async fn canonicalize( &self, _path: &PathUri, @@ -121,6 +120,85 @@ impl ExecutorFileSystem for FailingFileSystem { } } +impl ExecutorFileSystem for FailingFileSystem { + fn canonicalize<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, PathUri> { + Box::pin(FailingFileSystem::canonicalize(self, path, sandbox)) + } + + fn read_file<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(FailingFileSystem::read_file(self, path, sandbox)) + } + + fn write_file<'a>( + &'a self, + path: &'a PathUri, + contents: Vec, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(FailingFileSystem::write_file(self, path, contents, sandbox)) + } + + fn create_directory<'a>( + &'a self, + path: &'a PathUri, + options: CreateDirectoryOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(FailingFileSystem::create_directory( + self, path, options, sandbox, + )) + } + + fn get_metadata<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, FileMetadata> { + Box::pin(FailingFileSystem::get_metadata(self, path, sandbox)) + } + + fn read_directory<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(FailingFileSystem::read_directory(self, path, sandbox)) + } + + fn remove<'a>( + &'a self, + path: &'a PathUri, + options: RemoveOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(FailingFileSystem::remove(self, path, options, sandbox)) + } + + fn copy<'a>( + &'a self, + source_path: &'a PathUri, + destination_path: &'a PathUri, + options: CopyOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(FailingFileSystem::copy( + self, + source_path, + destination_path, + options, + sandbox, + )) + } +} + struct TestConfig { config: Config, user_instructions: Option, diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index 631a826ac712..653e6987f308 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -1,8 +1,8 @@ -use async_trait::async_trait; use codex_config::NetworkConstraints; use codex_execpolicy::Policy; use codex_network_proxy::BlockedRequestObserver; use codex_network_proxy::ConfigReloader; +use codex_network_proxy::ConfigReloaderFuture; use codex_network_proxy::ConfigState; use codex_network_proxy::NetworkDecision; use codex_network_proxy::NetworkPolicyDecider; @@ -58,14 +58,13 @@ impl StaticNetworkProxyReloader { } } -#[async_trait] impl ConfigReloader for StaticNetworkProxyReloader { - async fn maybe_reload(&self) -> anyhow::Result> { - Ok(None) + fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option> { + Box::pin(async { Ok(None) }) } - async fn reload_now(&self) -> anyhow::Result { - Ok(self.state.clone()) + fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> { + Box::pin(async { Ok(self.state.clone()) }) } fn source_label(&self) -> String { diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs index b91eaa26ca3d..e0ffe99259c3 100644 --- a/codex-rs/core/src/network_proxy_loader.rs +++ b/codex-rs/core/src/network_proxy_loader.rs @@ -7,7 +7,6 @@ use crate::exec_policy::format_exec_policy_error_with_source; use crate::exec_policy::load_exec_policy; use anyhow::Context; use anyhow::Result; -use async_trait::async_trait; use codex_app_server_protocol::ConfigLayerSource; use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigLayerStack; @@ -23,6 +22,7 @@ use codex_config::permissions_toml::PermissionsToml; use codex_config::permissions_toml::overlay_network_domain_permissions; use codex_exec_server::LOCAL_FS; use codex_network_proxy::ConfigReloader; +use codex_network_proxy::ConfigReloaderFuture; use codex_network_proxy::ConfigState; use codex_network_proxy::NetworkMode; use codex_network_proxy::NetworkProxyConfig; @@ -361,13 +361,6 @@ impl MtimeConfigReloader { } }) } -} - -#[async_trait] -impl ConfigReloader for MtimeConfigReloader { - fn source_label(&self) -> String { - "config layers".to_string() - } async fn maybe_reload(&self) -> Result> { if !self.needs_reload().await { @@ -388,6 +381,20 @@ impl ConfigReloader for MtimeConfigReloader { } } +impl ConfigReloader for MtimeConfigReloader { + fn source_label(&self) -> String { + "config layers".to_string() + } + + fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option> { + Box::pin(MtimeConfigReloader::maybe_reload(self)) + } + + fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> { + Box::pin(MtimeConfigReloader::reload_now(self)) + } +} + #[cfg(test)] #[path = "network_proxy_loader_tests.rs"] mod tests; diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index 011da46b3d44..99cece3e9205 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -10,6 +10,7 @@ use crate::tools::sandboxing::managed_network_for_sandbox_permissions; use codex_network_proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER; use codex_network_proxy::CUSTOM_CA_ENV_KEYS; use codex_network_proxy::ConfigReloader; +use codex_network_proxy::ConfigReloaderFuture; use codex_network_proxy::ConfigState; use codex_network_proxy::NetworkProxy; use codex_network_proxy::NetworkProxyConfig; @@ -35,18 +36,17 @@ use tokio::sync::watch; struct StaticReloader; -#[async_trait::async_trait] impl ConfigReloader for StaticReloader { fn source_label(&self) -> String { "test config state".to_string() } - async fn maybe_reload(&self) -> anyhow::Result> { - Ok(None) + fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option> { + Box::pin(async { Ok(None) }) } - async fn reload_now(&self) -> anyhow::Result { - Err(anyhow::anyhow!("force reload is not supported in tests")) + fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> { + Box::pin(async { Err(anyhow::anyhow!("force reload is not supported in tests")) }) } } diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 71fa89f98030..960e1d3943a0 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -56,12 +56,14 @@ use codex_shell_escalation::EscalationDecision; use codex_shell_escalation::EscalationExecution; use codex_shell_escalation::EscalationPermissions; use codex_shell_escalation::EscalationPolicy; +use codex_shell_escalation::EscalationPolicyFuture; use codex_shell_escalation::EscalationSession; use codex_shell_escalation::ExecParams; use codex_shell_escalation::ExecResult; use codex_shell_escalation::PreparedExec; use codex_shell_escalation::ResolvedPermissionProfile; use codex_shell_escalation::ShellCommandExecutor; +use codex_shell_escalation::ShellCommandExecutorFuture; use codex_shell_escalation::Stopwatch; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; @@ -597,8 +599,7 @@ impl CoreShellActionProvider { // execve interception. const ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING: bool = false; -#[async_trait::async_trait] -impl EscalationPolicy for CoreShellActionProvider { +impl CoreShellActionProvider { async fn determine_action( &self, program: &AbsolutePathBuf, @@ -665,6 +666,19 @@ impl EscalationPolicy for CoreShellActionProvider { } } +impl EscalationPolicy for CoreShellActionProvider { + fn determine_action<'a>( + &'a self, + program: &'a AbsolutePathBuf, + argv: &'a [String], + workdir: &'a AbsolutePathBuf, + ) -> EscalationPolicyFuture<'a> { + Box::pin(CoreShellActionProvider::determine_action( + self, program, argv, workdir, + )) + } +} + fn evaluate_intercepted_exec_policy( policy: &Policy, program: &AbsolutePathBuf, @@ -787,15 +801,43 @@ struct PrepareSandboxedExecParams<'a> { additional_permissions: Option, } -#[async_trait::async_trait] impl ShellCommandExecutor for CoreShellCommandExecutor { - async fn run( + fn run( &self, _command: Vec, _cwd: PathBuf, env_overlay: HashMap, cancel_rx: CancellationToken, after_spawn: Option>, + ) -> ShellCommandExecutorFuture<'_, ExecResult> { + Box::pin(CoreShellCommandExecutor::run( + self, + env_overlay, + cancel_rx, + after_spawn, + )) + } + + fn prepare_escalated_exec<'a>( + &'a self, + program: &'a AbsolutePathBuf, + argv: &'a [String], + workdir: &'a AbsolutePathBuf, + env: HashMap, + execution: EscalationExecution, + ) -> ShellCommandExecutorFuture<'a, PreparedExec> { + Box::pin(CoreShellCommandExecutor::prepare_escalated_exec( + self, program, argv, workdir, env, execution, + )) + } +} + +impl CoreShellCommandExecutor { + async fn run( + &self, + env_overlay: HashMap, + cancel_rx: CancellationToken, + after_spawn: Option>, ) -> anyhow::Result { let mut exec_env = self.env.clone(); // `env_overlay` comes from `EscalationSession::env()`, so merge only the @@ -900,9 +942,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { Ok(prepared) } -} -impl CoreShellCommandExecutor { #[allow(clippy::too_many_arguments)] fn prepare_sandboxed_exec( &self, diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index ee5b9e7db32a..6d42e7c84dc1 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -38,7 +38,6 @@ use codex_shell_escalation::EscalationExecution; use codex_shell_escalation::EscalationPermissions; use codex_shell_escalation::ExecResult; use codex_shell_escalation::ResolvedPermissionProfile; -use codex_shell_escalation::ShellCommandExecutor; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::Value; diff --git a/codex-rs/core/src/unified_exec/mod_tests.rs b/codex-rs/core/src/unified_exec/mod_tests.rs index d994354130bf..ad39d62b9663 100644 --- a/codex-rs/core/src/unified_exec/mod_tests.rs +++ b/codex-rs/core/src/unified_exec/mod_tests.rs @@ -10,10 +10,9 @@ use crate::session::turn_context::TurnContext; use crate::tools::context::ExecCommandToolOutput; use crate::unified_exec::WriteStdinRequest; use crate::unified_exec::process::OutputHandles; -use async_trait::async_trait; use codex_exec_server::ExecProcess; use codex_exec_server::ExecProcessEventReceiver; -use codex_exec_server::ExecServerError; +use codex_exec_server::ExecProcessFuture; use codex_exec_server::ProcessId; use codex_exec_server::ProcessSignal; use codex_exec_server::ReadResponse; @@ -218,7 +217,31 @@ struct BlockingTerminateExecProcess { wake_tx: watch::Sender, } -#[async_trait] +impl BlockingTerminateExecProcess { + async fn read(&self) -> Result { + Ok(ReadResponse { + chunks: Vec::new(), + next_seq: 1, + exited: false, + exit_code: None, + closed: false, + failure: None, + }) + } + + async fn write(&self) -> Result { + Ok(WriteResponse { + status: WriteStatus::Accepted, + }) + } + + async fn terminate(&self) -> Result<(), codex_exec_server::ExecServerError> { + let _ = self.terminate_started.send(true); + self.allow_terminate.notified().await; + Ok(()) + } +} + impl ExecProcess for BlockingTerminateExecProcess { fn process_id(&self) -> &ProcessId { &self.process_id @@ -232,36 +255,25 @@ impl ExecProcess for BlockingTerminateExecProcess { ExecProcessEventReceiver::empty() } - async fn read( + fn read( &self, _after_seq: Option, _max_bytes: Option, _wait_ms: Option, - ) -> Result { - Ok(ReadResponse { - chunks: Vec::new(), - next_seq: 1, - exited: false, - exit_code: None, - closed: false, - failure: None, - }) + ) -> ExecProcessFuture<'_, ReadResponse> { + Box::pin(BlockingTerminateExecProcess::read(self)) } - async fn write(&self, _chunk: Vec) -> Result { - Ok(WriteResponse { - status: WriteStatus::Accepted, - }) + fn write(&self, _chunk: Vec) -> ExecProcessFuture<'_, WriteResponse> { + Box::pin(BlockingTerminateExecProcess::write(self)) } - async fn signal(&self, _signal: ProcessSignal) -> Result<(), ExecServerError> { - Ok(()) + fn signal(&self, _signal: ProcessSignal) -> ExecProcessFuture<'_, ()> { + Box::pin(async { Ok(()) }) } - async fn terminate(&self) -> Result<(), ExecServerError> { - let _ = self.terminate_started.send(true); - self.allow_terminate.notified().await; - Ok(()) + fn terminate(&self) -> ExecProcessFuture<'_, ()> { + Box::pin(BlockingTerminateExecProcess::terminate(self)) } } diff --git a/codex-rs/core/src/unified_exec/process_tests.rs b/codex-rs/core/src/unified_exec/process_tests.rs index 814de95af4b8..37ec5d858b22 100644 --- a/codex-rs/core/src/unified_exec/process_tests.rs +++ b/codex-rs/core/src/unified_exec/process_tests.rs @@ -1,8 +1,8 @@ use super::process::UnifiedExecProcess; use crate::unified_exec::UnifiedExecError; -use async_trait::async_trait; use codex_exec_server::ExecProcess; use codex_exec_server::ExecProcessEventReceiver; +use codex_exec_server::ExecProcessFuture; use codex_exec_server::ExecServerError; use codex_exec_server::ProcessId; use codex_exec_server::ProcessSignal; @@ -26,7 +26,31 @@ struct MockExecProcess { wake_tx: watch::Sender, } -#[async_trait] +impl MockExecProcess { + async fn read(&self) -> Result { + Ok(self + .read_responses + .lock() + .await + .pop_front() + .unwrap_or(ReadResponse { + chunks: Vec::new(), + next_seq: 1, + exited: false, + exit_code: None, + closed: false, + failure: None, + })) + } + + async fn terminate(&self) -> Result<(), ExecServerError> { + if let Some(message) = &self.terminate_error { + return Err(ExecServerError::Protocol(message.clone())); + } + Ok(()) + } +} + impl ExecProcess for MockExecProcess { fn process_id(&self) -> &ProcessId { &self.process_id @@ -40,40 +64,25 @@ impl ExecProcess for MockExecProcess { ExecProcessEventReceiver::empty() } - async fn read( + fn read( &self, _after_seq: Option, _max_bytes: Option, _wait_ms: Option, - ) -> Result { - Ok(self - .read_responses - .lock() - .await - .pop_front() - .unwrap_or(ReadResponse { - chunks: Vec::new(), - next_seq: 1, - exited: false, - exit_code: None, - closed: false, - failure: None, - })) + ) -> ExecProcessFuture<'_, ReadResponse> { + Box::pin(MockExecProcess::read(self)) } - async fn write(&self, _chunk: Vec) -> Result { - Ok(self.write_response.clone()) + fn write(&self, _chunk: Vec) -> ExecProcessFuture<'_, WriteResponse> { + Box::pin(async { Ok(self.write_response.clone()) }) } - async fn signal(&self, _signal: ProcessSignal) -> Result<(), ExecServerError> { - Ok(()) + fn signal(&self, _signal: ProcessSignal) -> ExecProcessFuture<'_, ()> { + Box::pin(async { Ok(()) }) } - async fn terminate(&self) -> Result<(), ExecServerError> { - if let Some(message) = &self.terminate_error { - return Err(ExecServerError::Protocol(message.clone())); - } - Ok(()) + fn terminate(&self) -> ExecProcessFuture<'_, ()> { + Box::pin(MockExecProcess::terminate(self)) } } diff --git a/codex-rs/deny.toml b/codex-rs/deny.toml index 983fd5b96244..9d06651e587e 100644 --- a/codex-rs/deny.toml +++ b/codex-rs/deny.toml @@ -221,6 +221,16 @@ deny = [ # Wrapper crates can optionally be specified to allow the crate when it # is a direct dependency of the otherwise banned crate #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, + # Removing async-trait materially improves debug build times: + # https://github.com/openai/codex/pull/27304 + { crate = "async-trait", wrappers = [ + "hickory-proto", + "opentelemetry-http", + "pagable", + "rmcp", + "tonic", + "zbus", + ], reason = "first-party traits must use native async trait methods with explicit Send bounds" }, ] # List of features to allow/deny diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 3173ad1bdda7..687a1ee59c7b 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -12,7 +12,6 @@ workspace = true [dependencies] arc-swap = { workspace = true } -async-trait = { workspace = true } axum = { workspace = true, features = ["http1", "tokio", "ws"] } base64 = { workspace = true } bytes = { workspace = true } diff --git a/codex-rs/exec-server/src/environment_provider.rs b/codex-rs/exec-server/src/environment_provider.rs index 7e132ee2b4c8..ffb5cbd1161d 100644 --- a/codex-rs/exec-server/src/environment_provider.rs +++ b/codex-rs/exec-server/src/environment_provider.rs @@ -1,4 +1,5 @@ -use async_trait::async_trait; +use std::future::Future; +use std::pin::Pin; use crate::Environment; use crate::ExecServerError; @@ -13,12 +14,14 @@ use crate::environment::REMOTE_ENVIRONMENT_ID; /// selection. Providers should only return provider-owned remote environments; /// `include_local` controls whether `EnvironmentManager` should add the local /// environment to the snapshot. -#[async_trait] pub trait EnvironmentProvider: Send + Sync { /// Returns the provider-owned environment startup snapshot. - async fn snapshot(&self) -> Result; + fn snapshot(&self) -> EnvironmentProviderFuture<'_>; } +pub type EnvironmentProviderFuture<'a> = + Pin> + Send + 'a>>; + #[derive(Clone, Debug)] pub struct EnvironmentProviderSnapshot { pub environments: Vec<(String, Environment)>, @@ -80,10 +83,9 @@ impl DefaultEnvironmentProvider { } } -#[async_trait] impl EnvironmentProvider for DefaultEnvironmentProvider { - async fn snapshot(&self) -> Result { - Ok(self.snapshot_inner()) + fn snapshot(&self) -> EnvironmentProviderFuture<'_> { + Box::pin(async { Ok(self.snapshot_inner()) }) } } diff --git a/codex-rs/exec-server/src/environment_toml.rs b/codex-rs/exec-server/src/environment_toml.rs index 26c178b5b8c2..87d5bea8b343 100644 --- a/codex-rs/exec-server/src/environment_toml.rs +++ b/codex-rs/exec-server/src/environment_toml.rs @@ -4,13 +4,13 @@ use std::path::Path; use std::path::PathBuf; use std::time::Duration; -use async_trait::async_trait; use serde::Deserialize; use tokio_tungstenite::tungstenite::client::IntoClientRequest; use crate::DefaultEnvironmentProvider; use crate::Environment; use crate::EnvironmentProvider; +use crate::EnvironmentProviderFuture; use crate::ExecServerError; use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT; use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT; @@ -92,10 +92,7 @@ impl TomlEnvironmentProvider { environments: parsed_environments, }) } -} -#[async_trait] -impl EnvironmentProvider for TomlEnvironmentProvider { async fn snapshot(&self) -> Result { let mut environments = Vec::with_capacity(self.environments.len()); for (id, transport_params) in &self.environments { @@ -116,6 +113,12 @@ impl EnvironmentProvider for TomlEnvironmentProvider { } } +impl EnvironmentProvider for TomlEnvironmentProvider { + fn snapshot(&self) -> EnvironmentProviderFuture<'_> { + Box::pin(TomlEnvironmentProvider::snapshot(self)) + } +} + fn parse_environment_toml( item: EnvironmentToml, config_dir: Option<&Path>, diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 6c322fd41fbe..d6a9aacb5114 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -33,6 +33,7 @@ pub use client_api::RemoteExecServerConnectArgs; pub use codex_file_system::CopyOptions; pub use codex_file_system::CreateDirectoryOptions; pub use codex_file_system::ExecutorFileSystem; +pub use codex_file_system::ExecutorFileSystemFuture; pub use codex_file_system::FileMetadata; pub use codex_file_system::FileSystemResult; pub use codex_file_system::FileSystemSandboxContext; @@ -45,14 +46,17 @@ pub use environment::LOCAL_ENVIRONMENT_ID; pub use environment::REMOTE_ENVIRONMENT_ID; pub use environment_provider::DefaultEnvironmentProvider; pub use environment_provider::EnvironmentProvider; +pub use environment_provider::EnvironmentProviderFuture; pub use fs_helper::CODEX_FS_HELPER_ARG1; pub use fs_helper_main::main as run_fs_helper_main; pub use local_file_system::LOCAL_FS; pub use local_file_system::LocalFileSystem; pub use process::ExecBackend; +pub use process::ExecBackendFuture; pub use process::ExecProcess; pub use process::ExecProcessEvent; pub use process::ExecProcessEventReceiver; +pub use process::ExecProcessFuture; pub use process::StartedExecProcess; pub use process_id::ProcessId; pub use protocol::EnvironmentInfo; diff --git a/codex-rs/exec-server/src/local_file_system.rs b/codex-rs/exec-server/src/local_file_system.rs index ee38d7ff9461..7b909cc8a65d 100644 --- a/codex-rs/exec-server/src/local_file_system.rs +++ b/codex-rs/exec-server/src/local_file_system.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; use std::path::Path; @@ -13,6 +12,7 @@ use crate::CopyOptions; use crate::CreateDirectoryOptions; use crate::ExecServerRuntimePaths; use crate::ExecutorFileSystem; +use crate::ExecutorFileSystemFuture; use crate::FileMetadata; use crate::FileSystemResult; use crate::FileSystemSandboxContext; @@ -78,8 +78,7 @@ impl LocalFileSystem { } } -#[async_trait] -impl ExecutorFileSystem for LocalFileSystem { +impl LocalFileSystem { async fn canonicalize( &self, path: &PathUri, @@ -160,8 +159,86 @@ impl ExecutorFileSystem for LocalFileSystem { } } -#[async_trait] -impl ExecutorFileSystem for UnsandboxedFileSystem { +impl ExecutorFileSystem for LocalFileSystem { + fn canonicalize<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, PathUri> { + Box::pin(LocalFileSystem::canonicalize(self, path, sandbox)) + } + + fn read_file<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(LocalFileSystem::read_file(self, path, sandbox)) + } + + fn write_file<'a>( + &'a self, + path: &'a PathUri, + contents: Vec, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(LocalFileSystem::write_file(self, path, contents, sandbox)) + } + + fn create_directory<'a>( + &'a self, + path: &'a PathUri, + options: CreateDirectoryOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(LocalFileSystem::create_directory( + self, path, options, sandbox, + )) + } + + fn get_metadata<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, FileMetadata> { + Box::pin(LocalFileSystem::get_metadata(self, path, sandbox)) + } + + fn read_directory<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(LocalFileSystem::read_directory(self, path, sandbox)) + } + + fn remove<'a>( + &'a self, + path: &'a PathUri, + options: RemoveOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(LocalFileSystem::remove(self, path, options, sandbox)) + } + + fn copy<'a>( + &'a self, + source_path: &'a PathUri, + destination_path: &'a PathUri, + options: CopyOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(LocalFileSystem::copy( + self, + source_path, + destination_path, + options, + sandbox, + )) + } +} + +impl UnsandboxedFileSystem { async fn canonicalize( &self, path: &PathUri, @@ -255,8 +332,88 @@ impl ExecutorFileSystem for UnsandboxedFileSystem { } } -#[async_trait] -impl ExecutorFileSystem for DirectFileSystem { +impl ExecutorFileSystem for UnsandboxedFileSystem { + fn canonicalize<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, PathUri> { + Box::pin(UnsandboxedFileSystem::canonicalize(self, path, sandbox)) + } + + fn read_file<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(UnsandboxedFileSystem::read_file(self, path, sandbox)) + } + + fn write_file<'a>( + &'a self, + path: &'a PathUri, + contents: Vec, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(UnsandboxedFileSystem::write_file( + self, path, contents, sandbox, + )) + } + + fn create_directory<'a>( + &'a self, + path: &'a PathUri, + options: CreateDirectoryOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(UnsandboxedFileSystem::create_directory( + self, path, options, sandbox, + )) + } + + fn get_metadata<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, FileMetadata> { + Box::pin(UnsandboxedFileSystem::get_metadata(self, path, sandbox)) + } + + fn read_directory<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(UnsandboxedFileSystem::read_directory(self, path, sandbox)) + } + + fn remove<'a>( + &'a self, + path: &'a PathUri, + options: RemoveOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(UnsandboxedFileSystem::remove(self, path, options, sandbox)) + } + + fn copy<'a>( + &'a self, + source_path: &'a PathUri, + destination_path: &'a PathUri, + options: CopyOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(UnsandboxedFileSystem::copy( + self, + source_path, + destination_path, + options, + sandbox, + )) + } +} + +impl DirectFileSystem { async fn canonicalize( &self, path: &PathUri, @@ -434,6 +591,85 @@ impl ExecutorFileSystem for DirectFileSystem { } } +impl ExecutorFileSystem for DirectFileSystem { + fn canonicalize<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, PathUri> { + Box::pin(DirectFileSystem::canonicalize(self, path, sandbox)) + } + + fn read_file<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(DirectFileSystem::read_file(self, path, sandbox)) + } + + fn write_file<'a>( + &'a self, + path: &'a PathUri, + contents: Vec, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(DirectFileSystem::write_file(self, path, contents, sandbox)) + } + + fn create_directory<'a>( + &'a self, + path: &'a PathUri, + options: CreateDirectoryOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(DirectFileSystem::create_directory( + self, path, options, sandbox, + )) + } + + fn get_metadata<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, FileMetadata> { + Box::pin(DirectFileSystem::get_metadata(self, path, sandbox)) + } + + fn read_directory<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(DirectFileSystem::read_directory(self, path, sandbox)) + } + + fn remove<'a>( + &'a self, + path: &'a PathUri, + options: RemoveOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(DirectFileSystem::remove(self, path, options, sandbox)) + } + + fn copy<'a>( + &'a self, + source_path: &'a PathUri, + destination_path: &'a PathUri, + options: CopyOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(DirectFileSystem::copy( + self, + source_path, + destination_path, + options, + sandbox, + )) + } +} + fn reject_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> { if sandbox.is_some() { return Err(io::Error::new( diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index 691f807032ce..60e6b8d912c0 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -4,7 +4,6 @@ use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; -use async_trait::async_trait; use codex_app_server_protocol::JSONRPCErrorError; use codex_protocol::config_types::EnvironmentVariablePattern; use codex_protocol::config_types::ShellEnvironmentPolicy; @@ -18,9 +17,11 @@ use tokio::sync::mpsc; use tokio::sync::watch; use crate::ExecBackend; +use crate::ExecBackendFuture; use crate::ExecProcess; use crate::ExecProcessEvent; use crate::ExecProcessEventReceiver; +use crate::ExecProcessFuture; use crate::ExecServerError; use crate::ProcessId; use crate::StartedExecProcess; @@ -460,8 +461,7 @@ fn shell_environment_policy(env_policy: &ExecEnvPolicy) -> ShellEnvironmentPolic } } -#[async_trait] -impl ExecBackend for LocalProcess { +impl LocalProcess { async fn start(&self, params: ExecParams) -> Result { let (response, wake_tx, events) = self .start_process(params) @@ -478,20 +478,13 @@ impl ExecBackend for LocalProcess { } } -#[async_trait] -impl ExecProcess for LocalExecProcess { - fn process_id(&self) -> &ProcessId { - &self.process_id - } - - fn subscribe_wake(&self) -> watch::Receiver { - self.wake_tx.subscribe() - } - - fn subscribe_events(&self) -> ExecProcessEventReceiver { - self.events.subscribe() +impl ExecBackend for LocalProcess { + fn start(&self, params: ExecParams) -> ExecBackendFuture<'_> { + Box::pin(LocalProcess::start(self, params)) } +} +impl LocalExecProcess { async fn read( &self, after_seq: Option, @@ -516,6 +509,41 @@ impl ExecProcess for LocalExecProcess { } } +impl ExecProcess for LocalExecProcess { + fn process_id(&self) -> &ProcessId { + &self.process_id + } + + fn subscribe_wake(&self) -> watch::Receiver { + self.wake_tx.subscribe() + } + + fn subscribe_events(&self) -> ExecProcessEventReceiver { + self.events.subscribe() + } + + fn read( + &self, + after_seq: Option, + max_bytes: Option, + wait_ms: Option, + ) -> ExecProcessFuture<'_, ReadResponse> { + Box::pin(LocalExecProcess::read(self, after_seq, max_bytes, wait_ms)) + } + + fn write(&self, chunk: Vec) -> ExecProcessFuture<'_, WriteResponse> { + Box::pin(LocalExecProcess::write(self, chunk)) + } + + fn signal(&self, signal: ProcessSignal) -> ExecProcessFuture<'_, ()> { + Box::pin(LocalExecProcess::signal(self, signal)) + } + + fn terminate(&self) -> ExecProcessFuture<'_, ()> { + Box::pin(LocalExecProcess::terminate(self)) + } +} + impl LocalProcess { async fn read( &self, diff --git a/codex-rs/exec-server/src/process.rs b/codex-rs/exec-server/src/process.rs index 97f53aaefbc7..b136ef4682fa 100644 --- a/codex-rs/exec-server/src/process.rs +++ b/codex-rs/exec-server/src/process.rs @@ -1,8 +1,9 @@ use std::collections::VecDeque; +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; use std::sync::Mutex as StdMutex; -use async_trait::async_trait; use tokio::sync::broadcast; use tokio::sync::watch; @@ -162,7 +163,6 @@ impl ExecProcessEventReceiver { /// `read` is the request/response API for callers that want to page through /// buffered output, while `subscribe_events` is the streaming API for callers /// that want output and lifecycle changes delivered as they happen. -#[async_trait] pub trait ExecProcess: Send + Sync { fn process_id(&self) -> &ProcessId; @@ -170,25 +170,30 @@ pub trait ExecProcess: Send + Sync { fn subscribe_events(&self) -> ExecProcessEventReceiver; - async fn read( + fn read( &self, after_seq: Option, max_bytes: Option, wait_ms: Option, - ) -> Result; + ) -> ExecProcessFuture<'_, ReadResponse>; - async fn write(&self, chunk: Vec) -> Result; + fn write(&self, chunk: Vec) -> ExecProcessFuture<'_, WriteResponse>; - async fn signal(&self, signal: ProcessSignal) -> Result<(), ExecServerError>; + fn signal(&self, signal: ProcessSignal) -> ExecProcessFuture<'_, ()>; - async fn terminate(&self) -> Result<(), ExecServerError>; + fn terminate(&self) -> ExecProcessFuture<'_, ()>; } -#[async_trait] +pub type ExecProcessFuture<'a, T> = + Pin> + Send + 'a>>; + pub trait ExecBackend: Send + Sync { - async fn start(&self, params: ExecParams) -> Result; + fn start(&self, params: ExecParams) -> ExecBackendFuture<'_>; } +pub type ExecBackendFuture<'a> = + Pin> + Send + 'a>>; + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; diff --git a/codex-rs/exec-server/src/remote_file_system.rs b/codex-rs/exec-server/src/remote_file_system.rs index d47b71cba228..03fe0532e979 100644 --- a/codex-rs/exec-server/src/remote_file_system.rs +++ b/codex-rs/exec-server/src/remote_file_system.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use base64::Engine as _; use base64::engine::general_purpose::STANDARD; use codex_utils_path_uri::PathUri; @@ -9,6 +8,7 @@ use crate::CopyOptions; use crate::CreateDirectoryOptions; use crate::ExecServerError; use crate::ExecutorFileSystem; +use crate::ExecutorFileSystemFuture; use crate::FileMetadata; use crate::FileSystemResult; use crate::FileSystemSandboxContext; @@ -36,10 +36,7 @@ impl RemoteFileSystem { trace!("remote fs new"); Self { client } } -} -#[async_trait] -impl ExecutorFileSystem for RemoteFileSystem { async fn canonicalize( &self, path: &PathUri, @@ -207,6 +204,85 @@ impl ExecutorFileSystem for RemoteFileSystem { } } +impl ExecutorFileSystem for RemoteFileSystem { + fn canonicalize<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, PathUri> { + Box::pin(RemoteFileSystem::canonicalize(self, path, sandbox)) + } + + fn read_file<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(RemoteFileSystem::read_file(self, path, sandbox)) + } + + fn write_file<'a>( + &'a self, + path: &'a PathUri, + contents: Vec, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(RemoteFileSystem::write_file(self, path, contents, sandbox)) + } + + fn create_directory<'a>( + &'a self, + path: &'a PathUri, + options: CreateDirectoryOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(RemoteFileSystem::create_directory( + self, path, options, sandbox, + )) + } + + fn get_metadata<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, FileMetadata> { + Box::pin(RemoteFileSystem::get_metadata(self, path, sandbox)) + } + + fn read_directory<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(RemoteFileSystem::read_directory(self, path, sandbox)) + } + + fn remove<'a>( + &'a self, + path: &'a PathUri, + options: RemoveOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(RemoteFileSystem::remove(self, path, options, sandbox)) + } + + fn copy<'a>( + &'a self, + source_path: &'a PathUri, + destination_path: &'a PathUri, + options: CopyOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(RemoteFileSystem::copy( + self, + source_path, + destination_path, + options, + sandbox, + )) + } +} + fn remote_sandbox_context( sandbox: Option<&FileSystemSandboxContext>, ) -> Option { diff --git a/codex-rs/exec-server/src/remote_process.rs b/codex-rs/exec-server/src/remote_process.rs index c7d16d4d4105..e130114cc34d 100644 --- a/codex-rs/exec-server/src/remote_process.rs +++ b/codex-rs/exec-server/src/remote_process.rs @@ -1,13 +1,13 @@ use std::sync::Arc; -use async_trait::async_trait; use tokio::sync::watch; use tracing::trace; use crate::ExecBackend; +use crate::ExecBackendFuture; use crate::ExecProcess; use crate::ExecProcessEventReceiver; -use crate::ExecServerError; +use crate::ExecProcessFuture; use crate::StartedExecProcess; use crate::client::LazyRemoteExecServerClient; use crate::client::Session; @@ -30,11 +30,11 @@ impl RemoteProcess { trace!("remote process new"); Self { client } } -} -#[async_trait] -impl ExecBackend for RemoteProcess { - async fn start(&self, params: ExecParams) -> Result { + async fn start( + &self, + params: ExecParams, + ) -> Result { let process_id = params.process_id.clone(); let client = self.client.get().await?; let session = client.register_session(&process_id).await?; @@ -49,7 +49,38 @@ impl ExecBackend for RemoteProcess { } } -#[async_trait] +impl ExecBackend for RemoteProcess { + fn start(&self, params: ExecParams) -> ExecBackendFuture<'_> { + Box::pin(RemoteProcess::start(self, params)) + } +} + +impl RemoteExecProcess { + async fn read( + &self, + after_seq: Option, + max_bytes: Option, + wait_ms: Option, + ) -> Result { + self.session.read(after_seq, max_bytes, wait_ms).await + } + + async fn write(&self, chunk: Vec) -> Result { + trace!("exec process write"); + self.session.write(chunk).await + } + + async fn signal(&self, signal: ProcessSignal) -> Result<(), crate::ExecServerError> { + trace!("exec process signal"); + self.session.signal(signal).await + } + + async fn terminate(&self) -> Result<(), crate::ExecServerError> { + trace!("exec process terminate"); + self.session.terminate().await + } +} + impl ExecProcess for RemoteExecProcess { fn process_id(&self) -> &crate::ProcessId { self.session.process_id() @@ -63,28 +94,25 @@ impl ExecProcess for RemoteExecProcess { self.session.subscribe_events() } - async fn read( + fn read( &self, after_seq: Option, max_bytes: Option, wait_ms: Option, - ) -> Result { - self.session.read(after_seq, max_bytes, wait_ms).await + ) -> ExecProcessFuture<'_, ReadResponse> { + Box::pin(RemoteExecProcess::read(self, after_seq, max_bytes, wait_ms)) } - async fn write(&self, chunk: Vec) -> Result { - trace!("exec process write"); - self.session.write(chunk).await + fn write(&self, chunk: Vec) -> ExecProcessFuture<'_, WriteResponse> { + Box::pin(RemoteExecProcess::write(self, chunk)) } - async fn signal(&self, signal: ProcessSignal) -> Result<(), ExecServerError> { - trace!("exec process signal"); - self.session.signal(signal).await + fn signal(&self, signal: ProcessSignal) -> ExecProcessFuture<'_, ()> { + Box::pin(RemoteExecProcess::signal(self, signal)) } - async fn terminate(&self) -> Result<(), ExecServerError> { - trace!("exec process terminate"); - self.session.terminate().await + fn terminate(&self) -> ExecProcessFuture<'_, ()> { + Box::pin(RemoteExecProcess::terminate(self)) } } diff --git a/codex-rs/exec-server/src/sandboxed_file_system.rs b/codex-rs/exec-server/src/sandboxed_file_system.rs index df708ffac709..94d18aafc19f 100644 --- a/codex-rs/exec-server/src/sandboxed_file_system.rs +++ b/codex-rs/exec-server/src/sandboxed_file_system.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use base64::Engine as _; use base64::engine::general_purpose::STANDARD; use codex_app_server_protocol::JSONRPCErrorError; @@ -9,6 +8,7 @@ use crate::CopyOptions; use crate::CreateDirectoryOptions; use crate::ExecServerRuntimePaths; use crate::ExecutorFileSystem; +use crate::ExecutorFileSystemFuture; use crate::FileMetadata; use crate::FileSystemResult; use crate::FileSystemSandboxContext; @@ -50,8 +50,7 @@ impl SandboxedFileSystem { } } -#[async_trait] -impl ExecutorFileSystem for SandboxedFileSystem { +impl SandboxedFileSystem { async fn canonicalize( &self, path: &PathUri, @@ -248,6 +247,92 @@ impl ExecutorFileSystem for SandboxedFileSystem { } } +impl ExecutorFileSystem for SandboxedFileSystem { + fn canonicalize<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, PathUri> { + Box::pin(SandboxedFileSystem::canonicalize(self, path, sandbox)) + } + + fn read_file<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(SandboxedFileSystem::read_file(self, path, sandbox)) + } + + fn write_file<'a>( + &'a self, + path: &'a PathUri, + contents: Vec, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(SandboxedFileSystem::write_file( + self, path, contents, sandbox, + )) + } + + fn create_directory<'a>( + &'a self, + path: &'a PathUri, + options: CreateDirectoryOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(SandboxedFileSystem::create_directory( + self, path, options, sandbox, + )) + } + + fn get_metadata<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, FileMetadata> { + Box::pin(SandboxedFileSystem::get_metadata(self, path, sandbox)) + } + + fn read_directory<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(SandboxedFileSystem::read_directory(self, path, sandbox)) + } + + fn remove<'a>( + &'a self, + path: &'a PathUri, + remove_options: RemoveOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(SandboxedFileSystem::remove( + self, + path, + remove_options, + sandbox, + )) + } + + fn copy<'a>( + &'a self, + source_path: &'a PathUri, + destination_path: &'a PathUri, + options: CopyOptions, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(SandboxedFileSystem::copy( + self, + source_path, + destination_path, + options, + sandbox, + )) + } +} + fn validate_native_path(path: &PathUri) -> FileSystemResult<()> { path.to_abs_path().map(drop) } diff --git a/codex-rs/ext/skills/Cargo.toml b/codex-rs/ext/skills/Cargo.toml index cc8dcb114958..afd5111e9598 100644 --- a/codex-rs/ext/skills/Cargo.toml +++ b/codex-rs/ext/skills/Cargo.toml @@ -31,6 +31,5 @@ tracing = { workspace = true } url = { workspace = true } [dev-dependencies] -async-trait = { workspace = true } pretty_assertions = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/codex-rs/ext/skills/tests/executor_file_system_authority.rs b/codex-rs/ext/skills/tests/executor_file_system_authority.rs index 5b9a440d874b..d5387abce263 100644 --- a/codex-rs/ext/skills/tests/executor_file_system_authority.rs +++ b/codex-rs/ext/skills/tests/executor_file_system_authority.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; -use async_trait::async_trait; use codex_core_skills::HostLoadedSkills; use codex_core_skills::loader::SkillRoot; use codex_core_skills::loader::load_skills_from_roots; @@ -11,8 +10,8 @@ use codex_exec_server::CopyOptions; use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::EnvironmentManager; use codex_exec_server::ExecutorFileSystem; +use codex_exec_server::ExecutorFileSystemFuture; use codex_exec_server::FileMetadata; -use codex_exec_server::FileSystemResult; use codex_exec_server::FileSystemSandboxContext; use codex_exec_server::ReadDirectoryEntry; use codex_exec_server::RemoveOptions; @@ -36,6 +35,42 @@ struct SyntheticFileSystem { } impl SyntheticFileSystem { + async fn canonicalize(&self, path: &PathUri) -> io::Result { + let path = path.to_abs_path()?; + if path == self.alias_root { + return PathUri::from_abs_path(&self.canonical_root); + } + self.metadata(&path)?; + PathUri::from_abs_path(&path) + } + + async fn read_file(&self, path: &PathUri) -> io::Result> { + if path.to_abs_path()? == self.canonical_root.join("skill/SKILL.md") { + Ok(SKILL_CONTENTS.as_bytes().to_vec()) + } else { + Err(io::Error::new(io::ErrorKind::NotFound, "not found")) + } + } + + async fn read_directory(&self, path: &PathUri) -> io::Result> { + let path = path.to_abs_path()?; + if path == self.canonical_root { + Ok(vec![ReadDirectoryEntry { + file_name: "skill".to_string(), + is_directory: true, + is_file: false, + }]) + } else if path == self.canonical_root.join("skill") { + Ok(vec![ReadDirectoryEntry { + file_name: "SKILL.md".to_string(), + is_directory: false, + is_file: true, + }]) + } else { + Err(io::Error::new(io::ErrorKind::NotFound, "not found")) + } + } + fn metadata(&self, path: &AbsolutePathBuf) -> io::Result { let skill_dir = self.canonical_root.join("skill"); let skill_path = skill_dir.join("SKILL.md"); @@ -56,99 +91,74 @@ impl SyntheticFileSystem { } } -#[async_trait] impl ExecutorFileSystem for SyntheticFileSystem { - async fn canonicalize( - &self, - path: &PathUri, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult { - let path = path.to_abs_path()?; - if path == self.alias_root { - return PathUri::from_abs_path(&self.canonical_root); - } - self.metadata(&path)?; - PathUri::from_abs_path(&path) + fn canonicalize<'a>( + &'a self, + path: &'a PathUri, + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, PathUri> { + Box::pin(SyntheticFileSystem::canonicalize(self, path)) } - async fn read_file( - &self, - path: &PathUri, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult> { - if path.to_abs_path()? == self.canonical_root.join("skill/SKILL.md") { - Ok(SKILL_CONTENTS.as_bytes().to_vec()) - } else { - Err(io::Error::new(io::ErrorKind::NotFound, "not found")) - } + fn read_file<'a>( + &'a self, + path: &'a PathUri, + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(SyntheticFileSystem::read_file(self, path)) } - async fn write_file( - &self, - _path: &PathUri, + fn write_file<'a>( + &'a self, + _path: &'a PathUri, _contents: Vec, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()> { - Err(io::Error::new(io::ErrorKind::Unsupported, "read only")) + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(async move { Err(io::Error::new(io::ErrorKind::Unsupported, "read only")) }) } - async fn create_directory( - &self, - _path: &PathUri, + fn create_directory<'a>( + &'a self, + _path: &'a PathUri, _options: CreateDirectoryOptions, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()> { - Err(io::Error::new(io::ErrorKind::Unsupported, "read only")) + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(async move { Err(io::Error::new(io::ErrorKind::Unsupported, "read only")) }) } - async fn get_metadata( - &self, - path: &PathUri, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult { - self.metadata(&path.to_abs_path()?) + fn get_metadata<'a>( + &'a self, + path: &'a PathUri, + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, FileMetadata> { + Box::pin(async move { self.metadata(&path.to_abs_path()?) }) } - async fn read_directory( - &self, - path: &PathUri, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult> { - let path = path.to_abs_path()?; - if path == self.canonical_root { - Ok(vec![ReadDirectoryEntry { - file_name: "skill".to_string(), - is_directory: true, - is_file: false, - }]) - } else if path == self.canonical_root.join("skill") { - Ok(vec![ReadDirectoryEntry { - file_name: "SKILL.md".to_string(), - is_directory: false, - is_file: true, - }]) - } else { - Err(io::Error::new(io::ErrorKind::NotFound, "not found")) - } + fn read_directory<'a>( + &'a self, + path: &'a PathUri, + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec> { + Box::pin(SyntheticFileSystem::read_directory(self, path)) } - async fn remove( - &self, - _path: &PathUri, + fn remove<'a>( + &'a self, + _path: &'a PathUri, _options: RemoveOptions, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()> { - Err(io::Error::new(io::ErrorKind::Unsupported, "read only")) + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(async move { Err(io::Error::new(io::ErrorKind::Unsupported, "read only")) }) } - async fn copy( - &self, - _source_path: &PathUri, - _destination_path: &PathUri, + fn copy<'a>( + &'a self, + _source_path: &'a PathUri, + _destination_path: &'a PathUri, _options: CopyOptions, - _sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()> { - Err(io::Error::new(io::ErrorKind::Unsupported, "read only")) + _sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()> { + Box::pin(async move { Err(io::Error::new(io::ErrorKind::Unsupported, "read only")) }) } } diff --git a/codex-rs/file-system/Cargo.toml b/codex-rs/file-system/Cargo.toml index bae75275763c..087e5da6c0a1 100644 --- a/codex-rs/file-system/Cargo.toml +++ b/codex-rs/file-system/Cargo.toml @@ -8,7 +8,6 @@ license.workspace = true workspace = true [dependencies] -async-trait = { workspace = true } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-path-uri = { workspace = true } diff --git a/codex-rs/file-system/src/lib.rs b/codex-rs/file-system/src/lib.rs index 95537402af8b..58dcde604cb7 100644 --- a/codex-rs/file-system/src/lib.rs +++ b/codex-rs/file-system/src/lib.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; use codex_protocol::models::SandboxEnforcement; @@ -10,8 +9,10 @@ use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; +use std::future::Future; use std::io; use std::path::Path; +use std::pin::Pin; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct CreateDirectoryOptions { @@ -130,71 +131,76 @@ fn file_system_policy_has_cwd_dependent_entries( pub type FileSystemResult = io::Result; +/// Future returned by [`ExecutorFileSystem`] operations. +pub type ExecutorFileSystemFuture<'a, T> = + Pin> + Send + 'a>>; + /// Abstract filesystem access used by components that may operate locally or via /// a remote environment. -#[async_trait] pub trait ExecutorFileSystem: Send + Sync { /// Resolves a path within this filesystem. - async fn canonicalize( - &self, - path: &PathUri, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult; - - async fn read_file( - &self, - path: &PathUri, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult>; + fn canonicalize<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, PathUri>; + + fn read_file<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec>; /// Reads a file and decodes it as UTF-8 text. - async fn read_file_text( - &self, - path: &PathUri, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult { - let bytes = self.read_file(path, sandbox).await?; - String::from_utf8(bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) + fn read_file_text<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, String> { + Box::pin(async move { + let bytes = self.read_file(path, sandbox).await?; + String::from_utf8(bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) + }) } - async fn write_file( - &self, - path: &PathUri, + fn write_file<'a>( + &'a self, + path: &'a PathUri, contents: Vec, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()>; + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()>; - async fn create_directory( - &self, - path: &PathUri, + fn create_directory<'a>( + &'a self, + path: &'a PathUri, create_directory_options: CreateDirectoryOptions, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()>; - - async fn get_metadata( - &self, - path: &PathUri, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult; - - async fn read_directory( - &self, - path: &PathUri, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult>; - - async fn remove( - &self, - path: &PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()>; + + fn get_metadata<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, FileMetadata>; + + fn read_directory<'a>( + &'a self, + path: &'a PathUri, + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, Vec>; + + fn remove<'a>( + &'a self, + path: &'a PathUri, remove_options: RemoveOptions, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()>; + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()>; - async fn copy( - &self, - source_path: &PathUri, - destination_path: &PathUri, + fn copy<'a>( + &'a self, + source_path: &'a PathUri, + destination_path: &'a PathUri, copy_options: CopyOptions, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()>; + sandbox: Option<&'a FileSystemSandboxContext>, + ) -> ExecutorFileSystemFuture<'a, ()>; } diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index e914fa0c7a98..c6d52953f13d 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -8,7 +8,6 @@ license.workspace = true workspace = true [dependencies] -async-trait = { workspace = true } base64 = { workspace = true } chrono = { workspace = true, features = ["serde"] } codex-agent-identity = { workspace = true } diff --git a/codex-rs/login/src/auth/external_bearer.rs b/codex-rs/login/src/auth/external_bearer.rs index c52859601423..e40c33264013 100644 --- a/codex-rs/login/src/auth/external_bearer.rs +++ b/codex-rs/login/src/auth/external_bearer.rs @@ -1,7 +1,7 @@ use super::manager::ExternalAuth; +use super::manager::ExternalAuthFuture; use super::manager::ExternalAuthRefreshContext; use super::manager::ExternalAuthTokens; -use async_trait::async_trait; use codex_app_server_protocol::AuthMode; use codex_protocol::config_types::ModelProviderAuthInfo; use std::fmt; @@ -25,13 +25,6 @@ impl BearerTokenRefresher { state: Arc::new(ExternalBearerAuthState::new(config)), } } -} - -#[async_trait] -impl ExternalAuth for BearerTokenRefresher { - fn auth_mode(&self) -> AuthMode { - AuthMode::ApiKey - } #[expect( clippy::await_holding_invalid_type, @@ -76,6 +69,23 @@ impl ExternalAuth for BearerTokenRefresher { } } +impl ExternalAuth for BearerTokenRefresher { + fn auth_mode(&self) -> AuthMode { + AuthMode::ApiKey + } + + fn resolve(&self) -> ExternalAuthFuture<'_, Option> { + Box::pin(BearerTokenRefresher::resolve(self)) + } + + fn refresh( + &self, + context: ExternalAuthRefreshContext, + ) -> ExternalAuthFuture<'_, ExternalAuthTokens> { + Box::pin(BearerTokenRefresher::refresh(self, context)) + } +} + impl fmt::Debug for BearerTokenRefresher { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("BearerTokenRefresher") diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index ececbe271e28..9961f7ef717d 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use chrono::Utc; use reqwest::StatusCode; use serde::Deserialize; @@ -7,8 +6,10 @@ use serde::Serialize; use serial_test::serial; use std::env; use std::fmt::Debug; +use std::future::Future; use std::path::Path; use std::path::PathBuf; +use std::pin::Pin; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; @@ -167,7 +168,6 @@ pub struct ExternalAuthRefreshContext { pub previous_account_id: Option, } -#[async_trait] /// Pluggable auth provider used by `AuthManager` for externally managed auth flows. /// /// Implementations may either resolve auth eagerly via `resolve()` or provide refreshed @@ -178,17 +178,19 @@ pub trait ExternalAuth: Send + Sync { /// Returns cached or immediately available auth, if this provider can resolve it synchronously /// from the caller's perspective. - async fn resolve(&self) -> std::io::Result> { - Ok(None) + fn resolve(&self) -> ExternalAuthFuture<'_, Option> { + Box::pin(async { Ok(None) }) } /// Refreshes auth in response to a manager-driven refresh attempt. - async fn refresh( + fn refresh( &self, context: ExternalAuthRefreshContext, - ) -> std::io::Result; + ) -> ExternalAuthFuture<'_, ExternalAuthTokens>; } +pub type ExternalAuthFuture<'a, T> = Pin> + Send + 'a>>; + impl RefreshTokenError { pub fn failed_reason(&self) -> Option { match self { diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index b857592ade92..6b305bae25c9 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -27,6 +27,7 @@ pub use auth::CODEX_API_KEY_ENV_VAR; pub use auth::CodexAuth; pub use auth::ExternalAuth; pub use auth::ExternalAuthChatgptMetadata; +pub use auth::ExternalAuthFuture; pub use auth::ExternalAuthRefreshContext; pub use auth::ExternalAuthRefreshReason; pub use auth::ExternalAuthTokens; diff --git a/codex-rs/memories/write/src/startup_tests.rs b/codex-rs/memories/write/src/startup_tests.rs index d5e46e09e05a..66b035ed00d2 100644 --- a/codex-rs/memories/write/src/startup_tests.rs +++ b/codex-rs/memories/write/src/startup_tests.rs @@ -11,6 +11,7 @@ use codex_git_utils::reset_git_repository; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_model_provider::ModelProvider; +use codex_model_provider::ModelProviderFuture; use codex_model_provider::ProviderAccountResult; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; @@ -38,10 +39,8 @@ use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; -use std::future::Future; use std::path::Path; use std::path::PathBuf; -use std::pin::Pin; use std::sync::Arc; use tempfile::TempDir; use tokio::time::Duration; @@ -616,13 +615,7 @@ impl ModelProvider for MockMemoryModelProvider { self.delegate.auth_manager() } - fn auth<'life0, 'async_trait>( - &'life0 self, - ) -> Pin> + Send + 'async_trait>> - where - 'life0: 'async_trait, - Self: 'async_trait, - { + fn auth(&self) -> ModelProviderFuture<'_, Option> { let delegate = Arc::clone(&self.delegate); Box::pin(async move { delegate.auth().await }) } diff --git a/codex-rs/model-provider/Cargo.toml b/codex-rs/model-provider/Cargo.toml index 58235ab24d5e..29599c6c7eec 100644 --- a/codex-rs/model-provider/Cargo.toml +++ b/codex-rs/model-provider/Cargo.toml @@ -13,7 +13,6 @@ path = "src/lib.rs" workspace = true [dependencies] -async-trait = { workspace = true } codex-api = { workspace = true } codex-agent-identity = { workspace = true } codex-aws-auth = { workspace = true } diff --git a/codex-rs/model-provider/src/amazon_bedrock/auth.rs b/codex-rs/model-provider/src/amazon_bedrock/auth.rs index 841b45bb48b7..6dd897050a2d 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/auth.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/auth.rs @@ -133,11 +133,6 @@ impl BedrockMantleSigV4AuthProvider { fn new(context: AwsAuthContext) -> Self { Self { context } } -} - -#[async_trait::async_trait] -impl AuthProvider for BedrockMantleSigV4AuthProvider { - fn add_auth_headers(&self, _headers: &mut HeaderMap) {} async fn apply_auth(&self, request: Request) -> std::result::Result { let mut request = request; @@ -162,6 +157,14 @@ impl AuthProvider for BedrockMantleSigV4AuthProvider { } } +impl AuthProvider for BedrockMantleSigV4AuthProvider { + fn add_auth_headers(&self, _headers: &mut HeaderMap) {} + + fn apply_auth(&self, request: Request) -> codex_api::AuthProviderFuture<'_> { + Box::pin(BedrockMantleSigV4AuthProvider::apply_auth(self, request)) + } +} + #[cfg(test)] mod tests { use codex_api::AuthProvider; diff --git a/codex-rs/model-provider/src/amazon_bedrock/mod.rs b/codex-rs/model-provider/src/amazon_bedrock/mod.rs index 872be005b017..d44e7cbd022b 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mod.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mod.rs @@ -20,6 +20,7 @@ use codex_protocol::error::Result; use codex_protocol::openai_models::ModelsResponse; use crate::provider::ModelProvider; +use crate::provider::ModelProviderFuture; use crate::provider::ProviderAccountResult; use crate::provider::ProviderAccountState; use crate::provider::ProviderCapabilities; @@ -68,9 +69,32 @@ impl AmazonBedrockModelProvider { | CodexAuth::PersonalAccessToken(_) => None, }) } + + async fn auth(&self) -> Option { + self.managed_auth().map(CodexAuth::BedrockApiKey) + } + + async fn api_provider(&self) -> Result { + let managed_auth = self.managed_auth(); + let mut api_provider_info = self.info.clone(); + api_provider_info.base_url = + Some(runtime_base_url(managed_auth.as_ref(), &self.aws).await?); + api_provider_info.to_api_provider(/*auth_mode*/ None) + } + + async fn runtime_base_url(&self) -> Result> { + let managed_auth = self.managed_auth(); + Ok(Some( + runtime_base_url(managed_auth.as_ref(), &self.aws).await?, + )) + } + + async fn api_auth(&self) -> Result { + let managed_auth = self.managed_auth(); + resolve_provider_auth(managed_auth.as_ref(), &self.aws).await + } } -#[async_trait::async_trait] impl ModelProvider for AmazonBedrockModelProvider { fn info(&self) -> &ModelProviderInfo { &self.info @@ -101,8 +125,8 @@ impl ModelProvider for AmazonBedrockModelProvider { .and_then(|_| self.auth_manager.as_ref().cloned()) } - async fn auth(&self) -> Option { - self.managed_auth().map(CodexAuth::BedrockApiKey) + fn auth(&self) -> ModelProviderFuture<'_, Option> { + Box::pin(AmazonBedrockModelProvider::auth(self)) } fn account_state(&self) -> ProviderAccountResult { @@ -112,24 +136,16 @@ impl ModelProvider for AmazonBedrockModelProvider { }) } - async fn api_provider(&self) -> Result { - let managed_auth = self.managed_auth(); - let mut api_provider_info = self.info.clone(); - api_provider_info.base_url = - Some(runtime_base_url(managed_auth.as_ref(), &self.aws).await?); - api_provider_info.to_api_provider(/*auth_mode*/ None) + fn api_provider(&self) -> ModelProviderFuture<'_, Result> { + Box::pin(AmazonBedrockModelProvider::api_provider(self)) } - async fn runtime_base_url(&self) -> Result> { - let managed_auth = self.managed_auth(); - Ok(Some( - runtime_base_url(managed_auth.as_ref(), &self.aws).await?, - )) + fn runtime_base_url(&self) -> ModelProviderFuture<'_, Result>> { + Box::pin(AmazonBedrockModelProvider::runtime_base_url(self)) } - async fn api_auth(&self) -> Result { - let managed_auth = self.managed_auth(); - resolve_provider_auth(managed_auth.as_ref(), &self.aws).await + fn api_auth(&self) -> ModelProviderFuture<'_, Result> { + Box::pin(AmazonBedrockModelProvider::api_auth(self)) } fn models_manager( diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs index 4e4660812b9d..f5efe1f57523 100644 --- a/codex-rs/model-provider/src/lib.rs +++ b/codex-rs/model-provider/src/lib.rs @@ -10,6 +10,7 @@ pub use bearer_auth_provider::BearerAuthProvider; pub use bearer_auth_provider::BearerAuthProvider as CoreAuthProvider; pub use codex_protocol::account::ProviderAccount; pub use provider::ModelProvider; +pub use provider::ModelProviderFuture; pub use provider::ProviderAccountError; pub use provider::ProviderAccountResult; pub use provider::ProviderAccountState; diff --git a/codex-rs/model-provider/src/models_endpoint.rs b/codex-rs/model-provider/src/models_endpoint.rs index 8a72beea7012..ebb51ea56449 100644 --- a/codex-rs/model-provider/src/models_endpoint.rs +++ b/codex-rs/model-provider/src/models_endpoint.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use std::time::Duration; -use async_trait::async_trait; use codex_api::ModelsClient; use codex_api::RequestTelemetry; use codex_api::ReqwestTransport; @@ -17,6 +16,7 @@ use codex_login::collect_auth_env_telemetry; use codex_login::default_client::build_reqwest_client; use codex_model_provider_info::ModelProviderInfo; use codex_models_manager::manager::ModelsEndpointClient; +use codex_models_manager::manager::ModelsEndpointFuture; use codex_otel::TelemetryAuthMode; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CoreResult; @@ -56,21 +56,6 @@ impl OpenAiModelsEndpoint { } } - fn auth_env(&self) -> AuthEnvTelemetry { - let codex_api_key_env_enabled = self - .auth_manager - .as_ref() - .is_some_and(|auth_manager| auth_manager.codex_api_key_env_enabled()); - collect_auth_env_telemetry(&self.provider_info, codex_api_key_env_enabled) - } -} - -#[async_trait] -impl ModelsEndpointClient for OpenAiModelsEndpoint { - fn has_command_auth(&self) -> bool { - self.provider_info.has_command_auth() - } - async fn uses_codex_backend(&self) -> bool { self.auth() .await @@ -107,6 +92,31 @@ impl ModelsEndpointClient for OpenAiModelsEndpoint { .map_err(|_| CodexErr::Timeout)? .map_err(map_api_error) } + + fn auth_env(&self) -> AuthEnvTelemetry { + let codex_api_key_env_enabled = self + .auth_manager + .as_ref() + .is_some_and(|auth_manager| auth_manager.codex_api_key_env_enabled()); + collect_auth_env_telemetry(&self.provider_info, codex_api_key_env_enabled) + } +} + +impl ModelsEndpointClient for OpenAiModelsEndpoint { + fn has_command_auth(&self) -> bool { + self.provider_info.has_command_auth() + } + + fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool> { + Box::pin(OpenAiModelsEndpoint::uses_codex_backend(self)) + } + + fn list_models<'a>( + &'a self, + client_version: &'a str, + ) -> ModelsEndpointFuture<'a, CoreResult<(Vec, Option)>> { + Box::pin(OpenAiModelsEndpoint::list_models(self, client_version)) + } } #[derive(Clone)] diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 6f6602d0148e..f794d49f4265 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -1,5 +1,7 @@ use std::fmt; +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; use std::sync::Arc; use codex_api::Provider; @@ -94,7 +96,6 @@ pub const DEFAULT_MEMORY_CONSOLIDATION_PREFERRED_MODEL: &str = "gpt-5.4"; /// Implementations own provider-specific behavior for a model backend. The /// `ModelProviderInfo` returned by `info` is the serialized/configured provider /// metadata used by the default OpenAI-compatible implementation. -#[async_trait::async_trait] pub trait ModelProvider: fmt::Debug + Send + Sync { /// Returns the configured provider metadata. fn info(&self) -> &ModelProviderInfo; @@ -139,27 +140,35 @@ pub trait ModelProvider: fmt::Debug + Send + Sync { fn auth_manager(&self) -> Option>; /// Returns the current provider-scoped auth value, if one is configured. - async fn auth(&self) -> Option; + fn auth(&self) -> ModelProviderFuture<'_, Option>; /// Returns the current app-visible account state for this provider. fn account_state(&self) -> ProviderAccountResult; /// Returns provider configuration adapted for the API client. - async fn api_provider(&self) -> codex_protocol::error::Result { - let auth = self.auth().await; - self.info() - .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode)) + fn api_provider(&self) -> ModelProviderFuture<'_, codex_protocol::error::Result> { + Box::pin(async move { + let auth = self.auth().await; + self.info() + .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode)) + }) } /// Returns the provider base URL that will be used at request time. - async fn runtime_base_url(&self) -> codex_protocol::error::Result> { - Ok(self.info().base_url.clone()) + fn runtime_base_url( + &self, + ) -> ModelProviderFuture<'_, codex_protocol::error::Result>> { + Box::pin(async { Ok(self.info().base_url.clone()) }) } /// Returns the auth provider used to attach request credentials. - async fn api_auth(&self) -> codex_protocol::error::Result { - let auth = self.auth().await; - resolve_provider_auth(auth.as_ref(), self.info()) + fn api_auth( + &self, + ) -> ModelProviderFuture<'_, codex_protocol::error::Result> { + Box::pin(async move { + let auth = self.auth().await; + resolve_provider_auth(auth.as_ref(), self.info()) + }) } /// Creates the model manager implementation appropriate for this provider. @@ -170,6 +179,8 @@ pub trait ModelProvider: fmt::Debug + Send + Sync { ) -> SharedModelsManager; } +pub type ModelProviderFuture<'a, T> = Pin + Send + 'a>>; + /// Shared runtime model provider handle. pub type SharedModelProvider = Arc; @@ -202,7 +213,6 @@ impl ConfiguredModelProvider { } } -#[async_trait::async_trait] impl ModelProvider for ConfiguredModelProvider { fn info(&self) -> &ModelProviderInfo { &self.info @@ -219,11 +229,13 @@ impl ModelProvider for ConfiguredModelProvider { .is_some_and(|auth| auth.is_chatgpt_auth()) } - async fn auth(&self) -> Option { - match self.auth_manager.as_ref() { - Some(auth_manager) => auth_manager.auth().await, - None => None, - } + fn auth(&self) -> ModelProviderFuture<'_, Option> { + Box::pin(async move { + match self.auth_manager.as_ref() { + Some(auth_manager) => auth_manager.auth().await, + None => None, + } + }) } fn account_state(&self) -> ProviderAccountResult { diff --git a/codex-rs/models-manager/Cargo.toml b/codex-rs/models-manager/Cargo.toml index f46bf2b285a4..7a157fd14088 100644 --- a/codex-rs/models-manager/Cargo.toml +++ b/codex-rs/models-manager/Cargo.toml @@ -13,7 +13,6 @@ path = "src/lib.rs" workspace = true [dependencies] -async-trait = { workspace = true } chrono = { workspace = true, features = ["serde"] } codex-app-server-protocol = { workspace = true } codex-collaboration-mode-templates = { workspace = true } diff --git a/codex-rs/models-manager/src/manager.rs b/codex-rs/models-manager/src/manager.rs index b21911a4d68d..e09518aa9658 100644 --- a/codex-rs/models-manager/src/manager.rs +++ b/codex-rs/models-manager/src/manager.rs @@ -2,7 +2,6 @@ use super::cache::ModelsCacheManager; use crate::collaboration_mode_presets::builtin_collaboration_mode_presets; use crate::config::ModelsManagerConfig; use crate::model_info; -use async_trait::async_trait; use codex_app_server_protocol::AuthMode; use codex_login::AuthManager; use codex_protocol::config_types::CollaborationModeMask; @@ -12,7 +11,9 @@ use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelVisibility; use codex_protocol::openai_models::ModelsResponse; use std::fmt; +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; @@ -29,21 +30,22 @@ const DEFAULT_MODEL_CACHE_TTL: Duration = Duration::from_secs(300); /// Implementations own provider-specific auth and transport details. The model /// manager owns refresh policy, cache behavior, and catalog merging; it calls /// this endpoint only when it decides a remote refresh should happen. -#[async_trait] pub trait ModelsEndpointClient: fmt::Debug + Send + Sync { /// Returns whether this provider can authenticate command-scoped requests. fn has_command_auth(&self) -> bool; /// Returns whether the currently resolved auth can use Codex backend-only models. - async fn uses_codex_backend(&self) -> bool; + fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool>; /// Fetches the latest remote model catalog and optional ETag. - async fn list_models( - &self, - client_version: &str, - ) -> CoreResult<(Vec, Option)>; + fn list_models<'a>( + &'a self, + client_version: &'a str, + ) -> ModelsEndpointFuture<'a, CoreResult<(Vec, Option)>>; } +pub type ModelsEndpointFuture<'a, T> = Pin + Send + 'a>>; + /// Strategy for refreshing available models. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RefreshStrategy { @@ -74,28 +76,34 @@ impl fmt::Display for RefreshStrategy { type SharedModelsEndpointClient = Arc; /// Coordinates model discovery plus cached metadata on disk. -#[async_trait] pub trait ModelsManager: fmt::Debug + Send + Sync { /// List all available models, refreshing according to the specified strategy. /// /// Returns model presets sorted by priority and filtered by auth mode and visibility. - async fn list_models(&self, refresh_strategy: RefreshStrategy) -> Vec { - async move { - let catalog = self.raw_model_catalog(refresh_strategy).await; - self.build_available_models(catalog.models) - } - .instrument(tracing::info_span!( - "list_models", - refresh_strategy = %refresh_strategy - )) - .await + fn list_models( + &self, + refresh_strategy: RefreshStrategy, + ) -> ModelsManagerFuture<'_, Vec> { + Box::pin( + async move { + let catalog = self.raw_model_catalog(refresh_strategy).await; + self.build_available_models(catalog.models) + } + .instrument(tracing::info_span!( + "list_models", + refresh_strategy = %refresh_strategy + )), + ) } /// Return the active raw model catalog, refreshing according to the specified strategy. - async fn raw_model_catalog(&self, refresh_strategy: RefreshStrategy) -> ModelsResponse; + fn raw_model_catalog( + &self, + refresh_strategy: RefreshStrategy, + ) -> ModelsManagerFuture<'_, ModelsResponse>; /// Return the current in-memory remote model catalog without refreshing or loading cache state. - async fn get_remote_models(&self) -> Vec; + fn get_remote_models(&self) -> ModelsManagerFuture<'_, Vec>; /// Attempt to return the current in-memory remote model catalog without blocking. /// @@ -138,42 +146,50 @@ pub trait ModelsManager: fmt::Debug + Send + Sync { /// /// If `model` is provided, returns it directly. Otherwise selects the default based on /// auth mode and available models. - async fn get_default_model( - &self, - model: &Option, + fn get_default_model<'a>( + &'a self, + model: &'a Option, refresh_strategy: RefreshStrategy, - ) -> String { - async move { - if let Some(model) = model.as_ref() { - return model.to_string(); + ) -> ModelsManagerFuture<'a, String> { + Box::pin( + async move { + if let Some(model) = model.as_ref() { + return model.to_string(); + } + default_model_from_available(self.list_models(refresh_strategy).await) } - default_model_from_available(self.list_models(refresh_strategy).await) - } - .instrument(tracing::info_span!( - "get_default_model", - model.provided = model.is_some(), - refresh_strategy = %refresh_strategy - )) - .await + .instrument(tracing::info_span!( + "get_default_model", + model.provided = model.is_some(), + refresh_strategy = %refresh_strategy + )), + ) } // todo(aibrahim): look if we can tighten it to pub(crate) /// Look up model metadata, applying remote overrides and config adjustments. - async fn get_model_info(&self, model: &str, config: &ModelsManagerConfig) -> ModelInfo { - async move { - let remote_models = self.get_remote_models().await; - construct_model_info_from_candidates(model, &remote_models, config) - } - .instrument(tracing::info_span!("get_model_info", model = model)) - .await + fn get_model_info<'a>( + &'a self, + model: &'a str, + config: &'a ModelsManagerConfig, + ) -> ModelsManagerFuture<'a, ModelInfo> { + Box::pin( + async move { + let remote_models = self.get_remote_models().await; + construct_model_info_from_candidates(model, &remote_models, config) + } + .instrument(tracing::info_span!("get_model_info", model = model)), + ) } /// Refresh models if the provided ETag differs from the cached ETag. /// /// Uses `Online` strategy to fetch latest models when ETags differ. - async fn refresh_if_new_etag(&self, etag: String); + fn refresh_if_new_etag(&self, etag: String) -> ModelsManagerFuture<'_, ()>; } +pub type ModelsManagerFuture<'a, T> = Pin + Send + 'a>>; + /// Shared model manager handle used across runtime services. pub type SharedModelsManager = Arc; @@ -224,19 +240,19 @@ impl StaticModelsManager { } } -#[async_trait] impl ModelsManager for OpenAiModelsManager { - async fn raw_model_catalog(&self, refresh_strategy: RefreshStrategy) -> ModelsResponse { - if let Err(err) = self.refresh_available_models(refresh_strategy).await { - error!("failed to refresh available models: {err}"); - } - ModelsResponse { - models: self.get_remote_models().await, - } + fn raw_model_catalog( + &self, + refresh_strategy: RefreshStrategy, + ) -> ModelsManagerFuture<'_, ModelsResponse> { + Box::pin(OpenAiModelsManager::raw_model_catalog( + self, + refresh_strategy, + )) } - async fn get_remote_models(&self) -> Vec { - self.remote_models.read().await.clone() + fn get_remote_models(&self) -> ModelsManagerFuture<'_, Vec> { + Box::pin(async move { self.remote_models.read().await.clone() }) } fn try_get_remote_models(&self) -> Result, TryLockError> { @@ -251,6 +267,21 @@ impl ModelsManager for OpenAiModelsManager { builtin_collaboration_mode_presets() } + fn refresh_if_new_etag(&self, etag: String) -> ModelsManagerFuture<'_, ()> { + Box::pin(OpenAiModelsManager::refresh_if_new_etag(self, etag)) + } +} + +impl OpenAiModelsManager { + async fn raw_model_catalog(&self, refresh_strategy: RefreshStrategy) -> ModelsResponse { + if let Err(err) = self.refresh_available_models(refresh_strategy).await { + error!("failed to refresh available models: {err}"); + } + ModelsResponse { + models: self.get_remote_models().await, + } + } + async fn refresh_if_new_etag(&self, etag: String) { let current_etag = self.get_etag().await; if current_etag.clone().is_some() && current_etag.as_deref() == Some(etag.as_str()) { @@ -263,9 +294,7 @@ impl ModelsManager for OpenAiModelsManager { error!("failed to refresh available models: {err}"); } } -} -impl OpenAiModelsManager { /// Refresh available models according to the specified strategy. async fn refresh_available_models(&self, refresh_strategy: RefreshStrategy) -> CoreResult<()> { if !self.should_refresh_models().await { @@ -378,16 +407,20 @@ impl OpenAiModelsManager { } } -#[async_trait] impl ModelsManager for StaticModelsManager { - async fn raw_model_catalog(&self, _refresh_strategy: RefreshStrategy) -> ModelsResponse { - ModelsResponse { - models: self.get_remote_models().await, - } + fn raw_model_catalog( + &self, + _refresh_strategy: RefreshStrategy, + ) -> ModelsManagerFuture<'_, ModelsResponse> { + Box::pin(async move { + ModelsResponse { + models: self.get_remote_models().await, + } + }) } - async fn get_remote_models(&self) -> Vec { - self.remote_models.clone() + fn get_remote_models(&self) -> ModelsManagerFuture<'_, Vec> { + Box::pin(async { self.remote_models.clone() }) } fn try_get_remote_models(&self) -> Result, TryLockError> { @@ -402,7 +435,9 @@ impl ModelsManager for StaticModelsManager { builtin_collaboration_mode_presets() } - async fn refresh_if_new_etag(&self, _etag: String) {} + fn refresh_if_new_etag(&self, _etag: String) -> ModelsManagerFuture<'_, ()> { + Box::pin(async {}) + } } fn load_remote_models_from_file() -> Result, std::io::Error> { diff --git a/codex-rs/models-manager/src/manager_tests.rs b/codex-rs/models-manager/src/manager_tests.rs index 6f3a6a27f471..a1dcdd3d44cd 100644 --- a/codex-rs/models-manager/src/manager_tests.rs +++ b/codex-rs/models-manager/src/manager_tests.rs @@ -100,72 +100,77 @@ impl TestModelsEndpoint { fn fetch_count(&self) -> usize { self.fetch_count.load(Ordering::SeqCst) } + + async fn list_models(&self) -> CoreResult<(Vec, Option)> { + self.fetch_count.fetch_add(1, Ordering::SeqCst); + let models = self + .responses + .lock() + .expect("responses lock should not be poisoned") + .pop_front() + .unwrap_or_default(); + Ok((models, None)) + } } #[derive(Debug)] struct TestExternalApiKeyAuth; -#[async_trait] impl ExternalAuth for TestExternalApiKeyAuth { fn auth_mode(&self) -> AuthMode { AuthMode::ApiKey } - async fn resolve(&self) -> std::io::Result> { - Ok(Some(ExternalAuthTokens::access_token_only( - "test-external-api-key", - ))) + fn resolve(&self) -> codex_login::ExternalAuthFuture<'_, Option> { + Box::pin(async { + Ok(Some(ExternalAuthTokens::access_token_only( + "test-external-api-key", + ))) + }) } - async fn refresh( + fn refresh( &self, _context: ExternalAuthRefreshContext, - ) -> std::io::Result { - Ok(ExternalAuthTokens::access_token_only( - "test-external-api-key", - )) + ) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> { + Box::pin(async { + Ok(ExternalAuthTokens::access_token_only( + "test-external-api-key", + )) + }) } } #[derive(Debug)] struct TestUnresolvedExternalApiKeyAuth; -#[async_trait] impl ExternalAuth for TestUnresolvedExternalApiKeyAuth { fn auth_mode(&self) -> AuthMode { AuthMode::ApiKey } - async fn refresh( + fn refresh( &self, _context: ExternalAuthRefreshContext, - ) -> std::io::Result { - Err(std::io::Error::other("unresolved test auth")) + ) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> { + Box::pin(async { Err(std::io::Error::other("unresolved test auth")) }) } } -#[async_trait] impl ModelsEndpointClient for TestModelsEndpoint { fn has_command_auth(&self) -> bool { self.has_command_auth } - async fn uses_codex_backend(&self) -> bool { - self.uses_codex_backend + fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool> { + Box::pin(async { self.uses_codex_backend }) } - async fn list_models( - &self, - _client_version: &str, - ) -> CoreResult<(Vec, Option)> { - self.fetch_count.fetch_add(1, Ordering::SeqCst); - let models = self - .responses - .lock() - .expect("responses lock should not be poisoned") - .pop_front() - .unwrap_or_default(); - Ok((models, None)) + fn list_models<'a>( + &'a self, + _client_version: &'a str, + ) -> ModelsEndpointFuture<'a, CoreResult<(Vec, Option)>> { + Box::pin(TestModelsEndpoint::list_models(self)) } } @@ -713,13 +718,6 @@ impl TestAuthAwareModelsEndpoint { fn fetch_count(&self) -> usize { self.fetch_count.load(Ordering::SeqCst) } -} - -#[async_trait] -impl ModelsEndpointClient for TestAuthAwareModelsEndpoint { - fn has_command_auth(&self) -> bool { - false - } async fn uses_codex_backend(&self) -> bool { match self.auth_manager.as_ref() { @@ -732,10 +730,7 @@ impl ModelsEndpointClient for TestAuthAwareModelsEndpoint { } } - async fn list_models( - &self, - _client_version: &str, - ) -> CoreResult<(Vec, Option)> { + async fn list_models(&self) -> CoreResult<(Vec, Option)> { self.fetch_count.fetch_add(1, Ordering::SeqCst); let models = self .responses @@ -747,6 +742,23 @@ impl ModelsEndpointClient for TestAuthAwareModelsEndpoint { } } +impl ModelsEndpointClient for TestAuthAwareModelsEndpoint { + fn has_command_auth(&self) -> bool { + false + } + + fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool> { + Box::pin(TestAuthAwareModelsEndpoint::uses_codex_backend(self)) + } + + fn list_models<'a>( + &'a self, + _client_version: &'a str, + ) -> ModelsEndpointFuture<'a, CoreResult<(Vec, Option)>> { + Box::pin(TestAuthAwareModelsEndpoint::list_models(self)) + } +} + #[tokio::test] async fn refresh_available_models_skips_network_when_external_api_key_overrides_chatgpt_auth() { let dynamic_slug = "dynamic-model-only-for-test-external-api-key"; diff --git a/codex-rs/network-proxy/Cargo.toml b/codex-rs/network-proxy/Cargo.toml index a40003838273..cd32ff92b22e 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -14,7 +14,6 @@ workspace = true [dependencies] anyhow = { workspace = true } -async-trait = { workspace = true } base64 = { workspace = true } clap = { workspace = true, features = ["derive"] } chrono = { workspace = true } diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 9ad16f8ecc4d..e80b154a5eae 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -34,6 +34,7 @@ pub use mitm_hook::MitmHookMatchConfig; pub use network_policy::NetworkDecision; pub use network_policy::NetworkDecisionSource; pub use network_policy::NetworkPolicyDecider; +pub use network_policy::NetworkPolicyDeciderFuture; pub use network_policy::NetworkPolicyDecision; pub use network_policy::NetworkPolicyRequest; pub use network_policy::NetworkPolicyRequestArgs; @@ -59,7 +60,9 @@ pub use proxy::proxy_url_env_value; pub use runtime::BlockedRequest; pub use runtime::BlockedRequestArgs; pub use runtime::BlockedRequestObserver; +pub use runtime::BlockedRequestObserverFuture; pub use runtime::ConfigReloader; +pub use runtime::ConfigReloaderFuture; pub use runtime::ConfigState; pub use runtime::NetworkProxyState; pub use state::NetworkProxyAuditMetadata; diff --git a/codex-rs/network-proxy/src/network_policy.rs b/codex-rs/network-proxy/src/network_policy.rs index 936abf3e2118..18bca2c9169f 100644 --- a/codex-rs/network-proxy/src/network_policy.rs +++ b/codex-rs/network-proxy/src/network_policy.rs @@ -3,10 +3,10 @@ use crate::runtime::HostBlockDecision; use crate::runtime::HostBlockReason; use crate::state::NetworkProxyState; use anyhow::Result; -use async_trait::async_trait; use chrono::SecondsFormat; use chrono::Utc; use std::future::Future; +use std::pin::Pin; use std::sync::Arc; const AUDIT_TARGET: &str = "codex_otel.network_proxy"; @@ -263,26 +263,26 @@ fn audit_timestamp() -> String { /// If `command` or `exec_policy_hint` is provided, callers can map exec-policy /// approvals to network access (e.g., allow all requests for commands matching /// approved prefixes like `curl *`). -#[async_trait] pub trait NetworkPolicyDecider: Send + Sync + 'static { - async fn decide(&self, req: NetworkPolicyRequest) -> NetworkDecision; + fn decide(&self, req: NetworkPolicyRequest) -> NetworkPolicyDeciderFuture<'_>; } -#[async_trait] +pub type NetworkPolicyDeciderFuture<'a> = + Pin + Send + 'a>>; + impl NetworkPolicyDecider for Arc { - async fn decide(&self, req: NetworkPolicyRequest) -> NetworkDecision { - (**self).decide(req).await + fn decide(&self, req: NetworkPolicyRequest) -> NetworkPolicyDeciderFuture<'_> { + Box::pin(async move { (**self).decide(req).await }) } } -#[async_trait] impl NetworkPolicyDecider for F where F: Fn(NetworkPolicyRequest) -> Fut + Send + Sync + 'static, - Fut: Future + Send, + Fut: Future + Send + 'static, { - async fn decide(&self, req: NetworkPolicyRequest) -> NetworkDecision { - (self)(req).await + fn decide(&self, req: NetworkPolicyRequest) -> NetworkPolicyDeciderFuture<'_> { + Box::pin((self)(req)) } } @@ -541,6 +541,7 @@ mod tests { use crate::reasons::REASON_NOT_ALLOWED; use crate::reasons::REASON_NOT_ALLOWED_LOCAL; use crate::runtime::ConfigReloader; + use crate::runtime::ConfigReloaderFuture; use crate::runtime::ConfigState; use crate::runtime::NetworkProxyAuditMetadata; use crate::state::NetworkProxyConstraints; @@ -560,14 +561,13 @@ mod tests { state: ConfigState, } - #[async_trait] impl ConfigReloader for StaticReloader { - async fn maybe_reload(&self) -> anyhow::Result> { - Ok(None) + fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option> { + Box::pin(async { Ok(None) }) } - async fn reload_now(&self) -> anyhow::Result { - Ok(self.state.clone()) + fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> { + Box::pin(async { Ok(self.state.clone()) }) } fn source_label(&self) -> String { diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index 60894d5d5b22..e721d0de708e 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -20,7 +20,6 @@ use crate::state::build_config_state; use crate::state::validate_policy_against_constraints; use anyhow::Context; use anyhow::Result; -use async_trait::async_trait; use codex_utils_absolute_path::AbsolutePathBuf; use globset::GlobSet; use serde::Serialize; @@ -30,6 +29,7 @@ use std::future::Future; use std::net::IpAddr; use std::net::SocketAddr; use std::path::Path; +use std::pin::Pin; use std::sync::Arc; use std::time::Duration; use time::OffsetDateTime; @@ -168,38 +168,38 @@ pub struct ConfigState { pub blocked_total: u64, } -#[async_trait] pub trait ConfigReloader: Send + Sync { /// Human-readable description of where config is loaded from, for logs. fn source_label(&self) -> String; /// Return a freshly loaded state if a reload is needed; otherwise, return `None`. - async fn maybe_reload(&self) -> Result>; + fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option>; /// Force a reload, regardless of whether a change was detected. - async fn reload_now(&self) -> Result; + fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState>; } -#[async_trait] +pub type ConfigReloaderFuture<'a, T> = Pin> + Send + 'a>>; + pub trait BlockedRequestObserver: Send + Sync + 'static { - async fn on_blocked_request(&self, request: BlockedRequest); + fn on_blocked_request(&self, request: BlockedRequest) -> BlockedRequestObserverFuture<'_>; } -#[async_trait] +pub type BlockedRequestObserverFuture<'a> = Pin + Send + 'a>>; + impl BlockedRequestObserver for Arc { - async fn on_blocked_request(&self, request: BlockedRequest) { - (**self).on_blocked_request(request).await + fn on_blocked_request(&self, request: BlockedRequest) -> BlockedRequestObserverFuture<'_> { + Box::pin(async move { (**self).on_blocked_request(request).await }) } } -#[async_trait] impl BlockedRequestObserver for F where F: Fn(BlockedRequest) -> Fut + Send + Sync + 'static, - Fut: Future + Send, + Fut: Future + Send + 'static, { - async fn on_blocked_request(&self, request: BlockedRequest) { - (self)(request).await + fn on_blocked_request(&self, request: BlockedRequest) -> BlockedRequestObserverFuture<'_> { + Box::pin((self)(request)) } } @@ -891,18 +891,17 @@ pub(crate) fn network_proxy_state_for_policy( struct NoopReloader; #[cfg(test)] -#[async_trait] impl ConfigReloader for NoopReloader { fn source_label(&self) -> String { "test config state".to_string() } - async fn maybe_reload(&self) -> Result> { - Ok(None) + fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option> { + Box::pin(async { Ok(None) }) } - async fn reload_now(&self) -> Result { - Err(anyhow::anyhow!("force reload is not supported in tests")) + fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> { + Box::pin(async { Err(anyhow::anyhow!("force reload is not supported in tests")) }) } } diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs index d9a8a559fd8f..6e600d2025ab 100644 --- a/codex-rs/network-proxy/src/socks5.rs +++ b/codex-rs/network-proxy/src/socks5.rs @@ -714,10 +714,10 @@ mod tests { use crate::network_policy::test_support::capture_events; use crate::network_policy::test_support::find_event_by_name; use crate::runtime::ConfigReloader; + use crate::runtime::ConfigReloaderFuture; use crate::runtime::ConfigState; use crate::state::NetworkProxyConstraints; use crate::state::build_config_state; - use async_trait::async_trait; use pretty_assertions::assert_eq; use rama_core::extensions::Extensions; use rama_core::extensions::ExtensionsMut; @@ -738,14 +738,13 @@ mod tests { state: ConfigState, } - #[async_trait] impl ConfigReloader for StaticReloader { - async fn maybe_reload(&self) -> anyhow::Result> { - Ok(None) + fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option> { + Box::pin(async { Ok(None) }) } - async fn reload_now(&self) -> anyhow::Result { - Ok(self.state.clone()) + fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> { + Box::pin(async { Ok(self.state.clone()) }) } fn source_label(&self) -> String { diff --git a/codex-rs/rollout/Cargo.toml b/codex-rs/rollout/Cargo.toml index 6bbdcff297c1..edc90596887c 100644 --- a/codex-rs/rollout/Cargo.toml +++ b/codex-rs/rollout/Cargo.toml @@ -14,7 +14,6 @@ workspace = true [dependencies] anyhow = { workspace = true } -async-trait = { workspace = true } chrono = { workspace = true, features = ["serde"] } codex-file-search = { workspace = true } codex-git-utils = { workspace = true } diff --git a/codex-rs/rollout/src/list.rs b/codex-rs/rollout/src/list.rs index af3785a9752c..8cad5d546cc6 100644 --- a/codex-rs/rollout/src/list.rs +++ b/codex-rs/rollout/src/list.rs @@ -1,6 +1,5 @@ #![allow(warnings, clippy::all)] -use async_trait::async_trait; use codex_utils_path as path_utils; use std::cmp::Reverse; use std::ffi::OsStr; @@ -195,15 +194,14 @@ impl AnchorState { /// /// We need to apply different logic if we're ultimately going to be returning /// threads ordered by created_at or updated_at. -#[async_trait] trait RolloutFileVisitor { - async fn visit( + fn visit( &mut self, ts: OffsetDateTime, id: Uuid, path: PathBuf, scanned: usize, - ) -> ControlFlow<()>; + ) -> impl std::future::Future> + Send; } /// Collects thread items during directory traversal in created_at order, @@ -218,7 +216,6 @@ struct FilesByCreatedAtVisitor<'a> { cwd_filters: Option<&'a [PathBuf]>, } -#[async_trait] impl<'a> RolloutFileVisitor for FilesByCreatedAtVisitor<'a> { async fn visit( &mut self, @@ -263,7 +260,6 @@ struct FilesByUpdatedAtVisitor<'a> { candidates: &'a mut Vec, } -#[async_trait] impl<'a> RolloutFileVisitor for FilesByUpdatedAtVisitor<'a> { async fn visit( &mut self, diff --git a/codex-rs/sandboxing/Cargo.toml b/codex-rs/sandboxing/Cargo.toml index 858219e9fcc7..226849521b46 100644 --- a/codex-rs/sandboxing/Cargo.toml +++ b/codex-rs/sandboxing/Cargo.toml @@ -26,7 +26,6 @@ which = { workspace = true } [dev-dependencies] anyhow = { workspace = true } -async-trait = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index d905d0adcf12..337eb56378ea 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -12,6 +12,7 @@ use super::seatbelt_regex_for_unreadable_glob; use super::unix_socket_dir_params; use super::unix_socket_policy; use codex_network_proxy::ConfigReloader; +use codex_network_proxy::ConfigReloaderFuture; use codex_network_proxy::ConfigState; use codex_network_proxy::NetworkMode; use codex_network_proxy::NetworkProxy; @@ -81,18 +82,17 @@ fn seatbelt_protected_metadata_name_requirements(root: &Path) -> String { struct TestConfigReloader; -#[async_trait::async_trait] impl ConfigReloader for TestConfigReloader { fn source_label(&self) -> String { "seatbelt test config".to_string() } - async fn maybe_reload(&self) -> anyhow::Result> { - Ok(None) + fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option> { + Box::pin(async { Ok(None) }) } - async fn reload_now(&self) -> anyhow::Result { - Err(anyhow::anyhow!("seatbelt test config cannot reload")) + fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> { + Box::pin(async { Err(anyhow::anyhow!("seatbelt test config cannot reload")) }) } } diff --git a/codex-rs/shell-escalation/Cargo.toml b/codex-rs/shell-escalation/Cargo.toml index a57db3ce53f0..92ae8b033f3c 100644 --- a/codex-rs/shell-escalation/Cargo.toml +++ b/codex-rs/shell-escalation/Cargo.toml @@ -13,7 +13,6 @@ workspace = true [dependencies] anyhow = { workspace = true } -async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } diff --git a/codex-rs/shell-escalation/src/lib.rs b/codex-rs/shell-escalation/src/lib.rs index 0f3c0afd300b..53a55c4d83af 100644 --- a/codex-rs/shell-escalation/src/lib.rs +++ b/codex-rs/shell-escalation/src/lib.rs @@ -16,6 +16,8 @@ pub use unix::EscalationPermissions; #[cfg(unix)] pub use unix::EscalationPolicy; #[cfg(unix)] +pub use unix::EscalationPolicyFuture; +#[cfg(unix)] pub use unix::EscalationSession; #[cfg(unix)] pub use unix::ExecParams; @@ -28,6 +30,8 @@ pub use unix::ResolvedPermissionProfile; #[cfg(unix)] pub use unix::ShellCommandExecutor; #[cfg(unix)] +pub use unix::ShellCommandExecutorFuture; +#[cfg(unix)] pub use unix::Stopwatch; #[cfg(unix)] pub use unix::main_execve_wrapper; diff --git a/codex-rs/shell-escalation/src/unix/escalate_server.rs b/codex-rs/shell-escalation/src/unix/escalate_server.rs index 55abf7d73420..7d01bc04736d 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_server.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_server.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; +use std::future::Future; use std::os::fd::AsRawFd; use std::path::PathBuf; +use std::pin::Pin; use std::process::Stdio; use std::sync::Arc; use std::sync::Mutex; @@ -31,7 +33,6 @@ use crate::unix::socket::AsyncSocket; /// This lets `shell-escalation` own the Unix escalation protocol while the caller /// keeps control over process spawning, output capture, and sandbox integration. /// Implementations can capture any sandbox state they need. -#[async_trait::async_trait] pub trait ShellCommandExecutor: Send + Sync { /// Runs the requested shell command and returns the captured result. /// @@ -41,26 +42,29 @@ pub trait ShellCommandExecutor: Send + Sync { /// for the shell process. `after_spawn` should be invoked immediately after /// the shell process has been spawned so the parent copy of the inherited /// escalation socket can be closed. - async fn run( + fn run( &self, command: Vec, cwd: PathBuf, env_overlay: HashMap, cancel_rx: CancellationToken, after_spawn: Option>, - ) -> anyhow::Result; + ) -> ShellCommandExecutorFuture<'_, ExecResult>; /// Prepares an escalated subcommand for execution on the server side. - async fn prepare_escalated_exec( - &self, - program: &AbsolutePathBuf, - argv: &[String], - workdir: &AbsolutePathBuf, + fn prepare_escalated_exec<'a>( + &'a self, + program: &'a AbsolutePathBuf, + argv: &'a [String], + workdir: &'a AbsolutePathBuf, env: HashMap, execution: EscalationExecution, - ) -> anyhow::Result; + ) -> ShellCommandExecutorFuture<'a, PreparedExec>; } +pub type ShellCommandExecutorFuture<'a, T> = + Pin> + Send + 'a>>; + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct ExecParams { /// The command string to pass to the shell via `-c` or `-lc`. @@ -377,6 +381,7 @@ async fn handle_escalate_session_with_policy( #[cfg(test)] mod tests { use super::*; + use crate::unix::escalation_policy::EscalationPolicyFuture; use codex_protocol::approvals::EscalationPermissions; use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::NetworkPermissions; @@ -401,15 +406,14 @@ mod tests { decision: EscalationDecision, } - #[async_trait::async_trait] impl EscalationPolicy for DeterministicEscalationPolicy { - async fn determine_action( - &self, - _file: &AbsolutePathBuf, - _argv: &[String], - _workdir: &AbsolutePathBuf, - ) -> anyhow::Result { - Ok(self.decision.clone()) + fn determine_action<'a>( + &'a self, + _file: &'a AbsolutePathBuf, + _argv: &'a [String], + _workdir: &'a AbsolutePathBuf, + ) -> EscalationPolicyFuture<'a> { + Box::pin(async move { Ok(self.decision.clone()) }) } } @@ -418,42 +422,30 @@ mod tests { expected_workdir: AbsolutePathBuf, } - #[async_trait::async_trait] impl EscalationPolicy for AssertingEscalationPolicy { - async fn determine_action( - &self, - file: &AbsolutePathBuf, - _argv: &[String], - workdir: &AbsolutePathBuf, - ) -> anyhow::Result { - assert_eq!(file, &self.expected_file); - assert_eq!(workdir, &self.expected_workdir); - Ok(EscalationDecision::run()) + fn determine_action<'a>( + &'a self, + file: &'a AbsolutePathBuf, + _argv: &'a [String], + workdir: &'a AbsolutePathBuf, + ) -> EscalationPolicyFuture<'a> { + Box::pin(async move { + assert_eq!(file, &self.expected_file); + assert_eq!(workdir, &self.expected_workdir); + Ok(EscalationDecision::run()) + }) } } struct ForwardingShellCommandExecutor; - #[async_trait::async_trait] - impl ShellCommandExecutor for ForwardingShellCommandExecutor { - async fn run( - &self, - _command: Vec, - _cwd: PathBuf, - _env_overlay: HashMap, - _cancel_rx: CancellationToken, - _after_spawn: Option>, - ) -> anyhow::Result { - unreachable!("run() is not used by handle_escalate_session_with_policy() tests") - } - + impl ForwardingShellCommandExecutor { async fn prepare_escalated_exec( &self, program: &AbsolutePathBuf, argv: &[String], workdir: &AbsolutePathBuf, env: HashMap, - _execution: EscalationExecution, ) -> anyhow::Result { Ok(PreparedExec { command: std::iter::once(program.to_string_lossy().to_string()) @@ -466,23 +458,39 @@ mod tests { } } - struct PermissionAssertingShellCommandExecutor { - expected_permissions: EscalationPermissions, - } - - #[async_trait::async_trait] - impl ShellCommandExecutor for PermissionAssertingShellCommandExecutor { - async fn run( + impl ShellCommandExecutor for ForwardingShellCommandExecutor { + fn run( &self, _command: Vec, _cwd: PathBuf, _env_overlay: HashMap, _cancel_rx: CancellationToken, _after_spawn: Option>, - ) -> anyhow::Result { - unreachable!("run() is not used by handle_escalate_session_with_policy() tests") + ) -> ShellCommandExecutorFuture<'_, ExecResult> { + Box::pin(async { + unreachable!("run() is not used by handle_escalate_session_with_policy() tests") + }) } + fn prepare_escalated_exec<'a>( + &'a self, + program: &'a AbsolutePathBuf, + argv: &'a [String], + workdir: &'a AbsolutePathBuf, + env: HashMap, + _execution: EscalationExecution, + ) -> ShellCommandExecutorFuture<'a, PreparedExec> { + Box::pin(ForwardingShellCommandExecutor::prepare_escalated_exec( + self, program, argv, workdir, env, + )) + } + } + + struct PermissionAssertingShellCommandExecutor { + expected_permissions: EscalationPermissions, + } + + impl PermissionAssertingShellCommandExecutor { async fn prepare_escalated_exec( &self, program: &AbsolutePathBuf, @@ -506,6 +514,36 @@ mod tests { } } + impl ShellCommandExecutor for PermissionAssertingShellCommandExecutor { + fn run( + &self, + _command: Vec, + _cwd: PathBuf, + _env_overlay: HashMap, + _cancel_rx: CancellationToken, + _after_spawn: Option>, + ) -> ShellCommandExecutorFuture<'_, ExecResult> { + Box::pin(async { + unreachable!("run() is not used by handle_escalate_session_with_policy() tests") + }) + } + + fn prepare_escalated_exec<'a>( + &'a self, + program: &'a AbsolutePathBuf, + argv: &'a [String], + workdir: &'a AbsolutePathBuf, + env: HashMap, + execution: EscalationExecution, + ) -> ShellCommandExecutorFuture<'a, PreparedExec> { + Box::pin( + PermissionAssertingShellCommandExecutor::prepare_escalated_exec( + self, program, argv, workdir, env, execution, + ), + ) + } + } + async fn wait_for_pid_file(pid_file: &std::path::Path) -> anyhow::Result { let deadline = Instant::now() + Duration::from_secs(5); loop { @@ -534,14 +572,10 @@ mod tests { after_spawn_invoked: Arc, } - #[async_trait::async_trait] - impl ShellCommandExecutor for AfterSpawnAssertingShellCommandExecutor { + impl AfterSpawnAssertingShellCommandExecutor { async fn run( &self, - _command: Vec, - _cwd: PathBuf, env_overlay: HashMap, - _cancel_rx: CancellationToken, after_spawn: Option>, ) -> anyhow::Result { let socket_fd = env_overlay @@ -560,16 +594,33 @@ mod tests { timed_out: false, }) } + } - async fn prepare_escalated_exec( + impl ShellCommandExecutor for AfterSpawnAssertingShellCommandExecutor { + fn run( &self, - _program: &AbsolutePathBuf, - _argv: &[String], - _workdir: &AbsolutePathBuf, + _command: Vec, + _cwd: PathBuf, + env_overlay: HashMap, + _cancel_rx: CancellationToken, + after_spawn: Option>, + ) -> ShellCommandExecutorFuture<'_, ExecResult> { + Box::pin(AfterSpawnAssertingShellCommandExecutor::run( + self, + env_overlay, + after_spawn, + )) + } + + fn prepare_escalated_exec<'a>( + &'a self, + _program: &'a AbsolutePathBuf, + _argv: &'a [String], + _workdir: &'a AbsolutePathBuf, _env: HashMap, _execution: EscalationExecution, - ) -> anyhow::Result { - unreachable!("prepare_escalated_exec() is not used by exec() tests") + ) -> ShellCommandExecutorFuture<'a, PreparedExec> { + Box::pin(async { unreachable!("prepare_escalated_exec() is not used by exec() tests") }) } } diff --git a/codex-rs/shell-escalation/src/unix/escalation_policy.rs b/codex-rs/shell-escalation/src/unix/escalation_policy.rs index c3da252dd9fb..c77af22db736 100644 --- a/codex-rs/shell-escalation/src/unix/escalation_policy.rs +++ b/codex-rs/shell-escalation/src/unix/escalation_policy.rs @@ -1,14 +1,19 @@ +use std::future::Future; +use std::pin::Pin; + use codex_utils_absolute_path::AbsolutePathBuf; use crate::unix::escalate_protocol::EscalationDecision; /// Decides what action to take in response to an execve request from a client. -#[async_trait::async_trait] pub trait EscalationPolicy: Send + Sync { - async fn determine_action( - &self, - file: &AbsolutePathBuf, - argv: &[String], - workdir: &AbsolutePathBuf, - ) -> anyhow::Result; + fn determine_action<'a>( + &'a self, + file: &'a AbsolutePathBuf, + argv: &'a [String], + workdir: &'a AbsolutePathBuf, + ) -> EscalationPolicyFuture<'a>; } + +pub type EscalationPolicyFuture<'a> = + Pin> + Send + 'a>>; diff --git a/codex-rs/shell-escalation/src/unix/mod.rs b/codex-rs/shell-escalation/src/unix/mod.rs index 8fb8688443f8..8e30056bb4dc 100644 --- a/codex-rs/shell-escalation/src/unix/mod.rs +++ b/codex-rs/shell-escalation/src/unix/mod.rs @@ -72,7 +72,9 @@ pub use self::escalate_server::ExecParams; pub use self::escalate_server::ExecResult; pub use self::escalate_server::PreparedExec; pub use self::escalate_server::ShellCommandExecutor; +pub use self::escalate_server::ShellCommandExecutorFuture; pub use self::escalation_policy::EscalationPolicy; +pub use self::escalation_policy::EscalationPolicyFuture; pub use self::execve_wrapper::main_execve_wrapper; pub use self::stopwatch::Stopwatch; pub use codex_protocol::approvals::EscalationPermissions; diff --git a/codex-rs/thread-store/Cargo.toml b/codex-rs/thread-store/Cargo.toml index 5a0608b4db60..7c59cf72f352 100644 --- a/codex-rs/thread-store/Cargo.toml +++ b/codex-rs/thread-store/Cargo.toml @@ -13,7 +13,6 @@ doctest = false workspace = true [dependencies] -async-trait = { workspace = true } chrono = { workspace = true, features = ["serde"] } codex-git-utils = { workspace = true } codex-install-context = { workspace = true } diff --git a/codex-rs/thread-store/src/in_memory.rs b/codex-rs/thread-store/src/in_memory.rs index 7983f16dd334..472e3d35e3e1 100644 --- a/codex-rs/thread-store/src/in_memory.rs +++ b/codex-rs/thread-store/src/in_memory.rs @@ -5,7 +5,6 @@ use std::sync::Mutex; use std::sync::MutexGuard; use std::sync::OnceLock; -use async_trait::async_trait; use chrono::Utc; use codex_protocol::ThreadId; use codex_protocol::models::PermissionProfile; @@ -31,6 +30,7 @@ use crate::ThreadMetadataPatch; use crate::ThreadPage; use crate::ThreadStore; use crate::ThreadStoreError; +use crate::ThreadStoreFuture; use crate::ThreadStoreResult; use crate::UpdateThreadMetadataParams; @@ -160,13 +160,6 @@ impl InMemoryThreadStore { pub async fn calls(&self) -> InMemoryThreadStoreCalls { self.state.lock().await.calls.clone() } -} - -#[async_trait] -impl ThreadStore for InMemoryThreadStore { - fn as_any(&self) -> &dyn std::any::Any { - self - } async fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreResult<()> { let mut state = self.state.lock().await; @@ -230,26 +223,6 @@ impl ThreadStore for InMemoryThreadStore { Ok(()) } - async fn persist_thread(&self, _thread_id: ThreadId) -> ThreadStoreResult<()> { - self.state.lock().await.calls.persist_thread += 1; - Ok(()) - } - - async fn flush_thread(&self, _thread_id: ThreadId) -> ThreadStoreResult<()> { - self.state.lock().await.calls.flush_thread += 1; - Ok(()) - } - - async fn shutdown_thread(&self, _thread_id: ThreadId) -> ThreadStoreResult<()> { - self.state.lock().await.calls.shutdown_thread += 1; - Ok(()) - } - - async fn discard_thread(&self, _thread_id: ThreadId) -> ThreadStoreResult<()> { - self.state.lock().await.calls.discard_thread += 1; - Ok(()) - } - async fn load_history( &self, params: LoadThreadHistoryParams, @@ -293,7 +266,7 @@ impl ThreadStore for InMemoryThreadStore { stored_thread_from_state(&state, thread_id, params.include_history) } - async fn list_threads(&self, _params: ListThreadsParams) -> ThreadStoreResult { + async fn list_threads(&self) -> ThreadStoreResult { let mut state = self.state.lock().await; state.calls.list_threads += 1; let mut items = state @@ -327,20 +300,6 @@ impl ThreadStore for InMemoryThreadStore { stored_thread_from_state(&state, params.thread_id, /*include_history*/ false) } - async fn archive_thread(&self, _params: ArchiveThreadParams) -> ThreadStoreResult<()> { - self.state.lock().await.calls.archive_thread += 1; - Ok(()) - } - - async fn unarchive_thread( - &self, - params: ArchiveThreadParams, - ) -> ThreadStoreResult { - let mut state = self.state.lock().await; - state.calls.unarchive_thread += 1; - stored_thread_from_state(&state, params.thread_id, /*include_history*/ false) - } - async fn delete_thread(&self, params: DeleteThreadParams) -> ThreadStoreResult<()> { let mut state = self.state.lock().await; state.calls.delete_thread += 1; @@ -361,6 +320,102 @@ impl ThreadStore for InMemoryThreadStore { } } +impl ThreadStore for InMemoryThreadStore { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(InMemoryThreadStore::create_thread(self, params)) + } + + fn resume_thread(&self, params: ResumeThreadParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(InMemoryThreadStore::resume_thread(self, params)) + } + + fn append_items(&self, params: AppendThreadItemsParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(InMemoryThreadStore::append_items(self, params)) + } + + fn persist_thread(&self, _thread_id: ThreadId) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { + self.state.lock().await.calls.persist_thread += 1; + Ok(()) + }) + } + + fn flush_thread(&self, _thread_id: ThreadId) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { + self.state.lock().await.calls.flush_thread += 1; + Ok(()) + }) + } + + fn shutdown_thread(&self, _thread_id: ThreadId) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { + self.state.lock().await.calls.shutdown_thread += 1; + Ok(()) + }) + } + + fn discard_thread(&self, _thread_id: ThreadId) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { + self.state.lock().await.calls.discard_thread += 1; + Ok(()) + }) + } + + fn load_history( + &self, + params: LoadThreadHistoryParams, + ) -> ThreadStoreFuture<'_, StoredThreadHistory> { + Box::pin(InMemoryThreadStore::load_history(self, params)) + } + + fn read_thread(&self, params: ReadThreadParams) -> ThreadStoreFuture<'_, StoredThread> { + Box::pin(InMemoryThreadStore::read_thread(self, params)) + } + + fn read_thread_by_rollout_path( + &self, + params: ReadThreadByRolloutPathParams, + ) -> ThreadStoreFuture<'_, StoredThread> { + Box::pin(InMemoryThreadStore::read_thread_by_rollout_path( + self, params, + )) + } + + fn list_threads(&self, _params: ListThreadsParams) -> ThreadStoreFuture<'_, ThreadPage> { + Box::pin(InMemoryThreadStore::list_threads(self)) + } + + fn update_thread_metadata( + &self, + params: UpdateThreadMetadataParams, + ) -> ThreadStoreFuture<'_, StoredThread> { + Box::pin(InMemoryThreadStore::update_thread_metadata(self, params)) + } + + fn archive_thread(&self, _params: ArchiveThreadParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { + self.state.lock().await.calls.archive_thread += 1; + Ok(()) + }) + } + + fn unarchive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, StoredThread> { + Box::pin(async move { + let mut state = self.state.lock().await; + state.calls.unarchive_thread += 1; + stored_thread_from_state(&state, params.thread_id, /*include_history*/ false) + }) + } + + fn delete_thread(&self, params: DeleteThreadParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(InMemoryThreadStore::delete_thread(self, params)) + } +} + fn stored_thread_from_state( state: &InMemoryThreadStoreState, thread_id: ThreadId, diff --git a/codex-rs/thread-store/src/lib.rs b/codex-rs/thread-store/src/lib.rs index 5d590528aee9..d0ab3805c92e 100644 --- a/codex-rs/thread-store/src/lib.rs +++ b/codex-rs/thread-store/src/lib.rs @@ -21,6 +21,7 @@ pub use live_thread::LiveThreadInitGuard; pub use local::LocalThreadStore; pub use local::LocalThreadStoreConfig; pub use store::ThreadStore; +pub use store::ThreadStoreFuture; pub use types::AppendThreadItemsParams; pub use types::ArchiveThreadParams; pub use types::ClearableField; diff --git a/codex-rs/thread-store/src/local/mod.rs b/codex-rs/thread-store/src/local/mod.rs index 3000fee9d93e..dd49fd2a592a 100644 --- a/codex-rs/thread-store/src/local/mod.rs +++ b/codex-rs/thread-store/src/local/mod.rs @@ -12,7 +12,6 @@ mod update_thread_metadata; #[cfg(test)] mod test_support; -use async_trait::async_trait; use codex_protocol::ThreadId; use codex_rollout::RolloutRecorder; use codex_rollout::StateDbHandle; @@ -38,6 +37,7 @@ use crate::ThreadPage; use crate::ThreadSearchPage; use crate::ThreadStore; use crate::ThreadStoreError; +use crate::ThreadStoreFuture; use crate::ThreadStoreResult; use crate::UpdateThreadMetadataParams; @@ -166,41 +166,6 @@ impl LocalThreadStore { } } } -} - -#[async_trait] -impl ThreadStore for LocalThreadStore { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - async fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreResult<()> { - live_writer::create_thread(self, params).await - } - - async fn resume_thread(&self, params: ResumeThreadParams) -> ThreadStoreResult<()> { - live_writer::resume_thread(self, params).await - } - - async fn append_items(&self, params: AppendThreadItemsParams) -> ThreadStoreResult<()> { - live_writer::append_items(self, params).await - } - - async fn persist_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()> { - live_writer::persist_thread(self, thread_id).await - } - - async fn flush_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()> { - live_writer::flush_thread(self, thread_id).await - } - - async fn shutdown_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()> { - live_writer::shutdown_thread(self, thread_id).await - } - - async fn discard_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()> { - live_writer::discard_thread(self, thread_id).await - } async fn load_history( &self, @@ -245,11 +210,7 @@ impl ThreadStore for LocalThreadStore { }) } - async fn read_thread(&self, params: ReadThreadParams) -> ThreadStoreResult { - read_thread::read_thread(self, params).await - } - - async fn read_thread_by_rollout_path( + async fn read_thread_by_rollout_path_params( &self, params: ReadThreadByRolloutPathParams, ) -> ThreadStoreResult { @@ -261,38 +222,89 @@ impl ThreadStore for LocalThreadStore { ) .await } +} + +impl ThreadStore for LocalThreadStore { + fn as_any(&self) -> &dyn std::any::Any { + self + } - async fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreResult { - list_threads::list_threads(self, params).await + fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { live_writer::create_thread(self, params).await }) } - async fn search_threads( + fn resume_thread(&self, params: ResumeThreadParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { live_writer::resume_thread(self, params).await }) + } + + fn append_items(&self, params: AppendThreadItemsParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { live_writer::append_items(self, params).await }) + } + + fn persist_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { live_writer::persist_thread(self, thread_id).await }) + } + + fn flush_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { live_writer::flush_thread(self, thread_id).await }) + } + + fn shutdown_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { live_writer::shutdown_thread(self, thread_id).await }) + } + + fn discard_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { live_writer::discard_thread(self, thread_id).await }) + } + + fn load_history( + &self, + params: LoadThreadHistoryParams, + ) -> ThreadStoreFuture<'_, StoredThreadHistory> { + Box::pin(LocalThreadStore::load_history(self, params)) + } + + fn read_thread(&self, params: ReadThreadParams) -> ThreadStoreFuture<'_, StoredThread> { + Box::pin(async move { read_thread::read_thread(self, params).await }) + } + + fn read_thread_by_rollout_path( + &self, + params: ReadThreadByRolloutPathParams, + ) -> ThreadStoreFuture<'_, StoredThread> { + Box::pin(LocalThreadStore::read_thread_by_rollout_path_params( + self, params, + )) + } + + fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreFuture<'_, ThreadPage> { + Box::pin(async move { list_threads::list_threads(self, params).await }) + } + + fn search_threads( &self, params: SearchThreadsParams, - ) -> ThreadStoreResult { - search_threads::search_threads(self, params).await + ) -> ThreadStoreFuture<'_, ThreadSearchPage> { + Box::pin(async move { search_threads::search_threads(self, params).await }) } - async fn update_thread_metadata( + fn update_thread_metadata( &self, params: UpdateThreadMetadataParams, - ) -> ThreadStoreResult { - update_thread_metadata::update_thread_metadata(self, params).await + ) -> ThreadStoreFuture<'_, StoredThread> { + Box::pin(async move { update_thread_metadata::update_thread_metadata(self, params).await }) } - async fn archive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreResult<()> { - archive_thread::archive_thread(self, params).await + fn archive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { archive_thread::archive_thread(self, params).await }) } - async fn unarchive_thread( - &self, - params: ArchiveThreadParams, - ) -> ThreadStoreResult { - unarchive_thread::unarchive_thread(self, params).await + fn unarchive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, StoredThread> { + Box::pin(async move { unarchive_thread::unarchive_thread(self, params).await }) } - async fn delete_thread(&self, params: DeleteThreadParams) -> ThreadStoreResult<()> { - delete_thread::delete_thread(self, params).await + fn delete_thread(&self, params: DeleteThreadParams) -> ThreadStoreFuture<'_, ()> { + Box::pin(async move { delete_thread::delete_thread(self, params).await }) } } diff --git a/codex-rs/thread-store/src/store.rs b/codex-rs/thread-store/src/store.rs index c0348365fa3f..ef02e85d508e 100644 --- a/codex-rs/thread-store/src/store.rs +++ b/codex-rs/thread-store/src/store.rs @@ -1,6 +1,7 @@ -use async_trait::async_trait; use codex_protocol::ThreadId; use std::any::Any; +use std::future::Future; +use std::pin::Pin; use crate::AppendThreadItemsParams; use crate::ArchiveThreadParams; @@ -24,81 +25,89 @@ use crate::ThreadStoreResult; use crate::TurnPage; use crate::UpdateThreadMetadataParams; +/// Future returned by [`ThreadStore`] operations. +pub type ThreadStoreFuture<'a, T> = Pin> + Send + 'a>>; + /// Storage-neutral thread persistence boundary. -#[async_trait] pub trait ThreadStore: Any + Send + Sync { /// Return this store as [`Any`] for implementation-owned escape hatches. fn as_any(&self) -> &dyn Any; /// Creates a new live thread. - async fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreResult<()>; + fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreFuture<'_, ()>; /// Reopens an existing thread for live appends. - async fn resume_thread(&self, params: ResumeThreadParams) -> ThreadStoreResult<()>; + fn resume_thread(&self, params: ResumeThreadParams) -> ThreadStoreFuture<'_, ()>; /// Appends raw rollout items to a live thread. /// /// Implementations should apply the shared rollout persistence policy before writing durable /// replay history and before updating any implementation-owned projections. - async fn append_items(&self, params: AppendThreadItemsParams) -> ThreadStoreResult<()>; + fn append_items(&self, params: AppendThreadItemsParams) -> ThreadStoreFuture<'_, ()>; /// Materializes the thread if persistence is lazy, then persists all queued items. - async fn persist_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()>; + fn persist_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>; /// Flushes all queued items and returns once they are durable/readable. - async fn flush_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()>; + fn flush_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>; /// Flushes pending items and closes the live thread writer. - async fn shutdown_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()>; + fn shutdown_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>; /// Discards the live thread writer without forcing pending in-memory items to become durable. /// /// Core calls this when session initialization fails after a live writer has been created. /// Implementations should release any live writer resources for the thread while preserving /// already-durable thread data. - async fn discard_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()>; + fn discard_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>; /// Loads persisted history for resume, fork, rollback, and memory jobs. - async fn load_history( + fn load_history( &self, params: LoadThreadHistoryParams, - ) -> ThreadStoreResult; + ) -> ThreadStoreFuture<'_, StoredThreadHistory>; /// Reads a thread summary and optionally its persisted history. - async fn read_thread(&self, params: ReadThreadParams) -> ThreadStoreResult; + fn read_thread(&self, params: ReadThreadParams) -> ThreadStoreFuture<'_, StoredThread>; /// Reads a rollout-backed thread by path when the store supports path-addressed lookups. /// /// Deprecated: new callers should use [`ThreadStore::read_thread`] instead. - async fn read_thread_by_rollout_path( + fn read_thread_by_rollout_path( &self, params: ReadThreadByRolloutPathParams, - ) -> ThreadStoreResult; + ) -> ThreadStoreFuture<'_, StoredThread>; /// Lists stored threads matching the supplied filters. - async fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreResult; + fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreFuture<'_, ThreadPage>; /// Searches stored threads and returns search-only preview metadata. - async fn search_threads( + fn search_threads( &self, _params: SearchThreadsParams, - ) -> ThreadStoreResult { - Err(ThreadStoreError::Unsupported { - operation: "thread/search", + ) -> ThreadStoreFuture<'_, ThreadSearchPage> { + Box::pin(async { + Err(ThreadStoreError::Unsupported { + operation: "thread/search", + }) }) } /// Lists turns within a stored thread. - async fn list_turns(&self, _params: ListTurnsParams) -> ThreadStoreResult { - Err(ThreadStoreError::Unsupported { - operation: "list_turns", + fn list_turns(&self, _params: ListTurnsParams) -> ThreadStoreFuture<'_, TurnPage> { + Box::pin(async { + Err(ThreadStoreError::Unsupported { + operation: "list_turns", + }) }) } /// Lists persisted items within a stored turn. - async fn list_items(&self, _params: ListItemsParams) -> ThreadStoreResult { - Err(ThreadStoreError::Unsupported { - operation: "list_items", + fn list_items(&self, _params: ListItemsParams) -> ThreadStoreFuture<'_, ItemPage> { + Box::pin(async { + Err(ThreadStoreError::Unsupported { + operation: "list_items", + }) }) } @@ -106,20 +115,17 @@ pub trait ThreadStore: Any + Send + Sync { /// /// Implementations should apply the supplied fields directly. Policy such as deciding whether /// an append-derived preview should be emitted belongs above the store. - async fn update_thread_metadata( + fn update_thread_metadata( &self, params: UpdateThreadMetadataParams, - ) -> ThreadStoreResult; + ) -> ThreadStoreFuture<'_, StoredThread>; /// Archives a thread. - async fn archive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreResult<()>; + fn archive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, ()>; /// Unarchives a thread and returns its updated metadata. - async fn unarchive_thread( - &self, - params: ArchiveThreadParams, - ) -> ThreadStoreResult; + fn unarchive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, StoredThread>; /// Deletes a thread's persisted rollout data and associated metadata. - async fn delete_thread(&self, params: DeleteThreadParams) -> ThreadStoreResult<()>; + fn delete_thread(&self, params: DeleteThreadParams) -> ThreadStoreFuture<'_, ()>; } diff --git a/codex-rs/tui/src/workspace_command.rs b/codex-rs/tui/src/workspace_command.rs index c6b2e770e618..02495d66ddba 100644 --- a/codex-rs/tui/src/workspace_command.rs +++ b/codex-rs/tui/src/workspace_command.rs @@ -138,9 +138,8 @@ pub(crate) trait WorkspaceCommandExecutor: Send + Sync { /// Runs a workspace command and returns captured output or an app-server request error. /// /// Callers should treat errors as infrastructure failures and should treat successful output - /// with a non-zero exit code as ordinary command failure. Returning a future instead of using - /// `async_trait` keeps the trait object-safe while matching the repo's native async trait - /// conventions. + /// with a non-zero exit code as ordinary command failure. Returning a boxed future keeps the + /// trait object-safe. fn run( &self, command: WorkspaceCommand, diff --git a/codex-rs/utils/readiness/Cargo.toml b/codex-rs/utils/readiness/Cargo.toml index 2a6dfaeb542f..076651d5227b 100644 --- a/codex-rs/utils/readiness/Cargo.toml +++ b/codex-rs/utils/readiness/Cargo.toml @@ -5,7 +5,6 @@ edition.workspace = true license.workspace = true [dependencies] -async-trait = { workspace = true } thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true, features = ["sync", "time"] } diff --git a/codex-rs/utils/readiness/src/lib.rs b/codex-rs/utils/readiness/src/lib.rs index 365bfe61ebd5..a2ee8f96671a 100644 --- a/codex-rs/utils/readiness/src/lib.rs +++ b/codex-rs/utils/readiness/src/lib.rs @@ -17,7 +17,6 @@ pub struct Token(i32); const LOCK_TIMEOUT: Duration = Duration::from_millis(1000); -#[async_trait::async_trait] pub trait Readiness: Send + Sync + 'static { /// Returns true if the flag is currently marked ready. At least one token needs to be marked /// as ready before. @@ -27,17 +26,22 @@ pub trait Readiness: Send + Sync + 'static { /// Subscribe to readiness and receive an authorization token. /// /// If the flag is already ready, returns `FlagAlreadyReady`. - async fn subscribe(&self) -> Result; + fn subscribe( + &self, + ) -> impl std::future::Future> + Send; /// Attempt to mark the flag ready, validated by the provided token. /// /// Returns `true` iff: /// - `token` is currently subscribed, and /// - the flag was not already ready. - async fn mark_ready(&self, token: Token) -> Result; + fn mark_ready( + &self, + token: Token, + ) -> impl std::future::Future> + Send; /// Asynchronously wait until the flag becomes ready. - async fn wait_ready(&self); + fn wait_ready(&self) -> impl std::future::Future + Send; } pub struct ReadinessFlag { @@ -92,7 +96,6 @@ impl fmt::Debug for ReadinessFlag { } } -#[async_trait::async_trait] impl Readiness for ReadinessFlag { fn is_ready(&self) -> bool { if self.load_ready() {