[codex] Remove async_trait from first-party code (#27475)

## Why

First-party async traits should expose their `Send` contracts explicitly
without requiring `async_trait`. This completes the migration pattern
established in #27303 and #27304.

## What changed

- Replaced the remaining first-party `async_trait` traits with native
return-position `impl Future + Send` where statically dispatched and
explicit boxed `Send` futures where object safety is required.
- Kept implementations behavior-preserving, outlining existing async
bodies into inherent methods where that keeps the diff reviewable.
- Removed all direct first-party `async-trait` dependencies and the
workspace dependency declaration.
- Added a cargo-deny policy that permits `async-trait` only through the
remaining transitive wrapper crates.
- Updated `rand` from 0.8.5 to 0.8.6 to resolve RUSTSEC-2026-0097 and
keep the full cargo-deny check passing.

## Validation

- `just test -p codex-exec-server`: 216 passed, 2 skipped.
- `just test -p codex-model-provider`: 39 passed.
- `just test -p codex-core` and `just test`: changed tests passed;
remaining failures are environment-sensitive suites unrelated to this
migration.
- `cargo deny check`
- `just fix`
- `just fmt`
- `cargo shear`
- `just bazel-lock-check`
This commit is contained in:
Adam Perry @ OpenAI
2026-06-11 18:16:39 -07:00
committed by GitHub
parent 1829ed1122
commit 5a56caf18c
98 changed files with 2010 additions and 1050 deletions
+1
View File
@@ -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\"]}}",
+9 -31
View File
@@ -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",
-1
View File
@@ -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"
-1
View File
@@ -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"] }
-2
View File
@@ -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,
+8 -10
View File
@@ -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<Output = AgentGraphStoreResult<()>> + 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<Output = AgentGraphStoreResult<()>> + 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<ThreadSpawnEdgeStatus>,
) -> AgentGraphStoreResult<Vec<ThreadId>>;
) -> impl std::future::Future<Output = AgentGraphStoreResult<Vec<ThreadId>>> + 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<ThreadSpawnEdgeStatus>,
) -> AgentGraphStoreResult<Vec<ThreadId>>;
) -> impl std::future::Future<Output = AgentGraphStoreResult<Vec<ThreadId>>> + Send;
}
-1
View File
@@ -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",
+10 -3
View File
@@ -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<ThreadConfigSource>> {
Box::pin(CountingThreadConfigLoader::load(self, context))
}
}
}
+13 -8
View File
@@ -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<OutgoingMessageSender>,
skills_watcher: Arc<SkillsWatcher>,
-1
View File
@@ -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
+4 -4
View File
@@ -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<Self::Output, CancelErr>;
fn or_cancel(
self,
token: &CancellationToken,
) -> impl Future<Output = Result<Self::Output, CancelErr>> + Send;
}
#[async_trait]
impl<F> OrCancelExt for F
where
F: Future + Send,
-1
View File
@@ -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 }
+26 -20
View File
@@ -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<T> = std::result::Result<T, CloudTaskError>;
pub type CloudBackendFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + 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<i64>,
cursor: Option<&str>,
) -> Result<TaskListPage>;
async fn get_task_summary(&self, id: TaskId) -> Result<TaskSummary>;
async fn get_task_diff(&self, id: TaskId) -> Result<Option<String>>;
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<String>>;
/// Return assistant output messages (no diff) when available.
async fn get_task_messages(&self, id: TaskId) -> Result<Vec<String>>;
fn get_task_messages(&self, id: TaskId) -> CloudBackendFuture<'_, Vec<String>>;
/// Return the creating prompt and assistant messages (when available).
async fn get_task_text(&self, id: TaskId) -> Result<TaskText>;
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<Vec<TurnAttempt>>;
) -> CloudBackendFuture<'_, Vec<TurnAttempt>>;
/// 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<String>,
) -> Result<ApplyOutcome>;
async fn apply_task(&self, id: TaskId, diff_override: Option<String>) -> Result<ApplyOutcome>;
async fn create_task(
) -> CloudBackendFuture<'_, ApplyOutcome>;
fn apply_task(
&self,
env_id: &str,
prompt: &str,
git_ref: &str,
id: TaskId,
diff_override: Option<String>,
) -> 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<CreatedTask>;
) -> CloudBackendFuture<'a, CreatedTask>;
}
+47 -37
View File
@@ -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<i64>,
cursor: Option<&str>,
) -> Result<TaskListPage> {
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<TaskSummary> {
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<Option<String>> {
self.tasks_api().diff(id).await
fn get_task_diff(&self, id: TaskId) -> CloudBackendFuture<'_, Option<String>> {
Box::pin(async move { self.tasks_api().diff(id).await })
}
async fn get_task_messages(&self, id: TaskId) -> Result<Vec<String>> {
self.tasks_api().messages(id).await
fn get_task_messages(&self, id: TaskId) -> CloudBackendFuture<'_, Vec<String>> {
Box::pin(async move { self.tasks_api().messages(id).await })
}
async fn get_task_text(&self, id: TaskId) -> Result<TaskText> {
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<Vec<TurnAttempt>> {
self.attempts_api().list(task, turn_id).await
) -> CloudBackendFuture<'_, Vec<TurnAttempt>> {
Box::pin(async move { self.attempts_api().list(task, turn_id).await })
}
async fn apply_task(&self, id: TaskId, diff_override: Option<String>) -> Result<ApplyOutcome> {
self.apply_api()
.run(id, diff_override, /*preflight*/ false)
.await
}
async fn apply_task_preflight(
fn apply_task(
&self,
id: TaskId,
diff_override: Option<String>,
) -> Result<ApplyOutcome> {
self.apply_api()
.run(id, diff_override, /*preflight*/ true)
.await
) -> CloudBackendFuture<'_, ApplyOutcome> {
Box::pin(async move {
self.apply_api()
.run(id, diff_override, /*preflight*/ false)
.await
})
}
async fn create_task(
fn apply_task_preflight(
&self,
env_id: &str,
prompt: &str,
git_ref: &str,
id: TaskId,
diff_override: Option<String>,
) -> CloudBackendFuture<'_, ApplyOutcome> {
Box::pin(async move {
self.apply_api()
.run(id, diff_override, /*preflight*/ true)
.await
})
}
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<crate::CreatedTask> {
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
})
}
}
+1
View File
@@ -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;
@@ -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 }
+66 -2
View File
@@ -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<i64>,
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<String>> {
Box::pin(MockClient::get_task_diff(self, id))
}
fn get_task_messages(&self, id: TaskId) -> CloudBackendFuture<'_, Vec<String>> {
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<String>,
) -> CloudBackendFuture<'_, ApplyOutcome> {
Box::pin(MockClient::apply_task(self, id, diff_override))
}
fn apply_task_preflight(
&self,
id: TaskId,
diff_override: Option<String>,
) -> CloudBackendFuture<'_, ApplyOutcome> {
Box::pin(MockClient::apply_task_preflight(self, id, diff_override))
}
fn list_sibling_attempts(
&self,
task: TaskId,
turn_id: String,
) -> CloudBackendFuture<'_, Vec<TurnAttempt>> {
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" => {
-3
View File
@@ -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 }
+67 -45
View File
@@ -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<Option<String>, 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<i64>,
cursor: Option<&str>,
) -> codex_cloud_tasks_client::Result<codex_cloud_tasks_client::TaskListPage> {
) -> Result<codex_cloud_tasks_client::TaskListPage, CloudTaskError> {
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<TaskSummary> {
async fn get_task_summary(&self, id: TaskId) -> Result<TaskSummary, CloudTaskError> {
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<Option<String>> {
Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented(
"not used in test",
))
}
async fn get_task_messages(
&self,
_id: TaskId,
) -> codex_cloud_tasks_client::Result<Vec<String>> {
Ok(vec![])
}
async fn get_task_text(
&self,
_id: TaskId,
) -> codex_cloud_tasks_client::Result<codex_cloud_tasks_client::TaskText> {
) -> Result<codex_cloud_tasks_client::TaskText, CloudTaskError> {
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,
})
}
}
async fn list_sibling_attempts(
impl codex_cloud_tasks_client::CloudBackend for FakeBackend {
fn list_tasks<'a>(
&'a self,
env: Option<&'a str>,
limit: Option<i64>,
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<String>> {
Box::pin(async {
Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented(
"not used in test",
))
})
}
fn get_task_messages(&self, _id: TaskId) -> CloudBackendFuture<'_, Vec<String>> {
Box::pin(async { Ok(vec![]) })
}
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<Vec<codex_cloud_tasks_client::TurnAttempt>> {
Ok(Vec::new())
) -> CloudBackendFuture<'_, Vec<codex_cloud_tasks_client::TurnAttempt>> {
Box::pin(async { Ok(Vec::new()) })
}
async fn apply_task(
fn apply_task(
&self,
_id: TaskId,
_diff_override: Option<String>,
) -> codex_cloud_tasks_client::Result<codex_cloud_tasks_client::ApplyOutcome> {
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<String>,
) -> codex_cloud_tasks_client::Result<codex_cloud_tasks_client::ApplyOutcome> {
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<codex_cloud_tasks_client::CreatedTask> {
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",
))
})
}
}
+8 -5
View File
@@ -106,16 +106,20 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result<BackendContext>
})
}
#[async_trait::async_trait]
trait GitInfoProvider {
async fn default_branch_name(&self, path: &std::path::Path) -> Option<String>;
fn default_branch_name(
&self,
path: &std::path::Path,
) -> impl std::future::Future<Output = Option<String>> + Send;
async fn current_branch_name(&self, path: &std::path::Path) -> Option<String>;
fn current_branch_name(
&self,
path: &std::path::Path,
) -> impl std::future::Future<Output = Option<String>> + Send;
}
struct RealGitInfo;
#[async_trait::async_trait]
impl GitInfoProvider for RealGitInfo {
async fn default_branch_name(&self, path: &std::path::Path) -> Option<String> {
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<String> {
self.default_branch.clone()
-1
View File
@@ -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 }
+11 -6
View File
@@ -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<AuthError> 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<Request, AuthError> {
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<Box<dyn Future<Output = Result<Request, AuthError>> + Send + 'a>>;
/// Shared auth handle passed through API clients.
pub type SharedAuthProvider = Arc<dyn AuthProvider>;
@@ -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<Response, TransportError> {
Err(TransportError::Build("execute should not run".to_string()))
@@ -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<Response, TransportError> {
*self.last_request.lock().expect("lock request store") = Some(req);
@@ -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<Response, TransportError> {
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<Response, TransportError> {
*self.last_request.lock().expect("lock request store") = Some(req);
@@ -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<Response, TransportError> {
*self.last_request.lock().unwrap() = Some(req);
@@ -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<Response, TransportError> {
*self.last_request.lock().unwrap() = Some(req);
@@ -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<Response, TransportError> {
*self.last_request.lock().expect("lock request store") = Some(req);
+1
View File
@@ -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;
+8 -8
View File
@@ -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<Response, TransportError> {
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<Request, AuthError> {
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<Response, TransportError> {
Err(TransportError::Build("execute should not run".to_string()))
@@ -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<Response, TransportError> {
Err(TransportError::Build("execute should not run".to_string()))
-1
View File
@@ -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 }
+8 -5
View File
@@ -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<Response, TransportError>;
async fn stream(&self, req: Request) -> Result<StreamResponse, TransportError>;
fn execute(
&self,
req: Request,
) -> impl std::future::Future<Output = Result<Response, TransportError>> + Send;
fn stream(
&self,
req: Request,
) -> impl std::future::Future<Output = Result<StreamResponse, TransportError>> + 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<Response, TransportError> {
if enabled!(Level::TRACE) {
-1
View File
@@ -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 }
+1
View File
@@ -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;
+57 -55
View File
@@ -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<PathUri> {
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<Vec<u8>> {
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<u8>> {
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<u8>,
_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<FileMetadata> {
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<Vec<ReadDirectoryEntry>> {
unimplemented!("test filesystem only supports reads")
fn read_directory<'a>(
&'a self,
_path: &'a PathUri,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {
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") })
}
}
+23 -20
View File
@@ -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<Vec<ThreadConfigSource>, ThreadConfigLoadError>;
) -> ThreadConfigLoaderFuture<'_, Vec<ThreadConfigSource>>;
async fn load_config_layers(
fn load_config_layers(
&self,
context: ThreadConfigContext,
) -> Result<Vec<ConfigLayerEntry>, ThreadConfigLoadError> {
let sources = self.load(context).await?;
sources
.into_iter()
.map(thread_config_source_to_layer)
.collect::<Result<Vec<_>, _>>()
.map(|layers| layers.into_iter().flatten().collect())
) -> ThreadConfigLoaderFuture<'_, Vec<ConfigLayerEntry>> {
Box::pin(async move {
let sources = self.load(context).await?;
sources
.into_iter()
.map(thread_config_source_to_layer)
.collect::<Result<Vec<_>, _>>()
.map(|layers| layers.into_iter().flatten().collect())
})
}
}
pub type ThreadConfigLoaderFuture<'a, T> =
Pin<Box<dyn Future<Output = Result<T, ThreadConfigLoadError>> + 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<Vec<ThreadConfigSource>, ThreadConfigLoadError> {
Ok(self.sources.clone())
) -> ThreadConfigLoaderFuture<'_, Vec<ThreadConfigSource>> {
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<Vec<ThreadConfigSource>, ThreadConfigLoadError> {
Ok(Vec::new())
) -> ThreadConfigLoaderFuture<'_, Vec<ThreadConfigSource>> {
Box::pin(async { Ok(Vec::new()) })
}
}
+31 -6
View File
@@ -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<ThreadConfigSource>> {
Box::pin(RemoteThreadConfigLoader::load(self, context))
}
}
fn load_thread_config_request(
context: ThreadConfigContext,
) -> tonic::Request<proto::LoadThreadConfigRequest> {
@@ -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<proto::LoadThreadConfigRequest>,
@@ -341,6 +346,26 @@ mod tests {
}
}
impl thread_config_loader_server::ThreadConfigLoader for TestServer {
fn load<'a, 'async_trait>(
&'a self,
request: Request<proto::LoadThreadConfigRequest>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<Response<proto::LoadThreadConfigResponse>, 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");
-1
View File
@@ -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"] }
+81 -3
View File
@@ -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<u8>> {
Box::pin(FailingFileSystem::read_file(self, path, sandbox))
}
fn write_file<'a>(
&'a self,
path: &'a PathUri,
contents: Vec<u8>,
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<ReadDirectoryEntry>> {
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<UserInstructions>,
@@ -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<Option<ConfigState>> {
Ok(None)
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
Box::pin(async { Ok(None) })
}
async fn reload_now(&self) -> anyhow::Result<ConfigState> {
Ok(self.state.clone())
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
Box::pin(async { Ok(self.state.clone()) })
}
fn source_label(&self) -> String {
+15 -8
View File
@@ -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<Option<ConfigState>> {
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<ConfigState>> {
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;
@@ -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<Option<ConfigState>> {
Ok(None)
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
Box::pin(async { Ok(None) })
}
async fn reload_now(&self) -> anyhow::Result<ConfigState> {
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")) })
}
}
@@ -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<AdditionalPermissionProfile>,
}
#[async_trait::async_trait]
impl ShellCommandExecutor for CoreShellCommandExecutor {
async fn run(
fn run(
&self,
_command: Vec<String>,
_cwd: PathBuf,
env_overlay: HashMap<String, String>,
cancel_rx: CancellationToken,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> 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<String, String>,
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<String, String>,
cancel_rx: CancellationToken,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> anyhow::Result<ExecResult> {
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,
@@ -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;
+35 -23
View File
@@ -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<u64>,
}
#[async_trait]
impl BlockingTerminateExecProcess {
async fn read(&self) -> Result<ReadResponse, codex_exec_server::ExecServerError> {
Ok(ReadResponse {
chunks: Vec::new(),
next_seq: 1,
exited: false,
exit_code: None,
closed: false,
failure: None,
})
}
async fn write(&self) -> Result<WriteResponse, codex_exec_server::ExecServerError> {
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<u64>,
_max_bytes: Option<usize>,
_wait_ms: Option<u64>,
) -> Result<ReadResponse, ExecServerError> {
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<u8>) -> Result<WriteResponse, ExecServerError> {
Ok(WriteResponse {
status: WriteStatus::Accepted,
})
fn write(&self, _chunk: Vec<u8>) -> 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))
}
}
+38 -29
View File
@@ -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,26 +26,8 @@ struct MockExecProcess {
wake_tx: watch::Sender<u64>,
}
#[async_trait]
impl ExecProcess for MockExecProcess {
fn process_id(&self) -> &ProcessId {
&self.process_id
}
fn subscribe_wake(&self) -> watch::Receiver<u64> {
self.wake_tx.subscribe()
}
fn subscribe_events(&self) -> ExecProcessEventReceiver {
ExecProcessEventReceiver::empty()
}
async fn read(
&self,
_after_seq: Option<u64>,
_max_bytes: Option<usize>,
_wait_ms: Option<u64>,
) -> Result<ReadResponse, ExecServerError> {
impl MockExecProcess {
async fn read(&self) -> Result<ReadResponse, ExecServerError> {
Ok(self
.read_responses
.lock()
@@ -61,14 +43,6 @@ impl ExecProcess for MockExecProcess {
}))
}
async fn write(&self, _chunk: Vec<u8>) -> Result<WriteResponse, ExecServerError> {
Ok(self.write_response.clone())
}
async fn signal(&self, _signal: ProcessSignal) -> Result<(), ExecServerError> {
Ok(())
}
async fn terminate(&self) -> Result<(), ExecServerError> {
if let Some(message) = &self.terminate_error {
return Err(ExecServerError::Protocol(message.clone()));
@@ -77,6 +51,41 @@ impl ExecProcess for MockExecProcess {
}
}
impl ExecProcess for MockExecProcess {
fn process_id(&self) -> &ProcessId {
&self.process_id
}
fn subscribe_wake(&self) -> watch::Receiver<u64> {
self.wake_tx.subscribe()
}
fn subscribe_events(&self) -> ExecProcessEventReceiver {
ExecProcessEventReceiver::empty()
}
fn read(
&self,
_after_seq: Option<u64>,
_max_bytes: Option<usize>,
_wait_ms: Option<u64>,
) -> ExecProcessFuture<'_, ReadResponse> {
Box::pin(MockExecProcess::read(self))
}
fn write(&self, _chunk: Vec<u8>) -> ExecProcessFuture<'_, WriteResponse> {
Box::pin(async { Ok(self.write_response.clone()) })
}
fn signal(&self, _signal: ProcessSignal) -> ExecProcessFuture<'_, ()> {
Box::pin(async { Ok(()) })
}
fn terminate(&self) -> ExecProcessFuture<'_, ()> {
Box::pin(MockExecProcess::terminate(self))
}
}
async fn remote_process(
write_status: WriteStatus,
terminate_error: Option<String>,
+10
View File
@@ -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
-1
View File
@@ -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 }
@@ -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<EnvironmentProviderSnapshot, ExecServerError>;
fn snapshot(&self) -> EnvironmentProviderFuture<'_>;
}
pub type EnvironmentProviderFuture<'a> =
Pin<Box<dyn Future<Output = Result<EnvironmentProviderSnapshot, ExecServerError>> + 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<EnvironmentProviderSnapshot, ExecServerError> {
Ok(self.snapshot_inner())
fn snapshot(&self) -> EnvironmentProviderFuture<'_> {
Box::pin(async { Ok(self.snapshot_inner()) })
}
}
+7 -4
View File
@@ -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<EnvironmentProviderSnapshot, ExecServerError> {
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>,
+4
View File
@@ -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;
+243 -7
View File
@@ -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<u8>> {
Box::pin(LocalFileSystem::read_file(self, path, sandbox))
}
fn write_file<'a>(
&'a self,
path: &'a PathUri,
contents: Vec<u8>,
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<ReadDirectoryEntry>> {
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<u8>> {
Box::pin(UnsandboxedFileSystem::read_file(self, path, sandbox))
}
fn write_file<'a>(
&'a self,
path: &'a PathUri,
contents: Vec<u8>,
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<ReadDirectoryEntry>> {
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<u8>> {
Box::pin(DirectFileSystem::read_file(self, path, sandbox))
}
fn write_file<'a>(
&'a self,
path: &'a PathUri,
contents: Vec<u8>,
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<ReadDirectoryEntry>> {
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(
+43 -15
View File
@@ -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<StartedExecProcess, ExecServerError> {
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<u64> {
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<u64>,
@@ -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<u64> {
self.wake_tx.subscribe()
}
fn subscribe_events(&self) -> ExecProcessEventReceiver {
self.events.subscribe()
}
fn read(
&self,
after_seq: Option<u64>,
max_bytes: Option<usize>,
wait_ms: Option<u64>,
) -> ExecProcessFuture<'_, ReadResponse> {
Box::pin(LocalExecProcess::read(self, after_seq, max_bytes, wait_ms))
}
fn write(&self, chunk: Vec<u8>) -> 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,
+14 -9
View File
@@ -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<u64>,
max_bytes: Option<usize>,
wait_ms: Option<u64>,
) -> Result<ReadResponse, ExecServerError>;
) -> ExecProcessFuture<'_, ReadResponse>;
async fn write(&self, chunk: Vec<u8>) -> Result<WriteResponse, ExecServerError>;
fn write(&self, chunk: Vec<u8>) -> 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<Box<dyn Future<Output = Result<T, ExecServerError>> + Send + 'a>>;
pub trait ExecBackend: Send + Sync {
async fn start(&self, params: ExecParams) -> Result<StartedExecProcess, ExecServerError>;
fn start(&self, params: ExecParams) -> ExecBackendFuture<'_>;
}
pub type ExecBackendFuture<'a> =
Pin<Box<dyn Future<Output = Result<StartedExecProcess, ExecServerError>> + Send + 'a>>;
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
+80 -4
View File
@@ -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<u8>> {
Box::pin(RemoteFileSystem::read_file(self, path, sandbox))
}
fn write_file<'a>(
&'a self,
path: &'a PathUri,
contents: Vec<u8>,
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<ReadDirectoryEntry>> {
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<FileSystemSandboxContext> {
+47 -19
View File
@@ -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<StartedExecProcess, ExecServerError> {
async fn start(
&self,
params: ExecParams,
) -> Result<StartedExecProcess, crate::ExecServerError> {
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<u64>,
max_bytes: Option<usize>,
wait_ms: Option<u64>,
) -> Result<ReadResponse, crate::ExecServerError> {
self.session.read(after_seq, max_bytes, wait_ms).await
}
async fn write(&self, chunk: Vec<u8>) -> Result<WriteResponse, crate::ExecServerError> {
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<u64>,
max_bytes: Option<usize>,
wait_ms: Option<u64>,
) -> Result<ReadResponse, ExecServerError> {
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<u8>) -> Result<WriteResponse, ExecServerError> {
trace!("exec process write");
self.session.write(chunk).await
fn write(&self, chunk: Vec<u8>) -> 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))
}
}
@@ -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<u8>> {
Box::pin(SandboxedFileSystem::read_file(self, path, sandbox))
}
fn write_file<'a>(
&'a self,
path: &'a PathUri,
contents: Vec<u8>,
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<ReadDirectoryEntry>> {
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)
}
-1
View File
@@ -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"] }
@@ -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<PathUri> {
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<Vec<u8>> {
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<Vec<ReadDirectoryEntry>> {
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<FileMetadata> {
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<PathUri> {
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<Vec<u8>> {
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<u8>> {
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<u8>,
_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<FileMetadata> {
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<Vec<ReadDirectoryEntry>> {
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<ReadDirectoryEntry>> {
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")) })
}
}
-1
View File
@@ -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 }
+56 -50
View File
@@ -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<T> = io::Result<T>;
/// Future returned by [`ExecutorFileSystem`] operations.
pub type ExecutorFileSystemFuture<'a, T> =
Pin<Box<dyn Future<Output = FileSystemResult<T>> + 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<PathUri>;
fn canonicalize<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, PathUri>;
async fn read_file(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>>;
fn read_file<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<u8>>;
/// Reads a file and decodes it as UTF-8 text.
async fn read_file_text(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<String> {
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<u8>,
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<()>;
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()>;
async fn get_metadata(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata>;
fn get_metadata<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, FileMetadata>;
async fn read_directory(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>>;
fn read_directory<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>>;
async fn remove(
&self,
path: &PathUri,
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, ()>;
}
-1
View File
@@ -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 }
+18 -8
View File
@@ -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<ExternalAuthTokens>> {
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")
+8 -6
View File
@@ -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<String>,
}
#[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<Option<ExternalAuthTokens>> {
Ok(None)
fn resolve(&self) -> ExternalAuthFuture<'_, Option<ExternalAuthTokens>> {
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<ExternalAuthTokens>;
) -> ExternalAuthFuture<'_, ExternalAuthTokens>;
}
pub type ExternalAuthFuture<'a, T> = Pin<Box<dyn Future<Output = std::io::Result<T>> + Send + 'a>>;
impl RefreshTokenError {
pub fn failed_reason(&self) -> Option<RefreshTokenFailedReason> {
match self {
+1
View File
@@ -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;
+2 -9
View File
@@ -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<Box<dyn Future<Output = Option<CodexAuth>> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
fn auth(&self) -> ModelProviderFuture<'_, Option<CodexAuth>> {
let delegate = Arc::clone(&self.delegate);
Box::pin(async move { delegate.auth().await })
}
-1
View File
@@ -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 }
@@ -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<Request, AuthError> {
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;
@@ -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<CodexAuth> {
self.managed_auth().map(CodexAuth::BedrockApiKey)
}
async fn api_provider(&self) -> Result<Provider> {
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<Option<String>> {
let managed_auth = self.managed_auth();
Ok(Some(
runtime_base_url(managed_auth.as_ref(), &self.aws).await?,
))
}
async fn api_auth(&self) -> Result<SharedAuthProvider> {
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<CodexAuth> {
self.managed_auth().map(CodexAuth::BedrockApiKey)
fn auth(&self) -> ModelProviderFuture<'_, Option<CodexAuth>> {
Box::pin(AmazonBedrockModelProvider::auth(self))
}
fn account_state(&self) -> ProviderAccountResult {
@@ -112,24 +136,16 @@ impl ModelProvider for AmazonBedrockModelProvider {
})
}
async fn api_provider(&self) -> Result<Provider> {
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<Provider>> {
Box::pin(AmazonBedrockModelProvider::api_provider(self))
}
async fn runtime_base_url(&self) -> Result<Option<String>> {
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<Option<String>>> {
Box::pin(AmazonBedrockModelProvider::runtime_base_url(self))
}
async fn api_auth(&self) -> Result<SharedAuthProvider> {
let managed_auth = self.managed_auth();
resolve_provider_auth(managed_auth.as_ref(), &self.aws).await
fn api_auth(&self) -> ModelProviderFuture<'_, Result<SharedAuthProvider>> {
Box::pin(AmazonBedrockModelProvider::api_auth(self))
}
fn models_manager(
+1
View File
@@ -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;
+26 -16
View File
@@ -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<ModelInfo>, Option<String>)>> {
Box::pin(OpenAiModelsEndpoint::list_models(self, client_version))
}
}
#[derive(Clone)]
+29 -17
View File
@@ -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<Arc<AuthManager>>;
/// Returns the current provider-scoped auth value, if one is configured.
async fn auth(&self) -> Option<CodexAuth>;
fn auth(&self) -> ModelProviderFuture<'_, Option<CodexAuth>>;
/// 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<Provider> {
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<Provider>> {
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<Option<String>> {
Ok(self.info().base_url.clone())
fn runtime_base_url(
&self,
) -> ModelProviderFuture<'_, codex_protocol::error::Result<Option<String>>> {
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<SharedAuthProvider> {
let auth = self.auth().await;
resolve_provider_auth(auth.as_ref(), self.info())
fn api_auth(
&self,
) -> ModelProviderFuture<'_, codex_protocol::error::Result<SharedAuthProvider>> {
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<Box<dyn Future<Output = T> + Send + 'a>>;
/// Shared runtime model provider handle.
pub type SharedModelProvider = Arc<dyn ModelProvider>;
@@ -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<CodexAuth> {
match self.auth_manager.as_ref() {
Some(auth_manager) => auth_manager.auth().await,
None => None,
}
fn auth(&self) -> ModelProviderFuture<'_, Option<CodexAuth>> {
Box::pin(async move {
match self.auth_manager.as_ref() {
Some(auth_manager) => auth_manager.auth().await,
None => None,
}
})
}
fn account_state(&self) -> ProviderAccountResult {
-1
View File
@@ -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 }
+98 -63
View File
@@ -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<ModelInfo>, Option<String>)>;
fn list_models<'a>(
&'a self,
client_version: &'a str,
) -> ModelsEndpointFuture<'a, CoreResult<(Vec<ModelInfo>, Option<String>)>>;
}
pub type ModelsEndpointFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 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<dyn ModelsEndpointClient>;
/// 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<ModelPreset> {
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<ModelPreset>> {
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<ModelInfo>;
fn get_remote_models(&self) -> ModelsManagerFuture<'_, Vec<ModelInfo>>;
/// 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<String>,
fn get_default_model<'a>(
&'a self,
model: &'a Option<String>,
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<Box<dyn Future<Output = T> + Send + 'a>>;
/// Shared model manager handle used across runtime services.
pub type SharedModelsManager = Arc<dyn ModelsManager>;
@@ -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<ModelInfo> {
self.remote_models.read().await.clone()
fn get_remote_models(&self) -> ModelsManagerFuture<'_, Vec<ModelInfo>> {
Box::pin(async move { self.remote_models.read().await.clone() })
}
fn try_get_remote_models(&self) -> Result<Vec<ModelInfo>, 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<ModelInfo> {
self.remote_models.clone()
fn get_remote_models(&self) -> ModelsManagerFuture<'_, Vec<ModelInfo>> {
Box::pin(async { self.remote_models.clone() })
}
fn try_get_remote_models(&self) -> Result<Vec<ModelInfo>, 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<Vec<ModelInfo>, std::io::Error> {
+80 -68
View File
@@ -100,64 +100,8 @@ impl TestModelsEndpoint {
fn fetch_count(&self) -> usize {
self.fetch_count.load(Ordering::SeqCst)
}
}
#[derive(Debug)]
struct TestExternalApiKeyAuth;
#[async_trait]
impl ExternalAuth for TestExternalApiKeyAuth {
fn auth_mode(&self) -> AuthMode {
AuthMode::ApiKey
}
async fn resolve(&self) -> std::io::Result<Option<ExternalAuthTokens>> {
Ok(Some(ExternalAuthTokens::access_token_only(
"test-external-api-key",
)))
}
async fn refresh(
&self,
_context: ExternalAuthRefreshContext,
) -> std::io::Result<ExternalAuthTokens> {
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(
&self,
_context: ExternalAuthRefreshContext,
) -> std::io::Result<ExternalAuthTokens> {
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
}
async fn list_models(
&self,
_client_version: &str,
) -> CoreResult<(Vec<ModelInfo>, Option<String>)> {
async fn list_models(&self) -> CoreResult<(Vec<ModelInfo>, Option<String>)> {
self.fetch_count.fetch_add(1, Ordering::SeqCst);
let models = self
.responses
@@ -169,6 +113,67 @@ impl ModelsEndpointClient for TestModelsEndpoint {
}
}
#[derive(Debug)]
struct TestExternalApiKeyAuth;
impl ExternalAuth for TestExternalApiKeyAuth {
fn auth_mode(&self) -> AuthMode {
AuthMode::ApiKey
}
fn resolve(&self) -> codex_login::ExternalAuthFuture<'_, Option<ExternalAuthTokens>> {
Box::pin(async {
Ok(Some(ExternalAuthTokens::access_token_only(
"test-external-api-key",
)))
})
}
fn refresh(
&self,
_context: ExternalAuthRefreshContext,
) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> {
Box::pin(async {
Ok(ExternalAuthTokens::access_token_only(
"test-external-api-key",
))
})
}
}
#[derive(Debug)]
struct TestUnresolvedExternalApiKeyAuth;
impl ExternalAuth for TestUnresolvedExternalApiKeyAuth {
fn auth_mode(&self) -> AuthMode {
AuthMode::ApiKey
}
fn refresh(
&self,
_context: ExternalAuthRefreshContext,
) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> {
Box::pin(async { Err(std::io::Error::other("unresolved test auth")) })
}
}
impl ModelsEndpointClient for TestModelsEndpoint {
fn has_command_auth(&self) -> bool {
self.has_command_auth
}
fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool> {
Box::pin(async { self.uses_codex_backend })
}
fn list_models<'a>(
&'a self,
_client_version: &'a str,
) -> ModelsEndpointFuture<'a, CoreResult<(Vec<ModelInfo>, Option<String>)>> {
Box::pin(TestModelsEndpoint::list_models(self))
}
}
fn openai_manager_for_tests(
codex_home: std::path::PathBuf,
endpoint_client: Arc<dyn ModelsEndpointClient>,
@@ -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<ModelInfo>, Option<String>)> {
async fn list_models(&self) -> CoreResult<(Vec<ModelInfo>, Option<String>)> {
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<ModelInfo>, Option<String>)>> {
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";
-1
View File
@@ -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 }
+3
View File
@@ -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;
+15 -15
View File
@@ -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<Box<dyn Future<Output = NetworkDecision> + Send + 'a>>;
impl<D: NetworkPolicyDecider + ?Sized> NetworkPolicyDecider for Arc<D> {
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<F, Fut> NetworkPolicyDecider for F
where
F: Fn(NetworkPolicyRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = NetworkDecision> + Send,
Fut: Future<Output = NetworkDecision> + 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<Option<ConfigState>> {
Ok(None)
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
Box::pin(async { Ok(None) })
}
async fn reload_now(&self) -> anyhow::Result<ConfigState> {
Ok(self.state.clone())
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
Box::pin(async { Ok(self.state.clone()) })
}
fn source_label(&self) -> String {
+17 -18
View File
@@ -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<Option<ConfigState>>;
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>>;
/// Force a reload, regardless of whether a change was detected.
async fn reload_now(&self) -> Result<ConfigState>;
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState>;
}
#[async_trait]
pub type ConfigReloaderFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + 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<Box<dyn Future<Output = ()> + Send + 'a>>;
impl<O: BlockedRequestObserver + ?Sized> BlockedRequestObserver for Arc<O> {
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<F, Fut> BlockedRequestObserver for F
where
F: Fn(BlockedRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send,
Fut: Future<Output = ()> + 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<Option<ConfigState>> {
Ok(None)
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
Box::pin(async { Ok(None) })
}
async fn reload_now(&self) -> Result<ConfigState> {
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")) })
}
}
+5 -6
View File
@@ -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<Option<ConfigState>> {
Ok(None)
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
Box::pin(async { Ok(None) })
}
async fn reload_now(&self) -> anyhow::Result<ConfigState> {
Ok(self.state.clone())
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
Box::pin(async { Ok(self.state.clone()) })
}
fn source_label(&self) -> String {
-1
View File
@@ -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 }
+2 -6
View File
@@ -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<Output = ControlFlow<()>> + 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<ThreadCandidate>,
}
#[async_trait]
impl<'a> RolloutFileVisitor for FilesByUpdatedAtVisitor<'a> {
async fn visit(
&mut self,
-1
View File
@@ -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"] }
+5 -5
View File
@@ -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<Option<ConfigState>> {
Ok(None)
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
Box::pin(async { Ok(None) })
}
async fn reload_now(&self) -> anyhow::Result<ConfigState> {
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")) })
}
}
-1
View File
@@ -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 }
+4
View File
@@ -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;
@@ -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<String>,
cwd: PathBuf,
env_overlay: HashMap<String, String>,
cancel_rx: CancellationToken,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> anyhow::Result<ExecResult>;
) -> 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<String, String>,
execution: EscalationExecution,
) -> anyhow::Result<PreparedExec>;
) -> ShellCommandExecutorFuture<'a, PreparedExec>;
}
pub type ShellCommandExecutorFuture<'a, T> =
Pin<Box<dyn Future<Output = anyhow::Result<T>> + 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<EscalationDecision> {
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<EscalationDecision> {
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<String>,
_cwd: PathBuf,
_env_overlay: HashMap<String, String>,
_cancel_rx: CancellationToken,
_after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> anyhow::Result<ExecResult> {
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<String, String>,
_execution: EscalationExecution,
) -> anyhow::Result<PreparedExec> {
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<String>,
_cwd: PathBuf,
_env_overlay: HashMap<String, String>,
_cancel_rx: CancellationToken,
_after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> anyhow::Result<ExecResult> {
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<String, String>,
_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<String>,
_cwd: PathBuf,
_env_overlay: HashMap<String, String>,
_cancel_rx: CancellationToken,
_after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> 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<String, String>,
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<i32> {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
@@ -534,14 +572,10 @@ mod tests {
after_spawn_invoked: Arc<AtomicBool>,
}
#[async_trait::async_trait]
impl ShellCommandExecutor for AfterSpawnAssertingShellCommandExecutor {
impl AfterSpawnAssertingShellCommandExecutor {
async fn run(
&self,
_command: Vec<String>,
_cwd: PathBuf,
env_overlay: HashMap<String, String>,
_cancel_rx: CancellationToken,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> anyhow::Result<ExecResult> {
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<String>,
_cwd: PathBuf,
env_overlay: HashMap<String, String>,
_cancel_rx: CancellationToken,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> 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<String, String>,
_execution: EscalationExecution,
) -> anyhow::Result<PreparedExec> {
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") })
}
}
@@ -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<EscalationDecision>;
fn determine_action<'a>(
&'a self,
file: &'a AbsolutePathBuf,
argv: &'a [String],
workdir: &'a AbsolutePathBuf,
) -> EscalationPolicyFuture<'a>;
}
pub type EscalationPolicyFuture<'a> =
Pin<Box<dyn Future<Output = anyhow::Result<EscalationDecision>> + Send + 'a>>;
@@ -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;
-1
View File
@@ -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 }
+98 -43
View File
@@ -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<ThreadPage> {
async fn list_threads(&self) -> ThreadStoreResult<ThreadPage> {
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<StoredThread> {
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,
+1
View File
@@ -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;
+70 -58
View File
@@ -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<StoredThread> {
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<StoredThread> {
@@ -261,38 +222,89 @@ impl ThreadStore for LocalThreadStore {
)
.await
}
}
async fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreResult<ThreadPage> {
list_threads::list_threads(self, params).await
impl ThreadStore for LocalThreadStore {
fn as_any(&self) -> &dyn std::any::Any {
self
}
async fn search_threads(
fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreFuture<'_, ()> {
Box::pin(async move { live_writer::create_thread(self, params).await })
}
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<ThreadSearchPage> {
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<StoredThread> {
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<StoredThread> {
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 })
}
}
+39 -33
View File
@@ -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<Box<dyn Future<Output = ThreadStoreResult<T>> + 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<StoredThreadHistory>;
) -> ThreadStoreFuture<'_, StoredThreadHistory>;
/// Reads a thread summary and optionally its persisted history.
async fn read_thread(&self, params: ReadThreadParams) -> ThreadStoreResult<StoredThread>;
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<StoredThread>;
) -> ThreadStoreFuture<'_, StoredThread>;
/// Lists stored threads matching the supplied filters.
async fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreResult<ThreadPage>;
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<ThreadSearchPage> {
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<TurnPage> {
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<ItemPage> {
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<StoredThread>;
) -> 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<StoredThread>;
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<'_, ()>;
}
+2 -3
View File
@@ -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,
-1
View File
@@ -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"] }
+8 -5
View File
@@ -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<Token, errors::ReadinessError>;
fn subscribe(
&self,
) -> impl std::future::Future<Output = Result<Token, errors::ReadinessError>> + 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<bool, errors::ReadinessError>;
fn mark_ready(
&self,
token: Token,
) -> impl std::future::Future<Output = Result<bool, errors::ReadinessError>> + Send;
/// Asynchronously wait until the flag becomes ready.
async fn wait_ready(&self);
fn wait_ready(&self) -> impl std::future::Future<Output = ()> + 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() {