Reject local-only app-server APIs without local env

This commit is contained in:
starr-openai
2026-05-18 16:19:50 -07:00
parent 1643f20b47
commit 2ac71d4e0b
7 changed files with 143 additions and 2 deletions

View File

@@ -362,8 +362,12 @@ impl MessageProcessor {
Arc::clone(&config),
outgoing.clone(),
config_manager.clone(),
thread_manager.environment_manager(),
);
let process_exec_processor = ProcessExecRequestProcessor::new(
outgoing.clone(),
thread_manager.environment_manager(),
);
let process_exec_processor = ProcessExecRequestProcessor::new(outgoing.clone());
let feedback_processor = FeedbackRequestProcessor::new(
auth_manager.clone(),
Arc::clone(&thread_manager),

View File

@@ -6,6 +6,7 @@ pub(crate) struct CommandExecRequestProcessor {
config: Arc<Config>,
outgoing: Arc<OutgoingMessageSender>,
config_manager: ConfigManager,
environment_manager: Arc<EnvironmentManager>,
command_exec_manager: CommandExecManager,
}
@@ -15,12 +16,14 @@ impl CommandExecRequestProcessor {
config: Arc<Config>,
outgoing: Arc<OutgoingMessageSender>,
config_manager: ConfigManager,
environment_manager: Arc<EnvironmentManager>,
) -> Self {
Self {
arg0_paths,
config,
outgoing,
config_manager,
environment_manager,
command_exec_manager: CommandExecManager::default(),
}
}
@@ -90,6 +93,10 @@ impl CommandExecRequestProcessor {
) -> Result<(), JSONRPCErrorError> {
tracing::debug!("ExecOneOffCommand params: {params:?}");
if self.environment_manager.try_local_environment().is_none() {
return Err(internal_error("local environment is not configured"));
}
let request = request_id.clone();
if params.command.is_empty() {

View File

@@ -23,6 +23,7 @@ use codex_app_server_protocol::ServerNotification;
use codex_core::exec::ExecExpiration;
use codex_core::exec::ExecExpirationOutcome;
use codex_core::exec::IO_DRAIN_TIMEOUT_MS;
use codex_exec_server::EnvironmentManager;
use codex_protocol::exec_output::bytes_to_string_smart;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
@@ -48,13 +49,18 @@ const OUTPUT_CHUNK_SIZE_HINT: usize = 64 * 1024;
#[derive(Clone)]
pub(crate) struct ProcessExecRequestProcessor {
outgoing: Arc<OutgoingMessageSender>,
environment_manager: Arc<EnvironmentManager>,
process_exec_manager: ProcessExecManager,
}
impl ProcessExecRequestProcessor {
pub(crate) fn new(outgoing: Arc<OutgoingMessageSender>) -> Self {
pub(crate) fn new(
outgoing: Arc<OutgoingMessageSender>,
environment_manager: Arc<EnvironmentManager>,
) -> Self {
Self {
outgoing,
environment_manager,
process_exec_manager: ProcessExecManager::default(),
}
}
@@ -64,6 +70,10 @@ impl ProcessExecRequestProcessor {
request_id: ConnectionRequestId,
params: ProcessSpawnParams,
) -> Result<(), JSONRPCErrorError> {
if self.environment_manager.try_local_environment().is_none() {
return Err(internal_error("local environment is not configured"));
}
let ProcessSpawnParams {
command,
process_handle,

View File

@@ -1738,6 +1738,14 @@ impl ThreadRequestProcessor {
if command.is_empty() {
return Err(invalid_request("command must not be empty"));
}
if self
.thread_manager
.environment_manager()
.try_local_environment()
.is_none()
{
return Err(internal_error("local environment is not configured"));
}
let (_, thread) = self.load_thread(&thread_id).await?;
self.submit_core_op(

View File

@@ -17,6 +17,7 @@ use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxPolicy;
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
@@ -400,6 +401,45 @@ async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Resu
Ok(())
}
#[tokio::test]
async fn command_exec_returns_error_when_local_environment_is_disabled() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri(), "never")?;
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
)
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let command_request_id = mcp
.send_command_exec_request(CommandExecParams {
command: vec!["sh".to_string(), "-lc".to_string(), "true".to_string()],
process_id: None,
tty: false,
stream_stdin: false,
stream_stdout_stderr: false,
output_bytes_cap: None,
disable_output_cap: false,
disable_timeout: false,
timeout_ms: None,
cwd: None,
env: None,
size: None,
sandbox_policy: None,
permission_profile: None,
})
.await?;
let error = mcp
.read_stream_until_error_message(RequestId::Integer(command_request_id))
.await?;
assert_eq!(error.error.message, "local environment is not configured");
Ok(())
}
#[tokio::test]
async fn command_exec_rejects_sandbox_policy_with_permission_profile() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;

View File

@@ -6,6 +6,7 @@ use codex_app_server_protocol::ProcessExitedNotification;
use codex_app_server_protocol::ProcessKillParams;
use codex_app_server_protocol::ProcessSpawnParams;
use codex_app_server_protocol::RequestId;
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
@@ -102,6 +103,33 @@ async fn process_spawn_returns_before_exit_and_emits_exit_notification() -> Resu
Ok(())
}
#[tokio::test]
async fn process_spawn_returns_error_when_local_environment_is_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
create_config_toml(codex_home.path(), &server.uri(), "never")?;
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
)
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let process_request_id = mcp
.send_process_spawn_request(process_spawn_params(
"disabled-process".to_string(),
codex_home.path(),
vec!["sh".to_string(), "-lc".to_string(), "true".to_string()],
)?)
.await?;
let error = mcp
.read_stream_until_error_message(RequestId::Integer(process_request_id))
.await?;
assert_eq!(error.error.message, "local environment is not configured");
Ok(())
}
#[tokio::test]
async fn process_spawn_reports_buffered_output_cap_reached() -> Result<()> {
let codex_home = TempDir::new()?;

View File

@@ -32,6 +32,7 @@ use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::UserInput as V2UserInput;
use codex_core::shell::default_user_shell;
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
use codex_features::FEATURES;
use codex_features::Feature;
use pretty_assertions::assert_eq;
@@ -182,6 +183,49 @@ async fn thread_shell_command_history_responses_exclude_persisted_command_execut
Ok(())
}
#[tokio::test]
async fn thread_shell_command_returns_error_when_local_environment_is_disabled() -> Result<()> {
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let server = create_mock_responses_server_sequence(vec![]).await;
create_config_toml(
codex_home.as_path(),
&server.uri(),
"never",
&BTreeMap::default(),
)?;
let mut mcp = McpProcess::new_with_env(
codex_home.as_path(),
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
)
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
.send_thread_start_request(ThreadStartParams::default())
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let shell_id = mcp
.send_thread_shell_command_request(ThreadShellCommandParams {
thread_id: thread.id,
command: "pwd".to_string(),
})
.await?;
let error = mcp
.read_stream_until_error_message(RequestId::Integer(shell_id))
.await?;
assert_eq!(error.error.message, "local environment is not configured");
Ok(())
}
#[tokio::test]
async fn thread_shell_command_uses_existing_active_turn() -> Result<()> {
let tmp = TempDir::new()?;