mirror of
https://github.com/openai/codex.git
synced 2026-03-17 11:26:33 +03:00
Compare commits
3 Commits
dev/mzeng/
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
603b6493a9 | ||
|
|
d37dcca7e0 | ||
|
|
57f865c069 |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -2500,7 +2500,6 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-ansi-escape",
|
||||
"codex-app-server-client",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-backend-client",
|
||||
|
||||
@@ -16,10 +16,8 @@ use std::os::fd::AsRawFd;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_core::error::CodexErr;
|
||||
use codex_core::error::Result;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::WritableRoot;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
/// Linux "platform defaults" that keep common system binaries and dynamic
|
||||
@@ -41,10 +39,10 @@ const LINUX_PLATFORM_DEFAULT_READ_ROOTS: &[&str] = &[
|
||||
/// Options that control how bubblewrap is invoked.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct BwrapOptions {
|
||||
/// Whether to mount a fresh `/proc` inside the PID namespace.
|
||||
/// Whether to mount a fresh `/proc` inside the sandbox.
|
||||
///
|
||||
/// This is the secure default, but some restrictive container environments
|
||||
/// deny `--proc /proc` even when PID namespaces are available.
|
||||
/// deny `--proc /proc`.
|
||||
pub mount_proc: bool,
|
||||
/// How networking should be configured inside the bubblewrap sandbox.
|
||||
pub network_mode: BwrapNetworkMode,
|
||||
@@ -167,7 +165,6 @@ fn create_bwrap_flags(
|
||||
// Request a user namespace explicitly rather than relying on bubblewrap's
|
||||
// auto-enable behavior, which is skipped when the caller runs as uid 0.
|
||||
args.push("--unshare-user".to_string());
|
||||
// Isolate the PID namespace.
|
||||
args.push("--unshare-pid".to_string());
|
||||
if options.network_mode.should_unshare_network() {
|
||||
args.push("--unshare-net".to_string());
|
||||
@@ -213,9 +210,15 @@ fn create_filesystem_args(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> Result<BwrapArgs> {
|
||||
let writable_roots = file_system_sandbox_policy.get_writable_roots_with_cwd(cwd);
|
||||
// Bubblewrap requires bind mount targets to exist. Skip missing writable
|
||||
// roots so mixed-platform configs can keep harmless paths for other
|
||||
// environments without breaking Linux command startup.
|
||||
let writable_roots = file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
.filter(|writable_root| writable_root.root.as_path().exists())
|
||||
.collect::<Vec<_>>();
|
||||
let unreadable_roots = file_system_sandbox_policy.get_unreadable_roots_with_cwd(cwd);
|
||||
ensure_mount_targets_exist(&writable_roots)?;
|
||||
|
||||
let mut args = if file_system_sandbox_policy.has_full_disk_read_access() {
|
||||
// Read-only root, then mount a minimal device tree.
|
||||
@@ -385,23 +388,6 @@ fn create_filesystem_args(
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate that writable roots exist before constructing mounts.
|
||||
///
|
||||
/// Bubblewrap requires bind mount targets to exist. We fail fast with a clear
|
||||
/// error so callers can present an actionable message.
|
||||
fn ensure_mount_targets_exist(writable_roots: &[WritableRoot]) -> Result<()> {
|
||||
for writable_root in writable_roots {
|
||||
let root = writable_root.root.as_path();
|
||||
if !root.exists() {
|
||||
return Err(CodexErr::UnsupportedOperation(format!(
|
||||
"Sandbox expected writable root {root}, but it does not exist.",
|
||||
root = root.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_to_string(path: &Path) -> String {
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
@@ -731,6 +717,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_missing_writable_roots() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let existing_root = temp_dir.path().join("existing");
|
||||
let missing_root = temp_dir.path().join("missing");
|
||||
std::fs::create_dir(&existing_root).expect("create existing root");
|
||||
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![
|
||||
AbsolutePathBuf::try_from(existing_root.as_path()).expect("absolute existing root"),
|
||||
AbsolutePathBuf::try_from(missing_root.as_path()).expect("absolute missing root"),
|
||||
],
|
||||
read_only_access: Default::default(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
|
||||
let args = create_filesystem_args(&FileSystemSandboxPolicy::from(&policy), temp_dir.path())
|
||||
.expect("filesystem args");
|
||||
let existing_root = path_to_string(&existing_root);
|
||||
let missing_root = path_to_string(&missing_root);
|
||||
|
||||
assert!(
|
||||
args.args.windows(3).any(|window| {
|
||||
window == ["--bind", existing_root.as_str(), existing_root.as_str()]
|
||||
}),
|
||||
"existing writable root should be rebound writable",
|
||||
);
|
||||
assert!(
|
||||
!args.args.iter().any(|arg| arg == &missing_root),
|
||||
"missing writable root should be skipped",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mounts_dev_before_writable_dev_binds() {
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
|
||||
@@ -310,6 +310,32 @@ async fn test_writable_root() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sandbox_ignores_missing_writable_roots_under_bwrap() {
|
||||
if should_skip_bwrap_tests().await {
|
||||
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
let existing_root = tempdir.path().join("existing");
|
||||
let missing_root = tempdir.path().join("missing");
|
||||
std::fs::create_dir(&existing_root).expect("create existing root");
|
||||
|
||||
let output = run_cmd_result_with_writable_roots(
|
||||
&["bash", "-lc", "printf sandbox-ok"],
|
||||
&[existing_root, missing_root],
|
||||
LONG_TIMEOUT_MS,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("sandboxed command should execute");
|
||||
|
||||
assert_eq!(output.exit_code, 0);
|
||||
assert_eq!(output.stdout.text, "sandbox-ok");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_new_privs_is_enabled() {
|
||||
let output = run_cmd_output(
|
||||
|
||||
@@ -29,7 +29,6 @@ base64 = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-ansi-escape = { workspace = true }
|
||||
codex-app-server-client = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
|
||||
@@ -39,7 +39,6 @@ use crate::tui::TuiEvent;
|
||||
use crate::update_action::UpdateAction;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_app_server_client::InProcessAppServerClient;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::CodexAuth;
|
||||
@@ -53,6 +52,7 @@ use codex_core::config::types::ApprovalsReviewer;
|
||||
use codex_core::config::types::ModelAvailabilityNuxConfig;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_core::models_manager::manager::RefreshStrategy;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG;
|
||||
@@ -113,7 +113,6 @@ use tokio::task::JoinHandle;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
mod agent_navigation;
|
||||
mod app_server_adapter;
|
||||
mod pending_interactive_replay;
|
||||
|
||||
use self::agent_navigation::AgentNavigationDirection;
|
||||
@@ -1948,7 +1947,7 @@ impl App {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn run(
|
||||
tui: &mut tui::Tui,
|
||||
mut app_server: InProcessAppServerClient,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
mut config: Config,
|
||||
cli_kv_overrides: Vec<(String, TomlValue)>,
|
||||
harness_overrides: ConfigOverrides,
|
||||
@@ -1968,8 +1967,20 @@ impl App {
|
||||
|
||||
let harness_overrides =
|
||||
normalize_harness_overrides_for_cwd(harness_overrides, &config.cwd)?;
|
||||
let auth_manager = app_server.auth_manager();
|
||||
let thread_manager = app_server.thread_manager();
|
||||
let thread_manager = Arc::new(ThreadManager::new(
|
||||
&config,
|
||||
auth_manager.clone(),
|
||||
SessionSource::Cli,
|
||||
CollaborationModesConfig {
|
||||
default_mode_request_user_input: config
|
||||
.features
|
||||
.enabled(Feature::DefaultModeRequestUserInput),
|
||||
},
|
||||
));
|
||||
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
.maybe_start_curated_repo_sync_for_config(&config);
|
||||
let mut model = thread_manager
|
||||
.get_models_manager()
|
||||
.get_default_model(&config.model, RefreshStrategy::Offline)
|
||||
@@ -1987,13 +1998,6 @@ impl App {
|
||||
)
|
||||
.await;
|
||||
if let Some(exit_info) = exit_info {
|
||||
app_server
|
||||
.shutdown()
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
tracing::warn!("app-server shutdown failed: {err}");
|
||||
})
|
||||
.ok();
|
||||
return Ok(exit_info);
|
||||
}
|
||||
if let Some(updated_model) = config.model.clone() {
|
||||
@@ -2225,7 +2229,6 @@ impl App {
|
||||
|
||||
let mut thread_created_rx = thread_manager.subscribe_thread_created();
|
||||
let mut listen_for_threads = true;
|
||||
let mut listen_for_app_server_events = true;
|
||||
let mut waiting_for_initial_session_configured = wait_for_initial_session_configured;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -2285,16 +2288,6 @@ impl App {
|
||||
Err(err) => break Err(err),
|
||||
}
|
||||
}
|
||||
app_server_event = app_server.next_event(), if listen_for_app_server_events => {
|
||||
match app_server_event {
|
||||
Some(event) => app.handle_app_server_event(&app_server, event).await,
|
||||
None => {
|
||||
listen_for_app_server_events = false;
|
||||
tracing::warn!("app-server event stream closed");
|
||||
}
|
||||
}
|
||||
AppRunControl::Continue
|
||||
}
|
||||
// Listen on new thread creation due to collab tools.
|
||||
created = thread_created_rx.recv(), if listen_for_threads => {
|
||||
match created {
|
||||
@@ -2325,9 +2318,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Err(err) = app_server.shutdown().await {
|
||||
tracing::warn!(error = %err, "failed to shut down embedded app server");
|
||||
}
|
||||
let clear_result = tui.terminal.clear();
|
||||
let exit_reason = match exit_reason_result {
|
||||
Ok(exit_reason) => {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
This module holds the temporary adapter layer between the TUI and the app
|
||||
server during the hybrid migration period.
|
||||
|
||||
For now, the TUI still owns its existing direct-core behavior, but startup
|
||||
allocates a local in-process app server and drains its event stream. Keeping
|
||||
the app-server-specific wiring here keeps that transitional logic out of the
|
||||
main `app.rs` orchestration path.
|
||||
|
||||
As more TUI flows move onto the app-server surface directly, this adapter
|
||||
should shrink and eventually disappear.
|
||||
*/
|
||||
|
||||
use super::App;
|
||||
use codex_app_server_client::InProcessAppServerClient;
|
||||
use codex_app_server_client::InProcessServerEvent;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
|
||||
impl App {
|
||||
pub(super) async fn handle_app_server_event(
|
||||
&mut self,
|
||||
app_server_client: &InProcessAppServerClient,
|
||||
event: InProcessServerEvent,
|
||||
) {
|
||||
match event {
|
||||
InProcessServerEvent::Lagged { skipped } => {
|
||||
tracing::warn!(
|
||||
skipped,
|
||||
"app-server event consumer lagged; dropping ignored events"
|
||||
);
|
||||
}
|
||||
InProcessServerEvent::ServerNotification(_) => {}
|
||||
InProcessServerEvent::LegacyNotification(_) => {}
|
||||
InProcessServerEvent::ServerRequest(request) => {
|
||||
let request_id = request.id().clone();
|
||||
tracing::warn!(
|
||||
?request_id,
|
||||
"rejecting app-server request while TUI still uses direct core APIs"
|
||||
);
|
||||
if let Err(err) = self
|
||||
.reject_app_server_request(
|
||||
app_server_client,
|
||||
request_id,
|
||||
"TUI client does not yet handle this app-server server request".to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn reject_app_server_request(
|
||||
&self,
|
||||
app_server_client: &InProcessAppServerClient,
|
||||
request_id: codex_app_server_protocol::RequestId,
|
||||
reason: String,
|
||||
) -> std::result::Result<(), String> {
|
||||
app_server_client
|
||||
.reject_server_request(
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code: -32000,
|
||||
message: reason,
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("failed to reject app-server request: {err}"))
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,6 @@ use additional_dirs::add_dir_warning_message;
|
||||
use app::App;
|
||||
pub use app::AppExitInfo;
|
||||
pub use app::ExitReason;
|
||||
use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
|
||||
use codex_app_server_client::InProcessAppServerClient;
|
||||
use codex_app_server_client::InProcessClientStartArgs;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_cloud_requirements::cloud_requirements_loader;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::CodexAuth;
|
||||
@@ -50,15 +46,12 @@ use codex_state::log_db;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_oss::ensure_oss_provider_ready;
|
||||
use codex_utils_oss::get_default_model_for_oss_provider;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use cwd_prompt::CwdPromptAction;
|
||||
use cwd_prompt::CwdPromptOutcome;
|
||||
use cwd_prompt::CwdSelection;
|
||||
use std::fs::OpenOptions;
|
||||
use std::future::Future;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use tracing_appender::non_blocking;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
@@ -246,74 +239,10 @@ pub use public_widgets::composer_input::ComposerAction;
|
||||
pub use public_widgets::composer_input::ComposerInput;
|
||||
// (tests access modules directly within the crate)
|
||||
|
||||
async fn start_embedded_app_server(
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
config: Config,
|
||||
cli_kv_overrides: Vec<(String, toml::Value)>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
feedback: codex_feedback::CodexFeedback,
|
||||
) -> color_eyre::Result<InProcessAppServerClient> {
|
||||
start_embedded_app_server_with(
|
||||
arg0_paths,
|
||||
config,
|
||||
cli_kv_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
feedback,
|
||||
InProcessAppServerClient::start,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn start_embedded_app_server_with<F, Fut>(
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
config: Config,
|
||||
cli_kv_overrides: Vec<(String, toml::Value)>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
feedback: codex_feedback::CodexFeedback,
|
||||
start_client: F,
|
||||
) -> color_eyre::Result<InProcessAppServerClient>
|
||||
where
|
||||
F: FnOnce(InProcessClientStartArgs) -> Fut,
|
||||
Fut: Future<Output = std::io::Result<InProcessAppServerClient>>,
|
||||
{
|
||||
let config_warnings = config
|
||||
.startup_warnings
|
||||
.iter()
|
||||
.map(|warning| ConfigWarningNotification {
|
||||
summary: warning.clone(),
|
||||
details: None,
|
||||
path: None,
|
||||
range: None,
|
||||
})
|
||||
.collect();
|
||||
let client = start_client(InProcessClientStartArgs {
|
||||
arg0_paths,
|
||||
config: Arc::new(config),
|
||||
cli_overrides: cli_kv_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
feedback,
|
||||
config_warnings,
|
||||
session_source: codex_protocol::protocol::SessionSource::Cli,
|
||||
enable_codex_api_key_env: false,
|
||||
client_name: "codex-tui".to_string(),
|
||||
client_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
experimental_api: true,
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
.await
|
||||
.wrap_err("failed to start embedded app server")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn run_main(
|
||||
mut cli: Cli,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
loader_overrides: LoaderOverrides,
|
||||
_loader_overrides: LoaderOverrides,
|
||||
) -> std::io::Result<AppExitInfo> {
|
||||
let (sandbox_mode, approval_policy) = if cli.full_auto {
|
||||
(
|
||||
@@ -611,8 +540,6 @@ pub async fn run_main(
|
||||
|
||||
run_ratatui_app(
|
||||
cli,
|
||||
arg0_paths,
|
||||
loader_overrides,
|
||||
config,
|
||||
overrides,
|
||||
cli_kv_overrides,
|
||||
@@ -626,8 +553,6 @@ pub async fn run_main(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_ratatui_app(
|
||||
cli: Cli,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
loader_overrides: LoaderOverrides,
|
||||
initial_config: Config,
|
||||
overrides: ConfigOverrides,
|
||||
cli_kv_overrides: Vec<(String, toml::Value)>,
|
||||
@@ -1025,27 +950,10 @@ async fn run_ratatui_app(
|
||||
|
||||
let use_alt_screen = determine_alt_screen_mode(no_alt_screen, config.tui_alternate_screen);
|
||||
tui.set_alt_screen_enabled(use_alt_screen);
|
||||
let app_server = match start_embedded_app_server(
|
||||
arg0_paths,
|
||||
config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides,
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(app_server) => app_server,
|
||||
Err(err) => {
|
||||
restore();
|
||||
session_log::log_session_end();
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let app_result = App::run(
|
||||
&mut tui,
|
||||
app_server,
|
||||
auth_manager,
|
||||
config,
|
||||
cli_kv_overrides.clone(),
|
||||
overrides.clone(),
|
||||
@@ -1328,20 +1236,6 @@ mod tests {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn start_test_embedded_app_server(
|
||||
config: Config,
|
||||
) -> color_eyre::Result<InProcessAppServerClient> {
|
||||
start_embedded_app_server(
|
||||
Arg0DispatchPaths::default(),
|
||||
config,
|
||||
Vec::new(),
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
codex_feedback::CodexFeedback::new(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn windows_shows_trust_prompt_without_sandbox() -> std::io::Result<()> {
|
||||
@@ -1358,51 +1252,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn embedded_app_server_exposes_client_manager_accessors() -> color_eyre::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let config = build_config(&temp_dir).await?;
|
||||
let app_server = start_test_embedded_app_server(config).await?;
|
||||
|
||||
assert!(Arc::ptr_eq(
|
||||
&app_server.auth_manager(),
|
||||
&app_server.auth_manager()
|
||||
));
|
||||
assert!(Arc::ptr_eq(
|
||||
&app_server.thread_manager(),
|
||||
&app_server.thread_manager()
|
||||
));
|
||||
|
||||
app_server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn embedded_app_server_start_failure_is_returned() -> color_eyre::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let config = build_config(&temp_dir).await?;
|
||||
let result = start_embedded_app_server_with(
|
||||
Arg0DispatchPaths::default(),
|
||||
config,
|
||||
Vec::new(),
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
codex_feedback::CodexFeedback::new(),
|
||||
|_args| async { Err(std::io::Error::other("boom")) },
|
||||
)
|
||||
.await;
|
||||
let err = match result {
|
||||
Ok(_) => panic!("startup failure should be returned"),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("failed to start embedded app server"),
|
||||
"error should preserve the embedded app server startup context"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn windows_shows_trust_prompt_with_sandbox() -> std::io::Result<()> {
|
||||
|
||||
@@ -89,6 +89,17 @@ impl App {
|
||||
);
|
||||
}
|
||||
notification => {
|
||||
if !app_server_client.is_remote()
|
||||
&& matches!(
|
||||
notification,
|
||||
ServerNotification::TurnCompleted(_)
|
||||
| ServerNotification::ThreadRealtimeItemAdded(_)
|
||||
| ServerNotification::ThreadRealtimeOutputAudioDelta(_)
|
||||
| ServerNotification::ThreadRealtimeError(_)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some((thread_id, events)) =
|
||||
server_notification_thread_events(notification)
|
||||
{
|
||||
@@ -116,6 +127,9 @@ impl App {
|
||||
AppServerEvent::LegacyNotification(notification) => {
|
||||
if let Some((thread_id, event)) = legacy_thread_event(notification.params) {
|
||||
self.pending_app_server_requests.note_legacy_event(&event);
|
||||
if legacy_event_is_shadowed_by_server_notification(&event.msg) {
|
||||
return;
|
||||
}
|
||||
if self.primary_thread_id.is_none()
|
||||
|| matches!(event.msg, EventMsg::SessionConfigured(_))
|
||||
&& self.primary_thread_id == Some(thread_id)
|
||||
@@ -198,6 +212,24 @@ fn legacy_thread_event(params: Option<Value>) -> Option<(ThreadId, Event)> {
|
||||
Some((thread_id, event))
|
||||
}
|
||||
|
||||
fn legacy_event_is_shadowed_by_server_notification(msg: &EventMsg) -> bool {
|
||||
matches!(
|
||||
msg,
|
||||
EventMsg::TokenCount(_)
|
||||
| EventMsg::Error(_)
|
||||
| EventMsg::ThreadNameUpdated(_)
|
||||
| EventMsg::TurnStarted(_)
|
||||
| EventMsg::ItemStarted(_)
|
||||
| EventMsg::ItemCompleted(_)
|
||||
| EventMsg::AgentMessageDelta(_)
|
||||
| EventMsg::PlanDelta(_)
|
||||
| EventMsg::AgentReasoningDelta(_)
|
||||
| EventMsg::AgentReasoningRawContentDelta(_)
|
||||
| EventMsg::RealtimeConversationStarted(_)
|
||||
| EventMsg::RealtimeConversationClosed(_)
|
||||
)
|
||||
}
|
||||
|
||||
fn server_notification_thread_events(
|
||||
notification: ServerNotification,
|
||||
) -> Option<(ThreadId, Vec<Event>)> {
|
||||
|
||||
Reference in New Issue
Block a user