mirror of
https://github.com/openai/codex.git
synced 2026-05-01 09:56:37 +00:00
Compare commits
5 Commits
codex-fix/
...
starr/ca-4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdd3a98444 | ||
|
|
8f8a7c6285 | ||
|
|
f2a9147108 | ||
|
|
c40119efb1 | ||
|
|
66ded7674c |
@@ -1411,6 +1411,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,
|
||||
));
|
||||
@@ -5537,6 +5541,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,
|
||||
));
|
||||
|
||||
@@ -2063,7 +2063,12 @@ impl Config {
|
||||
network: network_requirements,
|
||||
} = config_layer_stack.requirements().clone();
|
||||
|
||||
let user_instructions = Self::load_instructions(Some(&codex_home));
|
||||
let user_instructions =
|
||||
if codex_exec_server::ExecServerMode::from_env().skips_project_docs() {
|
||||
None
|
||||
} else {
|
||||
Self::load_instructions(Some(&codex_home))
|
||||
};
|
||||
let mut startup_warnings = Vec::new();
|
||||
|
||||
// Destructure ConfigOverrides fully to ensure all overrides are applied.
|
||||
|
||||
@@ -77,6 +77,7 @@ fn render_js_repl_instructions(config: &Config) -> Option<String> {
|
||||
/// Combines `Config::instructions` and `AGENTS.md` (if present) into a single
|
||||
/// string of instructions.
|
||||
pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
|
||||
let skip_project_docs = codex_exec_server::ExecServerMode::from_env().skips_project_docs();
|
||||
let project_docs = read_project_docs(config).await;
|
||||
|
||||
let mut output = String::new();
|
||||
@@ -105,7 +106,7 @@ pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
|
||||
output.push_str(&js_repl_section);
|
||||
}
|
||||
|
||||
if config.features.enabled(Feature::ChildAgentsMd) {
|
||||
if config.features.enabled(Feature::ChildAgentsMd) && !skip_project_docs {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
@@ -128,7 +129,7 @@ pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
|
||||
pub async fn read_project_docs(config: &Config) -> std::io::Result<Option<String>> {
|
||||
let max_total = config.project_doc_max_bytes;
|
||||
|
||||
if max_total == 0 {
|
||||
if max_total == 0 || codex_exec_server::ExecServerMode::from_env().skips_project_docs() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ use core_test_support::PathBufExt;
|
||||
use core_test_support::TempDirExt;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
use tempfile::TempDir;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
|
||||
/// Helper that returns a `Config` pointing at `root` and using `limit` as
|
||||
/// the maximum number of bytes to embed from AGENTS.md. The caller can
|
||||
@@ -70,6 +74,32 @@ async fn make_config_with_project_root_markers(
|
||||
config
|
||||
}
|
||||
|
||||
fn exec_server_env_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
async fn with_exec_server_url_env<T>(
|
||||
exec_server_url: Option<&str>,
|
||||
future: impl std::future::Future<Output = T>,
|
||||
) -> T {
|
||||
let _guard = exec_server_env_lock().lock().await;
|
||||
let previous = std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok();
|
||||
match exec_server_url {
|
||||
Some(value) => unsafe { std::env::set_var(CODEX_EXEC_SERVER_URL_ENV_VAR, value) },
|
||||
None => unsafe { std::env::remove_var(CODEX_EXEC_SERVER_URL_ENV_VAR) },
|
||||
}
|
||||
|
||||
let result = future.await;
|
||||
|
||||
match previous {
|
||||
Some(value) => unsafe { std::env::set_var(CODEX_EXEC_SERVER_URL_ENV_VAR, value) },
|
||||
None => unsafe { std::env::remove_var(CODEX_EXEC_SERVER_URL_ENV_VAR) },
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// AGENTS.md missing – should yield `None`.
|
||||
#[tokio::test]
|
||||
async fn no_doc_file_returns_none() {
|
||||
@@ -161,6 +191,21 @@ async fn zero_byte_limit_disables_docs() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn disabled_exec_server_mode_skips_project_docs() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "something").unwrap();
|
||||
|
||||
let mut cfg = make_config(&tmp, /*limit*/ 4096, Some("base instructions")).await;
|
||||
cfg.features
|
||||
.enable(Feature::ChildAgentsMd)
|
||||
.expect("test config should allow hierarchical AGENTS instructions");
|
||||
|
||||
let res = with_exec_server_url_env(Some("none"), get_user_instructions(&cfg)).await;
|
||||
|
||||
assert_eq!(res, Some("base instructions".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn js_repl_instructions_are_appended_when_enabled() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -35,6 +35,9 @@ use std::sync::Arc;
|
||||
|
||||
pub struct ApplyPatchHandler;
|
||||
|
||||
const APPLY_PATCH_DISABLED_MESSAGE: &str =
|
||||
"apply_patch is unavailable because the environment is disabled";
|
||||
|
||||
fn file_paths_for_action(action: &ApplyPatchAction) -> Vec<AbsolutePathBuf> {
|
||||
let mut keys = Vec::new();
|
||||
let cwd = action.cwd.as_path();
|
||||
@@ -150,6 +153,12 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
..
|
||||
} = invocation;
|
||||
|
||||
if !turn.environment.exec_enabled() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
APPLY_PATCH_DISABLED_MESSAGE.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let patch_input = match payload {
|
||||
ToolPayload::Function { arguments } => {
|
||||
let args: ApplyPatchToolArgs = parse_arguments(&arguments)?;
|
||||
|
||||
@@ -25,6 +25,8 @@ use codex_protocol::protocol::ExecCommandSource;
|
||||
pub struct JsReplHandler;
|
||||
pub struct JsReplResetHandler;
|
||||
|
||||
const JS_REPL_DISABLED_MESSAGE: &str = "js_repl is unavailable because the environment is disabled";
|
||||
|
||||
fn join_outputs(stdout: &str, stderr: &str) -> String {
|
||||
if stdout.is_empty() {
|
||||
stderr.to_string()
|
||||
@@ -115,6 +117,12 @@ impl ToolHandler for JsReplHandler {
|
||||
..
|
||||
} = invocation;
|
||||
|
||||
if !turn.environment.exec_enabled() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
JS_REPL_DISABLED_MESSAGE.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !session.features().enabled(Feature::JsRepl) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"js_repl is disabled by feature flag".to_string(),
|
||||
@@ -188,6 +196,11 @@ impl ToolHandler for JsReplResetHandler {
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
if !invocation.turn.environment.exec_enabled() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
JS_REPL_DISABLED_MESSAGE.to_string(),
|
||||
));
|
||||
}
|
||||
if !invocation.session.features().enabled(Feature::JsRepl) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"js_repl is disabled by feature flag".to_string(),
|
||||
|
||||
@@ -18,6 +18,9 @@ use crate::tools::registry::ToolKind;
|
||||
|
||||
pub struct ListDirHandler;
|
||||
|
||||
const LIST_DIR_DISABLED_MESSAGE: &str =
|
||||
"list_dir is unavailable because the environment is disabled";
|
||||
|
||||
const MAX_ENTRY_LENGTH: usize = 500;
|
||||
const INDENTATION_SPACES: usize = 2;
|
||||
|
||||
@@ -52,6 +55,12 @@ impl ToolHandler for ListDirHandler {
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
if !invocation.turn.environment.filesystem_enabled() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
LIST_DIR_DISABLED_MESSAGE.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let ToolInvocation { payload, .. } = invocation;
|
||||
|
||||
let arguments = match payload {
|
||||
|
||||
@@ -42,6 +42,8 @@ use codex_tools::ShellCommandBackendConfig;
|
||||
|
||||
pub struct ShellHandler;
|
||||
|
||||
const SHELL_DISABLED_MESSAGE: &str = "shell is unavailable because the environment is disabled";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ShellCommandBackend {
|
||||
Classic,
|
||||
@@ -394,6 +396,12 @@ impl ShellHandler {
|
||||
shell_runtime_backend,
|
||||
} = args;
|
||||
|
||||
if !turn.environment.exec_enabled() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
SHELL_DISABLED_MESSAGE.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut exec_params = exec_params;
|
||||
let dependency_env = session.dependency_env().await;
|
||||
if !dependency_env.is_empty() {
|
||||
|
||||
@@ -36,6 +36,9 @@ use std::sync::Arc;
|
||||
|
||||
pub struct UnifiedExecHandler;
|
||||
|
||||
const UNIFIED_EXEC_DISABLED_MESSAGE: &str =
|
||||
"exec_command is unavailable because the environment is disabled";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct ExecCommandArgs {
|
||||
cmd: String,
|
||||
@@ -179,6 +182,12 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
let manager: &UnifiedExecProcessManager = &session.services.unified_exec_manager;
|
||||
let context = UnifiedExecContext::new(session.clone(), turn.clone(), call_id.clone());
|
||||
|
||||
if !turn.environment.exec_enabled() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
UNIFIED_EXEC_DISABLED_MESSAGE.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let response = match tool_name.as_str() {
|
||||
"exec_command" => {
|
||||
let cwd = resolve_workdir_base_path(&arguments, context.turn.cwd.as_path())?;
|
||||
|
||||
@@ -24,6 +24,8 @@ pub struct ViewImageHandler;
|
||||
|
||||
const VIEW_IMAGE_UNSUPPORTED_MESSAGE: &str =
|
||||
"view_image is not allowed because you do not support image inputs";
|
||||
const VIEW_IMAGE_DISABLED_MESSAGE: &str =
|
||||
"view_image is unavailable because the environment is disabled";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ViewImageArgs {
|
||||
@@ -44,6 +46,12 @@ impl ToolHandler for ViewImageHandler {
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
if !invocation.turn.environment.filesystem_enabled() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
VIEW_IMAGE_DISABLED_MESSAGE.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !invocation
|
||||
.turn
|
||||
.model_info
|
||||
|
||||
@@ -227,7 +227,10 @@ $lines | Select-Object -Skip 1 | Set-Content -Path tokens.txt
|
||||
ModelProviderAuthInfo {
|
||||
command: self.command.clone(),
|
||||
args: self.args.clone(),
|
||||
timeout_ms: non_zero_u64(/*value*/ 1_000),
|
||||
// Process startup can be slow on loaded Windows CI workers, so keep this aligned with
|
||||
// `codex-login` auth tests and avoid turning provider-auth assertions into a
|
||||
// process-launch timing test.
|
||||
timeout_ms: non_zero_u64(/*value*/ 10_000),
|
||||
refresh_interval_ms: 60_000,
|
||||
cwd: match codex_utils_absolute_path::AbsolutePathBuf::try_from(self.tempdir.path()) {
|
||||
Ok(cwd) => cwd,
|
||||
|
||||
@@ -14,37 +14,95 @@ use crate::remote_process::RemoteProcess;
|
||||
|
||||
pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
|
||||
|
||||
const CODEX_EXEC_SERVER_URL_NONE: &str = "none";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum ExecServerMode {
|
||||
#[default]
|
||||
Local,
|
||||
Disabled,
|
||||
Remote(String),
|
||||
}
|
||||
|
||||
impl ExecServerMode {
|
||||
pub fn from_env() -> Self {
|
||||
Self::from_env_value(std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok())
|
||||
}
|
||||
|
||||
pub fn from_env_value(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(CODEX_EXEC_SERVER_URL_NONE) => Self::Disabled,
|
||||
Some(url) => Self::Remote(url.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Local | Self::Disabled => None,
|
||||
Self::Remote(url) => Some(url.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn skips_project_docs(&self) -> bool {
|
||||
matches!(self, Self::Disabled)
|
||||
}
|
||||
|
||||
pub fn exec_enabled(&self) -> bool {
|
||||
!matches!(self, Self::Disabled)
|
||||
}
|
||||
|
||||
pub fn filesystem_enabled(&self) -> bool {
|
||||
!matches!(self, Self::Disabled)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExecutorEnvironment: Send + Sync {
|
||||
fn get_exec_backend(&self) -> Arc<dyn ExecBackend>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EnvironmentManager {
|
||||
exec_server_url: Option<String>,
|
||||
exec_server_mode: ExecServerMode,
|
||||
current_environment: OnceCell<Arc<Environment>>,
|
||||
}
|
||||
|
||||
impl EnvironmentManager {
|
||||
pub fn new(exec_server_url: Option<String>) -> Self {
|
||||
Self {
|
||||
exec_server_url: normalize_exec_server_url(exec_server_url),
|
||||
exec_server_mode: ExecServerMode::from_env_value(exec_server_url),
|
||||
current_environment: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env() -> Self {
|
||||
Self::new(std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok())
|
||||
Self {
|
||||
exec_server_mode: ExecServerMode::from_env(),
|
||||
current_environment: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
self.exec_server_url.as_deref()
|
||||
self.exec_server_mode.exec_server_url()
|
||||
}
|
||||
|
||||
pub fn skips_project_docs(&self) -> bool {
|
||||
self.exec_server_mode.skips_project_docs()
|
||||
}
|
||||
|
||||
pub fn exec_enabled(&self) -> bool {
|
||||
self.exec_server_mode.exec_enabled()
|
||||
}
|
||||
|
||||
pub fn filesystem_enabled(&self) -> bool {
|
||||
self.exec_server_mode.filesystem_enabled()
|
||||
}
|
||||
|
||||
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_from_mode(self.exec_server_mode.clone()).await?,
|
||||
))
|
||||
})
|
||||
.await
|
||||
@@ -54,7 +112,7 @@ impl EnvironmentManager {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Environment {
|
||||
exec_server_url: Option<String>,
|
||||
exec_server_mode: ExecServerMode,
|
||||
remote_exec_server_client: Option<ExecServerClient>,
|
||||
exec_backend: Arc<dyn ExecBackend>,
|
||||
}
|
||||
@@ -70,7 +128,7 @@ impl Default for Environment {
|
||||
}
|
||||
|
||||
Self {
|
||||
exec_server_url: None,
|
||||
exec_server_mode: ExecServerMode::Local,
|
||||
remote_exec_server_client: None,
|
||||
exec_backend: Arc::new(local_process),
|
||||
}
|
||||
@@ -80,18 +138,21 @@ 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("exec_server_mode", &self.exec_server_mode)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
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_from_mode(ExecServerMode::from_env_value(exec_server_url)).await
|
||||
}
|
||||
|
||||
async fn create_from_mode(exec_server_mode: ExecServerMode) -> Result<Self, ExecServerError> {
|
||||
let remote_exec_server_client = if let Some(url) = exec_server_mode.exec_server_url() {
|
||||
Some(
|
||||
ExecServerClient::connect_websocket(RemoteExecServerConnectArgs {
|
||||
websocket_url: url.clone(),
|
||||
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),
|
||||
@@ -117,14 +178,26 @@ impl Environment {
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
exec_server_url,
|
||||
exec_server_mode,
|
||||
remote_exec_server_client,
|
||||
exec_backend,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
self.exec_server_url.as_deref()
|
||||
self.exec_server_mode.exec_server_url()
|
||||
}
|
||||
|
||||
pub fn skips_project_docs(&self) -> bool {
|
||||
self.exec_server_mode.skips_project_docs()
|
||||
}
|
||||
|
||||
pub fn exec_enabled(&self) -> bool {
|
||||
self.exec_server_mode.exec_enabled()
|
||||
}
|
||||
|
||||
pub fn filesystem_enabled(&self) -> bool {
|
||||
self.exec_server_mode.filesystem_enabled()
|
||||
}
|
||||
|
||||
pub fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
|
||||
@@ -140,13 +213,6 @@ impl Environment {
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
})
|
||||
}
|
||||
|
||||
impl ExecutorEnvironment for Environment {
|
||||
fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
|
||||
Arc::clone(&self.exec_backend)
|
||||
@@ -159,6 +225,7 @@ mod tests {
|
||||
|
||||
use super::Environment;
|
||||
use super::EnvironmentManager;
|
||||
use super::ExecServerMode;
|
||||
use crate::ProcessId;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -177,6 +244,25 @@ mod tests {
|
||||
let manager = EnvironmentManager::new(Some(String::new()));
|
||||
|
||||
assert_eq!(manager.exec_server_url(), None);
|
||||
assert!(!manager.skips_project_docs());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_server_mode_from_env_value_parses_disabled_mode() {
|
||||
assert_eq!(
|
||||
ExecServerMode::from_env_value(Some("none".to_string())),
|
||||
ExecServerMode::Disabled
|
||||
);
|
||||
assert_eq!(
|
||||
ExecServerMode::from_env_value(Some(" NONE ".to_string())),
|
||||
ExecServerMode::Disabled
|
||||
);
|
||||
assert_eq!(
|
||||
ExecServerMode::from_env_value(Some("ws://localhost:1234".to_string())),
|
||||
ExecServerMode::Remote("ws://localhost:1234".to_string())
|
||||
);
|
||||
assert!(!ExecServerMode::Disabled.exec_enabled());
|
||||
assert!(!ExecServerMode::Disabled.filesystem_enabled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -34,6 +34,7 @@ pub use codex_app_server_protocol::FsWriteFileResponse;
|
||||
pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
pub use environment::Environment;
|
||||
pub use environment::EnvironmentManager;
|
||||
pub use environment::ExecServerMode;
|
||||
pub use environment::ExecutorEnvironment;
|
||||
pub use file_system::CopyOptions;
|
||||
pub use file_system::CreateDirectoryOptions;
|
||||
|
||||
@@ -4,4 +4,8 @@ codex_rust_crate(
|
||||
name = "exec",
|
||||
crate_name = "codex_exec",
|
||||
test_tags = ["no-sandbox"],
|
||||
unit_test_target_compatible_with = select({
|
||||
"@platforms//os:windows": ["@platforms//:incompatible"],
|
||||
"//conditions:default": [],
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,6 +31,21 @@ pub enum ToolUserShellType {
|
||||
Cmd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct ToolEnvironmentCapabilities {
|
||||
pub exec_enabled: bool,
|
||||
pub filesystem_enabled: bool,
|
||||
}
|
||||
|
||||
impl ToolEnvironmentCapabilities {
|
||||
pub const fn new(exec_enabled: bool, filesystem_enabled: bool) -> Self {
|
||||
Self {
|
||||
exec_enabled,
|
||||
filesystem_enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum UnifiedExecShellMode {
|
||||
Direct,
|
||||
@@ -109,6 +124,7 @@ pub struct ToolsConfig {
|
||||
pub agent_jobs_tools: bool,
|
||||
pub agent_jobs_worker_tools: bool,
|
||||
pub agent_type_description: String,
|
||||
pub environment_capabilities: ToolEnvironmentCapabilities,
|
||||
}
|
||||
|
||||
pub struct ToolsConfigParams<'a> {
|
||||
@@ -154,6 +170,9 @@ impl ToolsConfig {
|
||||
features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info);
|
||||
let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals);
|
||||
let request_permissions_tool_enabled = features.enabled(Feature::RequestPermissionsTool);
|
||||
let environment_capabilities = ToolEnvironmentCapabilities::new(
|
||||
/*exec_enabled*/ true, /*filesystem_enabled*/ true,
|
||||
);
|
||||
let shell_command_backend =
|
||||
if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) {
|
||||
ShellCommandBackendConfig::ZshFork
|
||||
@@ -165,7 +184,9 @@ impl ToolsConfig {
|
||||
sandbox_policy,
|
||||
*windows_sandbox_level,
|
||||
);
|
||||
let shell_type = if !features.enabled(Feature::ShellTool) {
|
||||
let shell_type = if !environment_capabilities.exec_enabled
|
||||
|| !features.enabled(Feature::ShellTool)
|
||||
{
|
||||
ConfigShellToolType::Disabled
|
||||
} else if features.enabled(Feature::ShellZshFork) {
|
||||
ConfigShellToolType::ShellCommand
|
||||
@@ -186,7 +207,8 @@ impl ToolsConfig {
|
||||
Some(ApplyPatchToolType::Freeform) => Some(ApplyPatchToolType::Freeform),
|
||||
Some(ApplyPatchToolType::Function) => Some(ApplyPatchToolType::Function),
|
||||
None => include_apply_patch_tool.then_some(ApplyPatchToolType::Freeform),
|
||||
};
|
||||
}
|
||||
.filter(|_| environment_capabilities.exec_enabled);
|
||||
|
||||
let agent_jobs_worker_tools = include_agent_jobs
|
||||
&& matches!(
|
||||
@@ -223,6 +245,7 @@ impl ToolsConfig {
|
||||
agent_jobs_tools: include_agent_jobs,
|
||||
agent_jobs_worker_tools,
|
||||
agent_type_description: String::new(),
|
||||
environment_capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +293,22 @@ impl ToolsConfig {
|
||||
nested.code_mode_only_enabled = false;
|
||||
nested
|
||||
}
|
||||
|
||||
pub fn with_environment_capabilities(
|
||||
mut self,
|
||||
environment_capabilities: ToolEnvironmentCapabilities,
|
||||
) -> Self {
|
||||
self.environment_capabilities = environment_capabilities;
|
||||
|
||||
if !environment_capabilities.exec_enabled {
|
||||
self.shell_type = ConfigShellToolType::Disabled;
|
||||
self.apply_patch_tool_type = None;
|
||||
self.js_repl_enabled = false;
|
||||
self.js_repl_tools_only = false;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_image_generation(model_info: &ModelInfo) -> bool {
|
||||
|
||||
@@ -3,6 +3,7 @@ use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::openai_models::ApplyPatchToolType;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
@@ -73,6 +74,53 @@ fn unified_exec_is_blocked_for_windows_sandboxed_policies_only() {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_environment_capabilities_turn_off_exec_backed_tools() {
|
||||
let model_info = model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::ApplyPatchFreeform);
|
||||
features.enable(Feature::JsRepl);
|
||||
|
||||
let available_models = Vec::new();
|
||||
let 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,
|
||||
));
|
||||
|
||||
assert_eq!(tools_config.shell_type, ConfigShellToolType::Disabled);
|
||||
assert_eq!(tools_config.apply_patch_tool_type, None);
|
||||
assert!(!tools_config.js_repl_enabled);
|
||||
assert!(!tools_config.js_repl_tools_only);
|
||||
assert_eq!(
|
||||
tools_config.environment_capabilities,
|
||||
ToolEnvironmentCapabilities::new(
|
||||
/*exec_enabled*/ false, /*filesystem_enabled*/ false,
|
||||
)
|
||||
);
|
||||
|
||||
let enabled_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,
|
||||
});
|
||||
assert_eq!(
|
||||
enabled_tools_config.apply_patch_tool_type,
|
||||
Some(ApplyPatchToolType::Freeform)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
|
||||
let model_info = model_info();
|
||||
|
||||
@@ -189,7 +189,7 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
plan.register_handler("update_plan", ToolHandlerKind::Plan);
|
||||
|
||||
if config.js_repl_enabled {
|
||||
if config.js_repl_enabled && config.environment_capabilities.exec_enabled {
|
||||
plan.push_spec(
|
||||
create_js_repl_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
@@ -265,7 +265,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 let Some(apply_patch_tool_type) = &config.apply_patch_tool_type
|
||||
&& config.environment_capabilities.exec_enabled
|
||||
{
|
||||
match apply_patch_tool_type {
|
||||
ApplyPatchToolType::Freeform => {
|
||||
plan.push_spec(
|
||||
@@ -285,10 +287,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 +334,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;
|
||||
@@ -672,6 +673,47 @@ fn js_repl_enabled_adds_tools() {
|
||||
assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_environment_omits_environment_backed_tools() {
|
||||
let mut model_info = model_info();
|
||||
model_info.experimental_supported_tools = vec!["list_dir".to_string()];
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::ApplyPatchFreeform);
|
||||
features.enable(Feature::JsRepl);
|
||||
|
||||
let available_models = Vec::new();
|
||||
let 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,
|
||||
));
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*app_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
for tool_name in [
|
||||
"exec_command",
|
||||
"write_stdin",
|
||||
"apply_patch",
|
||||
"js_repl",
|
||||
"js_repl_reset",
|
||||
"list_dir",
|
||||
"view_image",
|
||||
] {
|
||||
assert_lacks_tool_name(&tools, tool_name);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_generation_tools_require_feature_and_supported_model() {
|
||||
let supported_model_info = model_info();
|
||||
|
||||
6
defs.bzl
6
defs.bzl
@@ -129,6 +129,7 @@ def codex_rust_crate(
|
||||
integration_test_timeout = None,
|
||||
test_data_extra = [],
|
||||
test_tags = [],
|
||||
unit_test_target_compatible_with = None,
|
||||
unit_test_timeout = None,
|
||||
extra_binaries = []):
|
||||
"""Defines a Rust crate with library, binaries, and tests wired for Bazel + Cargo parity.
|
||||
@@ -164,6 +165,8 @@ def codex_rust_crate(
|
||||
test_data_extra: Extra runtime data for tests.
|
||||
test_tags: Tags applied to unit + integration test targets.
|
||||
Typically used to disable the sandbox, but see https://bazel.build/reference/be/common-definitions#common.tags
|
||||
unit_test_target_compatible_with: Optional target compatibility
|
||||
constraints for the generated unit-test binary and wrapper.
|
||||
unit_test_timeout: Optional Bazel timeout for the unit-test target
|
||||
generated from `src/**/*.rs`.
|
||||
extra_binaries: Additional binary labels to surface as test data and
|
||||
@@ -235,6 +238,7 @@ def codex_rust_crate(
|
||||
)
|
||||
|
||||
unit_test_binary = name + "-unit-tests-bin"
|
||||
unit_test_target_compatible_with = unit_test_target_compatible_with or []
|
||||
rust_test(
|
||||
name = unit_test_binary,
|
||||
crate = name,
|
||||
@@ -253,6 +257,7 @@ def codex_rust_crate(
|
||||
rustc_env = rustc_env,
|
||||
data = test_data_extra,
|
||||
tags = test_tags + ["manual"],
|
||||
target_compatible_with = unit_test_target_compatible_with,
|
||||
)
|
||||
|
||||
unit_test_kwargs = {}
|
||||
@@ -265,6 +270,7 @@ def codex_rust_crate(
|
||||
test_bin = ":" + unit_test_binary,
|
||||
workspace_root_marker = "//codex-rs/utils/cargo-bin:repo_root.marker",
|
||||
tags = test_tags,
|
||||
target_compatible_with = unit_test_target_compatible_with,
|
||||
**unit_test_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -5,9 +5,16 @@ set -euo pipefail
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
# Resolve the dynamic targets before printing anything so callers do not
|
||||
# continue with a partial list if `bazel query` fails.
|
||||
manual_rust_test_targets="$(bazel query 'kind("rust_test rule", attr(tags, "manual", //codex-rs/... except //codex-rs/v8-poc/...))')"
|
||||
manual_rust_test_targets=""
|
||||
if [[ "${RUNNER_OS:-}" != "Windows" ]]; then
|
||||
# Resolve the dynamic targets before printing anything so callers do not
|
||||
# continue with a partial list if `bazel query` fails.
|
||||
#
|
||||
# The generated manual `*-unit-tests-bin` targets pull in Windows-incompatible
|
||||
# V8/Python dependencies under gnullvm, so only include them on platforms
|
||||
# where they currently analyze successfully.
|
||||
manual_rust_test_targets="$(bazel query 'kind("rust_test rule", attr(tags, "manual", //codex-rs/... except //codex-rs/v8-poc/...))')"
|
||||
fi
|
||||
|
||||
printf '%s\n' \
|
||||
"//codex-rs/..." \
|
||||
@@ -17,4 +24,6 @@ printf '%s\n' \
|
||||
# underlying `rust_test` binaries. Add the internal manual `*-unit-tests-bin`
|
||||
# targets explicitly so inline `#[cfg(test)]` code is linted like
|
||||
# `cargo clippy --tests`.
|
||||
printf '%s\n' "${manual_rust_test_targets}"
|
||||
if [[ -n "${manual_rust_test_targets}" ]]; then
|
||||
printf '%s\n' "${manual_rust_test_targets}"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user