Compare commits

...

5 Commits

Author SHA1 Message Date
starr-openai
cdd3a98444 Increase provider auth test timeout on Windows
Bazel Windows runners can be slow to launch PowerShell, and these tests
assert auth wiring rather than process-start latency. Match the broader
timeout already used by codex-login provider-auth tests.

Co-authored-by: Codex <noreply@openai.com>
2026-04-03 21:41:48 -07:00
starr-openai
8f8a7c6285 Skip manual Bazel clippy test targets on Windows 2026-04-03 19:05:58 -07:00
starr-openai
f2a9147108 Skip exec unit tests on Bazel windows targets 2026-04-03 18:59:31 -07:00
starr-openai
c40119efb1 Fix async env-lock clippy lint in project_doc tests 2026-04-03 18:34:35 -07:00
starr-openai
66ded7674c Add explicit disabled-environment tool gating 2026-04-03 18:26:58 -07:00
21 changed files with 403 additions and 44 deletions

View File

@@ -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,
));

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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)?;

View File

@@ -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(),

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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())?;

View File

@@ -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

View File

@@ -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,

View File

@@ -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]

View File

@@ -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;

View File

@@ -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": [],
}),
)

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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
)

View File

@@ -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