codex: pass attached executor capability to tools

This commit is contained in:
starr-openai
2026-04-01 18:25:11 -07:00
parent bd9c85eedf
commit f984e97d6e
14 changed files with 234 additions and 120 deletions

View File

@@ -1,6 +1,5 @@
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::OnceCell;
use crate::ExecServerClient;
@@ -10,8 +9,6 @@ use crate::file_system::ExecutorFileSystem;
use crate::local_file_system::LocalFileSystem;
use crate::local_process::LocalProcess;
use crate::process::ExecBackend;
use crate::process::StartedExecProcess;
use crate::protocol::ExecParams;
use crate::remote_file_system::RemoteFileSystem;
use crate::remote_process::RemoteProcess;
@@ -21,6 +18,57 @@ pub trait ExecutorEnvironment: Send + Sync {
fn get_exec_backend(&self) -> Arc<dyn ExecBackend>;
}
#[derive(Clone)]
pub struct AttachedExecutor {
exec_server_url: Option<String>,
exec_backend: Arc<dyn ExecBackend>,
file_system: Arc<dyn ExecutorFileSystem>,
}
impl std::fmt::Debug for AttachedExecutor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AttachedExecutor")
.field("exec_server_url", &self.exec_server_url)
.finish_non_exhaustive()
}
}
impl AttachedExecutor {
fn new_local(exec_backend: Arc<dyn ExecBackend>) -> Self {
Self {
exec_server_url: None,
exec_backend,
file_system: Arc::new(LocalFileSystem),
}
}
fn new_remote(exec_server_url: String, client: ExecServerClient) -> Self {
Self {
exec_server_url: Some(exec_server_url),
exec_backend: Arc::new(RemoteProcess::new(client.clone())),
file_system: Arc::new(RemoteFileSystem::new(client)),
}
}
pub fn exec_server_url(&self) -> Option<&str> {
self.exec_server_url.as_deref()
}
pub fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
Arc::clone(&self.exec_backend)
}
pub fn get_filesystem(&self) -> Arc<dyn ExecutorFileSystem> {
Arc::clone(&self.file_system)
}
}
impl ExecutorEnvironment for AttachedExecutor {
fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
self.get_exec_backend()
}
}
#[derive(Debug, Default)]
pub struct EnvironmentManager {
executor_mode: ExecutorMode,
@@ -88,8 +136,7 @@ impl ExecutorMode {
#[derive(Clone)]
pub struct Environment {
executor_mode: ExecutorMode,
remote_exec_server_client: Option<ExecServerClient>,
exec_backend: Arc<dyn ExecBackend>,
attached_executor: Option<Arc<AttachedExecutor>>,
}
impl Default for Environment {
@@ -104,8 +151,9 @@ impl Default for Environment {
Self {
executor_mode: ExecutorMode::LocalExecutor,
remote_exec_server_client: None,
exec_backend: Arc::new(local_process),
attached_executor: Some(Arc::new(AttachedExecutor::new_local(Arc::new(
local_process,
)))),
}
}
}
@@ -124,40 +172,36 @@ impl Environment {
}
async fn create_with_mode(executor_mode: ExecutorMode) -> Result<Self, ExecServerError> {
let remote_exec_server_client = if let Some(url) = executor_mode.remote_exec_server_url() {
Some(
ExecServerClient::connect_websocket(RemoteExecServerConnectArgs {
websocket_url: url.to_string(),
client_name: "codex-environment".to_string(),
connect_timeout: std::time::Duration::from_secs(5),
initialize_timeout: std::time::Duration::from_secs(5),
})
.await?,
)
} else {
let attached_executor = if let Some(url) = executor_mode.remote_exec_server_url() {
let client = ExecServerClient::connect_websocket(RemoteExecServerConnectArgs {
websocket_url: url.to_string(),
client_name: "codex-environment".to_string(),
connect_timeout: std::time::Duration::from_secs(5),
initialize_timeout: std::time::Duration::from_secs(5),
})
.await?;
Some(Arc::new(AttachedExecutor::new_remote(
url.to_string(),
client,
)))
} else if matches!(executor_mode, ExecutorMode::NoExecutor) {
None
} else {
let local_process = LocalProcess::default();
local_process
.initialize()
.map_err(|err| ExecServerError::Protocol(err.message))?;
local_process
.initialized()
.map_err(ExecServerError::Protocol)?;
Some(Arc::new(AttachedExecutor::new_local(Arc::new(
local_process,
))))
};
let exec_backend: Arc<dyn ExecBackend> =
if let Some(client) = remote_exec_server_client.clone() {
Arc::new(RemoteProcess::new(client))
} else if matches!(executor_mode, ExecutorMode::NoExecutor) {
Arc::new(NoAttachedExecutorBackend)
} else {
let local_process = LocalProcess::default();
local_process
.initialize()
.map_err(|err| ExecServerError::Protocol(err.message))?;
local_process
.initialized()
.map_err(ExecServerError::Protocol)?;
Arc::new(local_process)
};
Ok(Self {
executor_mode,
remote_exec_server_client,
exec_backend,
attached_executor,
})
}
@@ -169,28 +213,22 @@ impl Environment {
self.executor_mode.has_attached_executor()
}
pub fn attached_executor(&self) -> Option<Arc<AttachedExecutor>> {
self.attached_executor.clone()
}
pub fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
Arc::clone(&self.exec_backend)
self.attached_executor
.as_ref()
.expect("environment does not have an attached executor")
.get_exec_backend()
}
pub fn get_filesystem(&self) -> Arc<dyn ExecutorFileSystem> {
if let Some(client) = self.remote_exec_server_client.clone() {
Arc::new(RemoteFileSystem::new(client))
} else {
Arc::new(LocalFileSystem)
}
}
}
#[derive(Clone, Default)]
struct NoAttachedExecutorBackend;
#[async_trait]
impl ExecBackend for NoAttachedExecutorBackend {
async fn start(&self, _params: ExecParams) -> Result<StartedExecProcess, ExecServerError> {
Err(ExecServerError::Protocol(
"no attached executor is configured for this session".to_string(),
))
self.attached_executor
.as_ref()
.expect("environment does not have an attached executor")
.get_filesystem()
}
}
@@ -206,7 +244,7 @@ fn parse_executor_mode(exec_server_url: Option<String>) -> ExecutorMode {
impl ExecutorEnvironment for Environment {
fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
Arc::clone(&self.exec_backend)
self.get_exec_backend()
}
}
@@ -216,6 +254,8 @@ mod tests {
use super::Environment;
use super::EnvironmentManager;
use super::ExecutorMode;
use super::parse_executor_mode;
use crate::ProcessId;
use pretty_assertions::assert_eq;
@@ -228,7 +268,7 @@ mod tests {
assert_eq!(environment.exec_server_url(), None);
assert!(environment.has_attached_executor());
assert_eq!(environment.executor_mode, ExecutorMode::LocalExecutor);
assert!(environment.remote_exec_server_client.is_none());
assert!(environment.attached_executor().is_some());
}
#[test]
@@ -307,31 +347,15 @@ mod tests {
assert_eq!(environment.exec_server_url(), None);
assert!(!environment.has_attached_executor());
assert_eq!(environment.executor_mode, ExecutorMode::NoExecutor);
assert!(environment.remote_exec_server_client.is_none());
assert!(environment.attached_executor().is_none());
}
#[tokio::test]
async fn no_executor_environment_rejects_exec_start() {
async fn no_executor_environment_has_no_executor_capability() {
let environment = Environment::create(Some("none".to_string()))
.await
.expect("create environment");
let err = environment
.get_exec_backend()
.start(crate::ExecParams {
process_id: ProcessId::from("no-executor-proc"),
argv: vec!["true".to_string()],
cwd: std::env::current_dir().expect("read current dir"),
env: Default::default(),
tty: false,
arg0: None,
})
.await
.expect_err("no-executor backend should reject starts");
assert_eq!(
err.to_string(),
"exec-server protocol error: no attached executor is configured for this session"
);
assert!(environment.attached_executor().is_none());
}
}

View File

@@ -31,6 +31,7 @@ pub use codex_app_server_protocol::FsRemoveParams;
pub use codex_app_server_protocol::FsRemoveResponse;
pub use codex_app_server_protocol::FsWriteFileParams;
pub use codex_app_server_protocol::FsWriteFileResponse;
pub use environment::AttachedExecutor;
pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
pub use environment::Environment;
pub use environment::EnvironmentManager;