mirror of
https://github.com/openai/codex.git
synced 2026-04-05 23:21:43 +03:00
Compare commits
2 Commits
latest-alp
...
codex/ca41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b659579dc | ||
|
|
bc174daf8e |
@@ -951,6 +951,7 @@ impl TurnContext {
|
||||
.with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone())
|
||||
.with_web_search_config(self.tools_config.web_search_config.clone())
|
||||
.with_allow_login_shell(self.tools_config.allow_login_shell)
|
||||
.with_environment_capabilities(self.tools_config.environment_capabilities)
|
||||
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
|
||||
&config.agent_roles,
|
||||
));
|
||||
@@ -1411,6 +1412,10 @@ impl Session {
|
||||
)
|
||||
.with_web_search_config(per_turn_config.web_search_config.clone())
|
||||
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
|
||||
.with_environment_capabilities(codex_tools::ToolEnvironmentCapabilities::new(
|
||||
environment.exec_enabled(),
|
||||
environment.filesystem_enabled(),
|
||||
))
|
||||
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
|
||||
&per_turn_config.agent_roles,
|
||||
));
|
||||
@@ -5535,6 +5540,10 @@ async fn spawn_review_thread(
|
||||
)
|
||||
.with_web_search_config(/*web_search_config*/ None)
|
||||
.with_allow_login_shell(config.permissions.allow_login_shell)
|
||||
.with_environment_capabilities(codex_tools::ToolEnvironmentCapabilities::new(
|
||||
parent_turn_context.environment.exec_enabled(),
|
||||
parent_turn_context.environment.filesystem_enabled(),
|
||||
))
|
||||
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
|
||||
&config.agent_roles,
|
||||
));
|
||||
|
||||
@@ -77,8 +77,8 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
config,
|
||||
auth_manager,
|
||||
models_manager,
|
||||
environment_manager: Arc::new(EnvironmentManager::new(
|
||||
parent_ctx.environment.exec_server_url().map(str::to_owned),
|
||||
environment_manager: Arc::new(EnvironmentManager::from_mode(
|
||||
parent_ctx.environment.mode().clone(),
|
||||
)),
|
||||
skills_manager: Arc::clone(&parent_session.services.skills_manager),
|
||||
plugins_manager: Arc::clone(&parent_session.services.plugins_manager),
|
||||
|
||||
@@ -239,7 +239,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.await?
|
||||
{
|
||||
Some(prepared) => {
|
||||
if ctx.turn.environment.exec_server_url().is_some() {
|
||||
if ctx.turn.environment.mode().is_remote() {
|
||||
return Err(ToolError::Rejected(
|
||||
"unified_exec zsh-fork is not supported when exec_server_url is configured".to_string(),
|
||||
));
|
||||
|
||||
@@ -593,7 +593,13 @@ impl UnifiedExecProcessManager {
|
||||
.ok_or(UnifiedExecError::MissingCommandLine)?;
|
||||
let inherited_fds = spawn_lifecycle.inherited_fds();
|
||||
|
||||
if environment.exec_server_url().is_some() {
|
||||
if !environment.exec_enabled() {
|
||||
return Err(UnifiedExecError::create_process(
|
||||
"environment is disabled; exec_command is unavailable".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if environment.mode().is_remote() {
|
||||
if !inherited_fds.is_empty() {
|
||||
return Err(UnifiedExecError::create_process(
|
||||
"remote exec-server does not support inherited file descriptors".to_string(),
|
||||
|
||||
@@ -1,50 +1,175 @@
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use crate::CopyOptions;
|
||||
use crate::CreateDirectoryOptions;
|
||||
use crate::ExecServerClient;
|
||||
use crate::ExecServerError;
|
||||
use crate::ReadDirectoryEntry;
|
||||
use crate::RemoteExecServerConnectArgs;
|
||||
use crate::RemoveOptions;
|
||||
use crate::StartedExecProcess;
|
||||
use crate::file_system::ExecutorFileSystem;
|
||||
use crate::file_system::FileMetadata;
|
||||
use crate::local_file_system::LocalFileSystem;
|
||||
use crate::local_process::LocalProcess;
|
||||
use crate::process::ExecBackend;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::remote_file_system::RemoteFileSystem;
|
||||
use crate::remote_process::RemoteProcess;
|
||||
|
||||
pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
|
||||
|
||||
/// Describes where execution and filesystem operations for a session come from.
|
||||
///
|
||||
/// `CODEX_EXEC_SERVER_URL=none` maps to [`EnvironmentMode::Disabled`] so callers
|
||||
/// can distinguish "intentionally unavailable" from "use the local executor".
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum EnvironmentMode {
|
||||
/// Run against the local process and filesystem implementations.
|
||||
Local,
|
||||
/// Run against a remote exec-server endpoint.
|
||||
Remote { exec_server_url: String },
|
||||
/// Disable executor-backed capabilities for this session entirely.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// Feature-style view of what a selected environment supports.
|
||||
///
|
||||
/// Tool building and runtime guards should prefer these booleans over
|
||||
/// re-interpreting environment URLs so future environment modes can evolve
|
||||
/// without touching every call site.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct EnvironmentCapabilities {
|
||||
exec_enabled: bool,
|
||||
filesystem_enabled: bool,
|
||||
}
|
||||
|
||||
impl EnvironmentCapabilities {
|
||||
/// Creates a capability set for a concrete environment mode.
|
||||
pub fn new(exec_enabled: bool, filesystem_enabled: bool) -> Self {
|
||||
Self {
|
||||
exec_enabled,
|
||||
filesystem_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether process execution should be exposed.
|
||||
pub fn exec_enabled(self) -> bool {
|
||||
self.exec_enabled
|
||||
}
|
||||
|
||||
/// Returns whether filesystem-backed tools should be exposed.
|
||||
pub fn filesystem_enabled(self) -> bool {
|
||||
self.filesystem_enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentMode {
|
||||
/// Returns the remote exec-server URL when this mode is remote.
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Local | Self::Disabled => None,
|
||||
Self::Remote { exec_server_url } => Some(exec_server_url.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this mode uses a remote exec-server.
|
||||
pub fn is_remote(&self) -> bool {
|
||||
matches!(self, Self::Remote { .. })
|
||||
}
|
||||
|
||||
/// Returns whether this mode disables environment-backed APIs.
|
||||
pub fn is_disabled(&self) -> bool {
|
||||
matches!(self, Self::Disabled)
|
||||
}
|
||||
|
||||
/// Returns the tool/runtime capabilities implied by this mode.
|
||||
pub fn capabilities(&self) -> EnvironmentCapabilities {
|
||||
match self {
|
||||
Self::Local | Self::Remote { .. } => EnvironmentCapabilities::new(
|
||||
/*exec_enabled*/ true, /*filesystem_enabled*/ true,
|
||||
),
|
||||
Self::Disabled => EnvironmentCapabilities::new(
|
||||
/*exec_enabled*/ false, /*filesystem_enabled*/ false,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_exec_server_url(exec_server_url: Option<String>) -> Self {
|
||||
match exec_server_url.as_deref().map(str::trim) {
|
||||
None | Some("") => Self::Local,
|
||||
Some(url) if url.eq_ignore_ascii_case("none") => Self::Disabled,
|
||||
Some(url) => Self::Remote {
|
||||
exec_server_url: url.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides access to the exec backend for a selected environment.
|
||||
///
|
||||
/// Implementations are expected to return the backend that matches the current
|
||||
/// environment mode, including disabled backends that reject execution.
|
||||
pub trait ExecutorEnvironment: Send + Sync {
|
||||
fn get_exec_backend(&self) -> Arc<dyn ExecBackend>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// Lazily creates and caches the active environment for a session.
|
||||
///
|
||||
/// The manager keeps the session's environment mode stable so subagents and
|
||||
/// follow-up turns preserve explicit `Disabled` semantics.
|
||||
#[derive(Debug)]
|
||||
pub struct EnvironmentManager {
|
||||
exec_server_url: Option<String>,
|
||||
mode: EnvironmentMode,
|
||||
current_environment: OnceCell<Arc<Environment>>,
|
||||
}
|
||||
|
||||
impl Default for EnvironmentManager {
|
||||
fn default() -> Self {
|
||||
Self::new(/*exec_server_url*/ None)
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentManager {
|
||||
/// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value.
|
||||
pub fn new(exec_server_url: Option<String>) -> Self {
|
||||
Self::from_mode(EnvironmentMode::from_exec_server_url(exec_server_url))
|
||||
}
|
||||
|
||||
/// Builds a manager from an already-parsed environment mode.
|
||||
pub fn from_mode(mode: EnvironmentMode) -> Self {
|
||||
Self {
|
||||
exec_server_url: normalize_exec_server_url(exec_server_url),
|
||||
mode,
|
||||
current_environment: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a manager from process environment variables.
|
||||
pub fn from_env() -> Self {
|
||||
Self::new(std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok())
|
||||
}
|
||||
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
self.exec_server_url.as_deref()
|
||||
/// Returns the stable mode for this manager.
|
||||
pub fn mode(&self) -> &EnvironmentMode {
|
||||
&self.mode
|
||||
}
|
||||
|
||||
/// Returns the remote exec-server URL when one is configured.
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
self.mode.exec_server_url()
|
||||
}
|
||||
|
||||
/// Returns the cached environment, creating it on first access.
|
||||
pub async fn current(&self) -> Result<Arc<Environment>, ExecServerError> {
|
||||
self.current_environment
|
||||
.get_or_try_init(|| async {
|
||||
Ok(Arc::new(
|
||||
Environment::create(self.exec_server_url.clone()).await?,
|
||||
Environment::create_for_mode(self.mode.clone()).await?,
|
||||
))
|
||||
})
|
||||
.await
|
||||
@@ -52,9 +177,13 @@ impl EnvironmentManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Concrete execution/filesystem environment selected for a session.
|
||||
///
|
||||
/// This bundles the chosen mode together with the corresponding exec backend
|
||||
/// and remote client, if any.
|
||||
#[derive(Clone)]
|
||||
pub struct Environment {
|
||||
exec_server_url: Option<String>,
|
||||
mode: EnvironmentMode,
|
||||
remote_exec_server_client: Option<ExecServerClient>,
|
||||
exec_backend: Arc<dyn ExecBackend>,
|
||||
}
|
||||
@@ -70,7 +199,7 @@ impl Default for Environment {
|
||||
}
|
||||
|
||||
Self {
|
||||
exec_server_url: None,
|
||||
mode: EnvironmentMode::Local,
|
||||
remote_exec_server_client: None,
|
||||
exec_backend: Arc::new(local_process),
|
||||
}
|
||||
@@ -80,18 +209,23 @@ impl Default for Environment {
|
||||
impl std::fmt::Debug for Environment {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Environment")
|
||||
.field("exec_server_url", &self.exec_server_url)
|
||||
.field("mode", &self.mode)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
/// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value.
|
||||
pub async fn create(exec_server_url: Option<String>) -> Result<Self, ExecServerError> {
|
||||
let exec_server_url = normalize_exec_server_url(exec_server_url);
|
||||
let remote_exec_server_client = if let Some(url) = &exec_server_url {
|
||||
Self::create_for_mode(EnvironmentMode::from_exec_server_url(exec_server_url)).await
|
||||
}
|
||||
|
||||
/// Builds an environment for an explicit mode.
|
||||
pub async fn create_for_mode(mode: EnvironmentMode) -> Result<Self, ExecServerError> {
|
||||
let remote_exec_server_client = if let EnvironmentMode::Remote { exec_server_url } = &mode {
|
||||
Some(
|
||||
ExecServerClient::connect_websocket(RemoteExecServerConnectArgs {
|
||||
websocket_url: url.clone(),
|
||||
websocket_url: exec_server_url.clone(),
|
||||
client_name: "codex-environment".to_string(),
|
||||
connect_timeout: std::time::Duration::from_secs(5),
|
||||
initialize_timeout: std::time::Duration::from_secs(5),
|
||||
@@ -102,10 +236,13 @@ impl Environment {
|
||||
None
|
||||
};
|
||||
|
||||
let exec_backend: Arc<dyn ExecBackend> =
|
||||
if let Some(client) = remote_exec_server_client.clone() {
|
||||
Arc::new(RemoteProcess::new(client))
|
||||
} else {
|
||||
let exec_backend: Arc<dyn ExecBackend> = match &mode {
|
||||
EnvironmentMode::Remote { .. } => Arc::new(RemoteProcess::new(
|
||||
remote_exec_server_client
|
||||
.clone()
|
||||
.expect("remote mode should have an exec-server client"),
|
||||
)),
|
||||
EnvironmentMode::Local => {
|
||||
let local_process = LocalProcess::default();
|
||||
local_process
|
||||
.initialize()
|
||||
@@ -114,17 +251,40 @@ impl Environment {
|
||||
.initialized()
|
||||
.map_err(ExecServerError::Protocol)?;
|
||||
Arc::new(local_process)
|
||||
};
|
||||
}
|
||||
EnvironmentMode::Disabled => Arc::new(DisabledExecBackend),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
exec_server_url,
|
||||
mode,
|
||||
remote_exec_server_client,
|
||||
exec_backend,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the selected mode for this environment.
|
||||
pub fn mode(&self) -> &EnvironmentMode {
|
||||
&self.mode
|
||||
}
|
||||
|
||||
/// Returns the capabilities exposed by this environment.
|
||||
pub fn capabilities(&self) -> EnvironmentCapabilities {
|
||||
self.mode.capabilities()
|
||||
}
|
||||
|
||||
/// Returns whether process execution is available.
|
||||
pub fn exec_enabled(&self) -> bool {
|
||||
self.capabilities().exec_enabled()
|
||||
}
|
||||
|
||||
/// Returns whether filesystem-backed operations are available.
|
||||
pub fn filesystem_enabled(&self) -> bool {
|
||||
self.capabilities().filesystem_enabled()
|
||||
}
|
||||
|
||||
/// Returns the remote exec-server URL when this environment is remote.
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
self.exec_server_url.as_deref()
|
||||
self.mode.exec_server_url()
|
||||
}
|
||||
|
||||
pub fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
|
||||
@@ -132,19 +292,82 @@ impl Environment {
|
||||
}
|
||||
|
||||
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)
|
||||
match &self.mode {
|
||||
EnvironmentMode::Remote { .. } => Arc::new(RemoteFileSystem::new(
|
||||
self.remote_exec_server_client
|
||||
.clone()
|
||||
.expect("remote mode should have an exec-server client"),
|
||||
)),
|
||||
EnvironmentMode::Local => Arc::new(LocalFileSystem),
|
||||
EnvironmentMode::Disabled => Arc::new(DisabledFileSystem),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_exec_server_url(exec_server_url: Option<String>) -> Option<String> {
|
||||
exec_server_url.and_then(|url| {
|
||||
let url = url.trim();
|
||||
(!url.is_empty()).then(|| url.to_string())
|
||||
})
|
||||
#[derive(Debug)]
|
||||
struct DisabledExecBackend;
|
||||
|
||||
#[async_trait]
|
||||
impl ExecBackend for DisabledExecBackend {
|
||||
async fn start(&self, params: ExecParams) -> Result<StartedExecProcess, ExecServerError> {
|
||||
Err(ExecServerError::Protocol(format!(
|
||||
"environment is disabled; cannot start process `{}`",
|
||||
params.process_id
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DisabledFileSystem;
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for DisabledFileSystem {
|
||||
async fn read_file(&self, path: &AbsolutePathBuf) -> io::Result<Vec<u8>> {
|
||||
Err(disabled_filesystem_error(path))
|
||||
}
|
||||
|
||||
async fn write_file(&self, path: &AbsolutePathBuf, _contents: Vec<u8>) -> io::Result<()> {
|
||||
Err(disabled_filesystem_error(path))
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
_options: CreateDirectoryOptions,
|
||||
) -> io::Result<()> {
|
||||
Err(disabled_filesystem_error(path))
|
||||
}
|
||||
|
||||
async fn get_metadata(&self, path: &AbsolutePathBuf) -> io::Result<FileMetadata> {
|
||||
Err(disabled_filesystem_error(path))
|
||||
}
|
||||
|
||||
async fn read_directory(&self, path: &AbsolutePathBuf) -> io::Result<Vec<ReadDirectoryEntry>> {
|
||||
Err(disabled_filesystem_error(path))
|
||||
}
|
||||
|
||||
async fn remove(&self, path: &AbsolutePathBuf, _options: RemoveOptions) -> io::Result<()> {
|
||||
Err(disabled_filesystem_error(path))
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
_destination_path: &AbsolutePathBuf,
|
||||
_options: CopyOptions,
|
||||
) -> io::Result<()> {
|
||||
Err(disabled_filesystem_error(source_path))
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled_filesystem_error(path: &AbsolutePathBuf) -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
format!(
|
||||
"environment is disabled; filesystem access is unavailable for `{}`",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
impl ExecutorEnvironment for Environment {
|
||||
@@ -155,16 +378,18 @@ impl ExecutorEnvironment for Environment {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::Environment;
|
||||
use super::EnvironmentManager;
|
||||
use super::EnvironmentMode;
|
||||
use crate::ProcessId;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_without_remote_exec_server_url_does_not_connect() {
|
||||
let environment = Environment::create(/*exec_server_url*/ None)
|
||||
async fn create_local_environment_does_not_connect() {
|
||||
let environment = Environment::create_for_mode(EnvironmentMode::Local)
|
||||
.await
|
||||
.expect("create environment");
|
||||
|
||||
@@ -176,9 +401,26 @@ mod tests {
|
||||
fn environment_manager_normalizes_empty_url() {
|
||||
let manager = EnvironmentManager::new(Some(String::new()));
|
||||
|
||||
assert_eq!(manager.mode(), &EnvironmentMode::Local);
|
||||
assert_eq!(manager.exec_server_url(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_manager_treats_none_value_as_disabled() {
|
||||
let manager = EnvironmentManager::new(Some("none".to_string()));
|
||||
|
||||
assert_eq!(manager.mode(), &EnvironmentMode::Disabled);
|
||||
assert_eq!(manager.exec_server_url(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_mode_capabilities_are_off() {
|
||||
let capabilities = EnvironmentMode::Disabled.capabilities();
|
||||
|
||||
assert!(!capabilities.exec_enabled());
|
||||
assert!(!capabilities.filesystem_enabled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_current_caches_environment() {
|
||||
let manager = EnvironmentManager::new(/*exec_server_url*/ None);
|
||||
@@ -208,4 +450,41 @@ mod tests {
|
||||
|
||||
assert_eq!(response.process.process_id().as_str(), "default-env-proc");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_environment_rejects_exec_and_filesystem_access() {
|
||||
let environment = Environment::create_for_mode(EnvironmentMode::Disabled)
|
||||
.await
|
||||
.expect("create disabled environment");
|
||||
|
||||
let exec_error = match environment
|
||||
.get_exec_backend()
|
||||
.start(crate::ExecParams {
|
||||
process_id: ProcessId::from("disabled-proc"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: std::env::current_dir().expect("read current dir"),
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => panic!("disabled environment should reject exec"),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert_eq!(
|
||||
exec_error.to_string(),
|
||||
"exec-server protocol error: environment is disabled; cannot start process `disabled-proc`"
|
||||
);
|
||||
|
||||
let path =
|
||||
codex_utils_absolute_path::AbsolutePathBuf::try_from(std::env::temp_dir().as_path())
|
||||
.expect("temp dir");
|
||||
let fs_error = environment
|
||||
.get_filesystem()
|
||||
.get_metadata(&path)
|
||||
.await
|
||||
.expect_err("disabled environment should reject filesystem access");
|
||||
assert_eq!(fs_error.kind(), io::ErrorKind::Unsupported);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ pub use codex_app_server_protocol::FsWriteFileParams;
|
||||
pub use codex_app_server_protocol::FsWriteFileResponse;
|
||||
pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
pub use environment::Environment;
|
||||
pub use environment::EnvironmentCapabilities;
|
||||
pub use environment::EnvironmentManager;
|
||||
pub use environment::EnvironmentMode;
|
||||
pub use environment::ExecutorEnvironment;
|
||||
pub use file_system::CopyOptions;
|
||||
pub use file_system::CreateDirectoryOptions;
|
||||
|
||||
@@ -86,6 +86,7 @@ pub use responses_api::mcp_tool_to_deferred_responses_api_tool;
|
||||
pub use responses_api::mcp_tool_to_responses_api_tool;
|
||||
pub use responses_api::tool_definition_to_responses_api_tool;
|
||||
pub use tool_config::ShellCommandBackendConfig;
|
||||
pub use tool_config::ToolEnvironmentCapabilities;
|
||||
pub use tool_config::ToolUserShellType;
|
||||
pub use tool_config::ToolsConfig;
|
||||
pub use tool_config::ToolsConfigParams;
|
||||
|
||||
@@ -43,6 +43,37 @@ pub struct ZshForkConfig {
|
||||
pub main_execve_wrapper_exe: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
/// Tool-layer capability snapshot derived from the active environment.
|
||||
///
|
||||
/// This mirrors the environment crate's capability model so tool registration
|
||||
/// can suppress environment-backed tools without knowing how the environment
|
||||
/// was selected.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct ToolEnvironmentCapabilities {
|
||||
exec_enabled: bool,
|
||||
filesystem_enabled: bool,
|
||||
}
|
||||
|
||||
impl ToolEnvironmentCapabilities {
|
||||
/// Creates the capability set that tool planning should use.
|
||||
pub fn new(exec_enabled: bool, filesystem_enabled: bool) -> Self {
|
||||
Self {
|
||||
exec_enabled,
|
||||
filesystem_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether execution tools should be registered.
|
||||
pub fn exec_enabled(self) -> bool {
|
||||
self.exec_enabled
|
||||
}
|
||||
|
||||
/// Returns whether filesystem-backed tools should be registered.
|
||||
pub fn filesystem_enabled(self) -> bool {
|
||||
self.filesystem_enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl UnifiedExecShellMode {
|
||||
pub fn for_session(
|
||||
shell_command_backend: ShellCommandBackendConfig,
|
||||
@@ -86,6 +117,7 @@ pub struct ToolsConfig {
|
||||
pub shell_type: ConfigShellToolType,
|
||||
pub shell_command_backend: ShellCommandBackendConfig,
|
||||
pub unified_exec_shell_mode: UnifiedExecShellMode,
|
||||
pub environment_capabilities: ToolEnvironmentCapabilities,
|
||||
pub allow_login_shell: bool,
|
||||
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
||||
pub web_search_mode: Option<WebSearchMode>,
|
||||
@@ -200,6 +232,9 @@ impl ToolsConfig {
|
||||
shell_type,
|
||||
shell_command_backend,
|
||||
unified_exec_shell_mode: UnifiedExecShellMode::Direct,
|
||||
environment_capabilities: ToolEnvironmentCapabilities::new(
|
||||
/*exec_enabled*/ true, /*filesystem_enabled*/ true,
|
||||
),
|
||||
allow_login_shell: true,
|
||||
apply_patch_tool_type,
|
||||
web_search_mode: *web_search_mode,
|
||||
@@ -236,6 +271,14 @@ impl ToolsConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_environment_capabilities(
|
||||
mut self,
|
||||
environment_capabilities: ToolEnvironmentCapabilities,
|
||||
) -> Self {
|
||||
self.environment_capabilities = environment_capabilities;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_unified_exec_shell_mode(
|
||||
mut self,
|
||||
unified_exec_shell_mode: UnifiedExecShellMode,
|
||||
|
||||
@@ -107,54 +107,58 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
}
|
||||
|
||||
match &config.shell_type {
|
||||
ConfigShellToolType::Default => {
|
||||
plan.push_spec(
|
||||
create_shell_tool(ShellToolOptions {
|
||||
exec_permission_approvals_enabled,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
}
|
||||
ConfigShellToolType::Local => {
|
||||
plan.push_spec(
|
||||
create_local_shell_tool(),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
}
|
||||
ConfigShellToolType::UnifiedExec => {
|
||||
plan.push_spec(
|
||||
create_exec_command_tool(CommandToolOptions {
|
||||
allow_login_shell: config.allow_login_shell,
|
||||
exec_permission_approvals_enabled,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.push_spec(
|
||||
create_write_stdin_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler("exec_command", ToolHandlerKind::UnifiedExec);
|
||||
plan.register_handler("write_stdin", ToolHandlerKind::UnifiedExec);
|
||||
}
|
||||
ConfigShellToolType::Disabled => {}
|
||||
ConfigShellToolType::ShellCommand => {
|
||||
plan.push_spec(
|
||||
create_shell_command_tool(CommandToolOptions {
|
||||
allow_login_shell: config.allow_login_shell,
|
||||
exec_permission_approvals_enabled,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
if config.environment_capabilities.exec_enabled() {
|
||||
match &config.shell_type {
|
||||
ConfigShellToolType::Default => {
|
||||
plan.push_spec(
|
||||
create_shell_tool(ShellToolOptions {
|
||||
exec_permission_approvals_enabled,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
}
|
||||
ConfigShellToolType::Local => {
|
||||
plan.push_spec(
|
||||
create_local_shell_tool(),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
}
|
||||
ConfigShellToolType::UnifiedExec => {
|
||||
plan.push_spec(
|
||||
create_exec_command_tool(CommandToolOptions {
|
||||
allow_login_shell: config.allow_login_shell,
|
||||
exec_permission_approvals_enabled,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.push_spec(
|
||||
create_write_stdin_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler("exec_command", ToolHandlerKind::UnifiedExec);
|
||||
plan.register_handler("write_stdin", ToolHandlerKind::UnifiedExec);
|
||||
}
|
||||
ConfigShellToolType::Disabled => {}
|
||||
ConfigShellToolType::ShellCommand => {
|
||||
plan.push_spec(
|
||||
create_shell_command_tool(CommandToolOptions {
|
||||
allow_login_shell: config.allow_login_shell,
|
||||
exec_permission_approvals_enabled,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.shell_type != ConfigShellToolType::Disabled {
|
||||
if config.environment_capabilities.exec_enabled()
|
||||
&& config.shell_type != ConfigShellToolType::Disabled
|
||||
{
|
||||
plan.register_handler("shell", ToolHandlerKind::Shell);
|
||||
plan.register_handler("container.exec", ToolHandlerKind::Shell);
|
||||
plan.register_handler("local_shell", ToolHandlerKind::Shell);
|
||||
@@ -189,7 +193,7 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
plan.register_handler("update_plan", ToolHandlerKind::Plan);
|
||||
|
||||
if config.js_repl_enabled {
|
||||
if config.environment_capabilities.exec_enabled() && config.js_repl_enabled {
|
||||
plan.push_spec(
|
||||
create_js_repl_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
@@ -265,7 +269,9 @@ pub fn build_tool_registry_plan(
|
||||
plan.register_handler(TOOL_SUGGEST_TOOL_NAME, ToolHandlerKind::ToolSuggest);
|
||||
}
|
||||
|
||||
if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type {
|
||||
if config.environment_capabilities.filesystem_enabled()
|
||||
&& let Some(apply_patch_tool_type) = &config.apply_patch_tool_type
|
||||
{
|
||||
match apply_patch_tool_type {
|
||||
ApplyPatchToolType::Freeform => {
|
||||
plan.push_spec(
|
||||
@@ -285,10 +291,11 @@ pub fn build_tool_registry_plan(
|
||||
plan.register_handler("apply_patch", ToolHandlerKind::ApplyPatch);
|
||||
}
|
||||
|
||||
if config
|
||||
.experimental_supported_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "list_dir")
|
||||
if config.environment_capabilities.filesystem_enabled()
|
||||
&& config
|
||||
.experimental_supported_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "list_dir")
|
||||
{
|
||||
plan.push_spec(
|
||||
create_list_dir_tool(),
|
||||
@@ -331,14 +338,16 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
}
|
||||
|
||||
plan.push_spec(
|
||||
create_view_image_tool(ViewImageToolOptions {
|
||||
can_request_original_image_detail: config.can_request_original_image_detail,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler("view_image", ToolHandlerKind::ViewImage);
|
||||
if config.environment_capabilities.filesystem_enabled() {
|
||||
plan.push_spec(
|
||||
create_view_image_tool(ViewImageToolOptions {
|
||||
can_request_original_image_detail: config.can_request_original_image_detail,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler("view_image", ToolHandlerKind::ViewImage);
|
||||
}
|
||||
|
||||
if config.collab_tools {
|
||||
if config.multi_agent_v2 {
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::JsonSchema;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ResponsesApiWebSearchFilters;
|
||||
use crate::ResponsesApiWebSearchUserLocation;
|
||||
use crate::ToolEnvironmentCapabilities;
|
||||
use crate::ToolHandlerSpec;
|
||||
use crate::ToolRegistryPlanAppTool;
|
||||
use crate::ToolsConfigParams;
|
||||
@@ -453,6 +454,114 @@ fn view_image_tool_includes_detail_with_original_detail_feature() {
|
||||
assert!(description.contains("omit this field for default resized behavior"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_environment_omits_environment_backed_tools() {
|
||||
let model_info = model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
features.enable(Feature::JsRepl);
|
||||
let available_models = Vec::new();
|
||||
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
})
|
||||
.with_environment_capabilities(ToolEnvironmentCapabilities::new(
|
||||
/*exec_enabled*/ false, /*filesystem_enabled*/ false,
|
||||
));
|
||||
tools_config
|
||||
.experimental_supported_tools
|
||||
.push("list_dir".to_string());
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*app_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_lacks_tool_name(&tools, "exec_command");
|
||||
assert_lacks_tool_name(&tools, "write_stdin");
|
||||
assert_lacks_tool_name(&tools, "js_repl");
|
||||
assert_lacks_tool_name(&tools, "js_repl_reset");
|
||||
assert_lacks_tool_name(&tools, "apply_patch");
|
||||
assert_lacks_tool_name(&tools, "list_dir");
|
||||
assert_lacks_tool_name(&tools, VIEW_IMAGE_TOOL_NAME);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_capabilities_gate_exec_and_filesystem_tools_independently() {
|
||||
let model_info = model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
features.enable(Feature::JsRepl);
|
||||
let available_models = Vec::new();
|
||||
|
||||
let mut exec_disabled = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
})
|
||||
.with_environment_capabilities(ToolEnvironmentCapabilities::new(
|
||||
/*exec_enabled*/ false, /*filesystem_enabled*/ true,
|
||||
));
|
||||
exec_disabled
|
||||
.experimental_supported_tools
|
||||
.push("list_dir".to_string());
|
||||
let (exec_disabled_tools, _) = build_specs(
|
||||
&exec_disabled,
|
||||
/*mcp_tools*/ None,
|
||||
/*app_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_lacks_tool_name(&exec_disabled_tools, "exec_command");
|
||||
assert_lacks_tool_name(&exec_disabled_tools, "write_stdin");
|
||||
assert_lacks_tool_name(&exec_disabled_tools, "js_repl");
|
||||
assert_lacks_tool_name(&exec_disabled_tools, "js_repl_reset");
|
||||
assert_contains_tool_names(
|
||||
&exec_disabled_tools,
|
||||
&["apply_patch", "list_dir", VIEW_IMAGE_TOOL_NAME],
|
||||
);
|
||||
|
||||
let mut filesystem_disabled = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
})
|
||||
.with_environment_capabilities(ToolEnvironmentCapabilities::new(
|
||||
/*exec_enabled*/ true, /*filesystem_enabled*/ false,
|
||||
));
|
||||
filesystem_disabled
|
||||
.experimental_supported_tools
|
||||
.push("list_dir".to_string());
|
||||
let (filesystem_disabled_tools, _) = build_specs(
|
||||
&filesystem_disabled,
|
||||
/*mcp_tools*/ None,
|
||||
/*app_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_contains_tool_names(
|
||||
&filesystem_disabled_tools,
|
||||
&["exec_command", "write_stdin", "js_repl", "js_repl_reset"],
|
||||
);
|
||||
assert_lacks_tool_name(&filesystem_disabled_tools, "apply_patch");
|
||||
assert_lacks_tool_name(&filesystem_disabled_tools, "list_dir");
|
||||
assert_lacks_tool_name(&filesystem_disabled_tools, VIEW_IMAGE_TOOL_NAME);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_agent_job_worker_tools_enabled() {
|
||||
let model_info = model_info();
|
||||
|
||||
Reference in New Issue
Block a user