Compare commits

...

5 Commits

Author SHA1 Message Date
pakrym-oai
9f117ee8f7 Merge remote-tracking branch 'origin/main' into pakrym/async-fs-trust-migration
# Conflicts:
#	codex-rs/core/src/git_info_tests.rs
#	codex-rs/core/src/realtime_context.rs
#	codex-rs/core/src/tools/spec_tests.rs
#	codex-rs/git-utils/src/info.rs
#	codex-rs/tui/src/onboarding/trust_directory.rs
#	codex-rs/tui_app_server/src/onboarding/trust_directory.rs
2026-03-24 15:54:04 -07:00
pakrym-oai
9d8c893cd3 Revert git_info implementation 2026-03-24 15:36:40 -07:00
pakrym-oai
109a876270 Migrate trust resolution and config loading to async fs 2026-03-24 15:25:47 -07:00
pakrym-oai
bc1714b51e codex: fix CI failure on PR #15665 2026-03-24 11:54:32 -07:00
pakrym-oai
1390d7be39 Drop sandbox_permissions from sandbox exec requests 2026-03-24 11:19:32 -07:00
31 changed files with 771 additions and 603 deletions

View File

@@ -821,6 +821,7 @@ mod tests {
match ConfigBuilder::default().build().await {
Ok(config) => config,
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
.await
.expect("default config should load"),
}
}

View File

@@ -20,8 +20,8 @@ use codex_app_server_protocol::FsWriteFileResponse;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::Environment;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::LOCAL_FS;
use codex_exec_server::RemoveOptions;
use std::io;
use std::sync::Arc;
@@ -34,7 +34,7 @@ pub(crate) struct FsApi {
impl Default for FsApi {
fn default() -> Self {
Self {
file_system: Environment::default().get_filesystem(),
file_system: Arc::new(LOCAL_FS),
}
}
}

View File

@@ -698,6 +698,7 @@ mod tests {
match ConfigBuilder::default().build().await {
Ok(config) => config,
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
.await
.expect("default config should load"),
}
}

View File

@@ -447,12 +447,14 @@ pub async fn run_main_with_transport(
Err(err) => {
let message = config_warning_from_error("Invalid configuration; using defaults.", &err);
config_warnings.push(message);
Config::load_default_with_cli_overrides(cli_kv_overrides.clone()).map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading default config after config error: {e}"),
)
})?
Config::load_default_with_cli_overrides(cli_kv_overrides.clone())
.await
.map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading default config after config error: {e}"),
)
})?
}
};

View File

@@ -71,7 +71,8 @@ async fn apply_role_to_config_inner(
role_layer_toml,
preserve_current_profile,
preserve_current_provider,
)?;
)
.await?;
Ok(())
}
@@ -143,7 +144,7 @@ fn preservation_policy(config: &Config, role_layer_toml: &TomlValue) -> (bool, b
mod reload {
use super::*;
pub(super) fn build_next_config(
pub(super) async fn build_next_config(
config: &Config,
role_layer_toml: TomlValue,
preserve_current_profile: bool,
@@ -164,7 +165,8 @@ mod reload {
reload_overrides(config, preserve_current_provider),
config.codex_home.clone(),
config_layer_stack,
)?;
)
.await?;
if preserve_current_profile {
next_config.active_profile = config.active_profile.clone();
}

View File

@@ -550,7 +550,7 @@ async fn get_base_instructions_no_user_content() {
];
let (session, _turn_context) = make_session_and_context().await;
let config = test_config();
let config = test_config().await;
for test_case in test_cases {
let model_info = model_info_for_slug(test_case.slug, &config);
@@ -715,8 +715,8 @@ fn collect_explicit_app_ids_from_skill_items_skips_plain_mentions_with_skill_con
assert_eq!(connector_ids, HashSet::<String>::new());
}
#[test]
fn non_app_mcp_tools_remain_visible_without_search_selection() {
#[tokio::test]
async fn non_app_mcp_tools_remain_visible_without_search_selection() {
let mcp_tools = HashMap::from([
(
"mcp__codex_apps__calendar_create_event".to_string(),
@@ -747,7 +747,7 @@ fn non_app_mcp_tools_remain_visible_without_search_selection() {
&explicitly_enabled_connectors,
&HashMap::new(),
);
let config = test_config();
let config = test_config().await;
selected_mcp_tools.extend(filter_codex_apps_mcp_tools(
&mcp_tools,
&connectors,
@@ -759,8 +759,8 @@ fn non_app_mcp_tools_remain_visible_without_search_selection() {
assert_eq!(tool_names, vec!["mcp__rmcp__echo".to_string()]);
}
#[test]
fn search_tool_selection_keeps_codex_apps_tools_without_mentions() {
#[tokio::test]
async fn search_tool_selection_keeps_codex_apps_tools_without_mentions() {
let selected_tool_names = [
"mcp__codex_apps__calendar_create_event".to_string(),
"mcp__rmcp__echo".to_string(),
@@ -794,7 +794,7 @@ fn search_tool_selection_keeps_codex_apps_tools_without_mentions() {
&explicitly_enabled_connectors,
&HashMap::new(),
);
let config = test_config();
let config = test_config().await;
selected_mcp_tools.extend(filter_codex_apps_mcp_tools(
&mcp_tools,
&connectors,
@@ -812,8 +812,8 @@ fn search_tool_selection_keeps_codex_apps_tools_without_mentions() {
);
}
#[test]
fn apps_mentions_add_codex_apps_tools_to_search_selected_set() {
#[tokio::test]
async fn apps_mentions_add_codex_apps_tools_to_search_selected_set() {
let selected_tool_names = ["mcp__rmcp__echo".to_string()];
let mcp_tools = HashMap::from([
(
@@ -844,7 +844,7 @@ fn apps_mentions_add_codex_apps_tools_to_search_selected_set() {
&explicitly_enabled_connectors,
&HashMap::new(),
);
let config = test_config();
let config = test_config().await;
selected_mcp_tools.extend(filter_codex_apps_mcp_tools(
&mcp_tools,
&connectors,

File diff suppressed because it is too large Load Diff

View File

@@ -209,13 +209,14 @@ fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option<PathBuf> {
}
}
#[cfg(test)]
pub(crate) fn test_config() -> Config {
pub(crate) async fn test_config() -> Config {
let codex_home = tempfile::tempdir().expect("create temp dir");
Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
codex_home.path().to_path_buf(),
)
.await
.expect("load default test config")
}
@@ -722,6 +723,7 @@ impl ConfigBuilder {
codex_home,
config_layer_stack,
)
.await
}
}
@@ -737,7 +739,7 @@ impl Config {
}
/// Load a default configuration when user config files are invalid.
pub fn load_default_with_cli_overrides(
pub async fn load_default_with_cli_overrides(
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<Self> {
let codex_home = find_codex_home()?;
@@ -756,6 +758,7 @@ impl Config {
codex_home,
ConfigLayerStack::default(),
)
.await
}
/// This is a secondary way of creating [Config], which is appropriate when
@@ -1660,7 +1663,7 @@ pub struct GhostSnapshotToml {
impl ConfigToml {
/// Derive the effective sandbox policy from the configuration.
fn derive_sandbox_policy(
async fn derive_sandbox_policy(
&self,
sandbox_mode_override: Option<SandboxMode>,
profile_sandbox_mode: Option<SandboxMode>,
@@ -1671,15 +1674,13 @@ impl ConfigToml {
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
|| profile_sandbox_mode.is_some()
|| self.sandbox_mode.is_some();
let resolved_sandbox_mode = sandbox_mode_override
.or(profile_sandbox_mode)
.or(self.sandbox_mode)
.or_else(|| {
// If no sandbox_mode is set but this directory has a trust decision,
// default to workspace-write except on unsandboxed Windows where we
// default to read-only.
self.get_active_project(resolved_cwd).and_then(|p| {
if p.is_trusted() || p.is_untrusted() {
let trust_default_sandbox_mode = if sandbox_mode_was_explicit {
None
} else {
self.get_active_project(resolved_cwd)
.await
.and_then(|project| {
if project.is_trusted() || project.is_untrusted() {
if cfg!(target_os = "windows")
&& windows_sandbox_level
== codex_protocol::config_types::WindowsSandboxLevel::Disabled
@@ -1692,7 +1693,11 @@ impl ConfigToml {
None
}
})
})
};
let resolved_sandbox_mode = sandbox_mode_override
.or(profile_sandbox_mode)
.or(self.sandbox_mode)
.or(trust_default_sandbox_mode)
.unwrap_or_default();
let mut sandbox_policy = match resolved_sandbox_mode {
SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(),
@@ -1742,7 +1747,7 @@ impl ConfigToml {
/// Resolves the cwd to an existing project, or returns None if ConfigToml
/// does not contain a project corresponding to cwd or a git repo for cwd
pub fn get_active_project(&self, resolved_cwd: &Path) -> Option<ProjectConfig> {
pub async fn get_active_project(&self, resolved_cwd: &Path) -> Option<ProjectConfig> {
let projects = self.projects.clone().unwrap_or_default();
if let Some(project_config) = projects.get(&resolved_cwd.to_string_lossy().to_string()) {
@@ -1752,7 +1757,7 @@ impl ConfigToml {
// If cwd lives inside a git repo/worktree, check whether the root git project
// (the primary repository working directory) is trusted. This lets
// worktrees inherit trust from the main project.
if let Some(repo_root) = resolve_root_git_project_for_trust(resolved_cwd)
if let Some(repo_root) = resolve_root_git_project_for_trust(resolved_cwd).await
&& let Some(project_config_for_root) =
projects.get(&repo_root.to_string_lossy().to_string_lossy().to_string())
{
@@ -1999,17 +2004,17 @@ pub(crate) fn resolve_web_search_mode_for_turn(
impl Config {
#[cfg(test)]
fn load_from_base_config_with_overrides(
async fn load_from_base_config_with_overrides(
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: PathBuf,
) -> std::io::Result<Self> {
// Note this ignores requirements.toml enforcement for tests.
let config_layer_stack = ConfigLayerStack::default();
Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack)
Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack).await
}
pub(crate) fn load_config_with_layer_stack(
pub(crate) async fn load_config_with_layer_stack(
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: PathBuf,
@@ -2128,6 +2133,7 @@ impl Config {
.collect::<Result<Vec<_>, _>>()?;
let active_project = cfg
.get_active_project(&resolved_cwd)
.await
.unwrap_or(ProjectConfig { trust_level: None });
let permission_config_syntax = resolve_permission_config_syntax(
&config_layer_stack,
@@ -2215,13 +2221,15 @@ impl Config {
)
} else {
let configured_network_proxy_config = NetworkProxyConfig::default();
let mut sandbox_policy = cfg.derive_sandbox_policy(
sandbox_mode,
config_profile.sandbox_mode,
windows_sandbox_level,
&resolved_cwd,
Some(&constrained_sandbox_policy),
);
let mut sandbox_policy = cfg
.derive_sandbox_policy(
sandbox_mode,
config_profile.sandbox_mode,
windows_sandbox_level,
&resolved_cwd,
Some(&constrained_sandbox_policy),
)
.await;
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
for path in &additional_writable_roots {
if !writable_roots.iter().any(|existing| existing == path) {

View File

@@ -14,8 +14,8 @@ fn normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths() {
assert_eq!(parsed, PathBuf::from(r"D:\c\x\worktrees\2508\swift-base"));
}
#[test]
fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> {
#[tokio::test]
async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let cwd = temp_dir.path().join("workspace");
let codex_home = temp_dir.path().join(".codex");
@@ -54,7 +54,8 @@ fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()>
..Default::default()
},
codex_home,
)?;
)
.await?;
let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?;
let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?;

View File

@@ -686,7 +686,7 @@ async fn project_trust_context(
let projects = project_trust_config.projects.unwrap_or_default();
let project_root_key = project_root.as_path().to_string_lossy().to_string();
let repo_root = resolve_root_git_project_for_trust(cwd.as_path());
let repo_root = resolve_root_git_project_for_trust(cwd.as_path()).await;
let repo_root_key = repo_root
.as_ref()
.map(|root| root.to_string_lossy().to_string());

View File

@@ -426,10 +426,14 @@ async fn test_get_git_working_tree_state_branch_fallback() {
assert_eq!(state.sha, GitSha::new(&remote_sha));
}
#[test]
fn resolve_root_git_project_for_trust_returns_none_outside_repo() {
#[tokio::test]
async fn resolve_root_git_project_for_trust_returns_none_outside_repo() {
let tmp = TempDir::new().expect("tempdir");
assert!(resolve_root_git_project_for_trust(tmp.path()).is_none());
assert!(
resolve_root_git_project_for_trust(tmp.path())
.await
.is_none()
);
}
#[tokio::test]
@@ -439,12 +443,15 @@ async fn resolve_root_git_project_for_trust_regular_repo_returns_repo_root() {
let expected = std::fs::canonicalize(&repo_path).unwrap();
assert_eq!(
resolve_root_git_project_for_trust(&repo_path),
resolve_root_git_project_for_trust(&repo_path).await,
Some(expected.clone())
);
let nested = repo_path.join("sub/dir");
std::fs::create_dir_all(&nested).unwrap();
assert_eq!(resolve_root_git_project_for_trust(&nested), Some(expected));
assert_eq!(
resolve_root_git_project_for_trust(&nested).await,
Some(expected)
);
}
#[tokio::test]
@@ -467,18 +474,20 @@ async fn resolve_root_git_project_for_trust_detects_worktree_and_returns_main_ro
.expect("git worktree add");
let expected = std::fs::canonicalize(&repo_path).ok();
let got =
resolve_root_git_project_for_trust(&wt_root).and_then(|p| std::fs::canonicalize(p).ok());
let got = resolve_root_git_project_for_trust(&wt_root)
.await
.and_then(|path| std::fs::canonicalize(path).ok());
assert_eq!(got, expected);
let nested = wt_root.join("nested/sub");
std::fs::create_dir_all(&nested).unwrap();
let got_nested =
resolve_root_git_project_for_trust(&nested).and_then(|p| std::fs::canonicalize(p).ok());
let got_nested = resolve_root_git_project_for_trust(&nested)
.await
.and_then(|path| std::fs::canonicalize(path).ok());
assert_eq!(got_nested, expected);
}
#[test]
fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_command() {
#[tokio::test]
async fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_command() {
let tmp = TempDir::new().expect("tempdir");
let repo_root = tmp.path().join("repo");
let common_dir = repo_root.join(".git");
@@ -495,17 +504,17 @@ fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_comma
let expected = std::fs::canonicalize(&repo_root).unwrap();
assert_eq!(
resolve_root_git_project_for_trust(&worktree_root),
resolve_root_git_project_for_trust(&worktree_root).await,
Some(expected.clone())
);
assert_eq!(
resolve_root_git_project_for_trust(&worktree_root.join("nested")),
resolve_root_git_project_for_trust(&worktree_root.join("nested")).await,
Some(expected)
);
}
#[test]
fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() {
#[tokio::test]
async fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() {
let tmp = TempDir::new().expect("tempdir");
let proj = tmp.path().join("proj");
std::fs::create_dir_all(proj.join("nested")).unwrap();
@@ -520,8 +529,12 @@ fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() {
)
.unwrap();
assert!(resolve_root_git_project_for_trust(&proj).is_none());
assert!(resolve_root_git_project_for_trust(&proj.join("nested")).is_none());
assert!(resolve_root_git_project_for_trust(&proj).await.is_none());
assert!(
resolve_root_git_project_for_trust(&proj.join("nested"))
.await
.is_none()
);
}
#[tokio::test]

View File

@@ -743,9 +743,9 @@ async fn interrupt_and_drain_turn(codex: &Codex) -> anyhow::Result<()> {
mod tests {
use super::*;
#[test]
fn guardian_review_session_config_change_invalidates_cached_session() {
let parent_config = crate::config::test_config();
#[tokio::test]
async fn guardian_review_session_config_change_invalidates_cached_session() {
let parent_config = crate::config::test_config().await;
let cached_spawn_config =
build_guardian_review_session_config(&parent_config, None, "active-model", None)
.expect("cached guardian config");

View File

@@ -963,9 +963,9 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
Ok(())
}
#[test]
fn guardian_review_session_config_preserves_parent_network_proxy() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_preserves_parent_network_proxy() {
let mut parent_config = test_config().await;
let network = NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig::default(),
Some(NetworkConstraints {
@@ -1005,9 +1005,9 @@ fn guardian_review_session_config_preserves_parent_network_proxy() {
);
}
#[test]
fn guardian_review_session_config_overrides_parent_developer_instructions() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_overrides_parent_developer_instructions() {
let mut parent_config = test_config().await;
parent_config.developer_instructions =
Some("parent or managed config should not replace guardian policy".to_string());
@@ -1021,9 +1021,9 @@ fn guardian_review_session_config_overrides_parent_developer_instructions() {
);
}
#[test]
fn guardian_review_session_config_uses_live_network_proxy_state() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_uses_live_network_proxy_state() {
let mut parent_config = test_config().await;
let mut parent_network = NetworkProxyConfig::default();
parent_network.network.enabled = true;
parent_network.network.allowed_domains = vec!["parent.example".to_string()];
@@ -1061,9 +1061,9 @@ fn guardian_review_session_config_uses_live_network_proxy_state() {
);
}
#[test]
fn guardian_review_session_config_rejects_pinned_collab_feature() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_rejects_pinned_collab_feature() {
let mut parent_config = test_config().await;
parent_config.features = ManagedFeatures::from_configured(
parent_config.features.get().clone(),
Some(Sourced {
@@ -1085,9 +1085,9 @@ fn guardian_review_session_config_rejects_pinned_collab_feature() {
);
}
#[test]
fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_slug() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_slug() {
let mut parent_config = test_config().await;
parent_config.model = Some("configured-model".to_string());
let guardian_config =
@@ -1097,8 +1097,8 @@ fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_
assert_eq!(guardian_config.model, Some("active-model".to_string()));
}
#[test]
fn guardian_review_session_config_uses_requirements_guardian_override() {
#[tokio::test]
async fn guardian_review_session_config_uses_requirements_guardian_override() {
let codex_home = tempfile::tempdir().expect("create temp dir");
let workspace = tempfile::tempdir().expect("create temp dir");
let config_layer_stack = ConfigLayerStack::new(
@@ -1121,6 +1121,7 @@ fn guardian_review_session_config_uses_requirements_guardian_override() {
codex_home.path().to_path_buf(),
config_layer_stack,
)
.await
.expect("load config");
let guardian_config =
@@ -1133,8 +1134,9 @@ fn guardian_review_session_config_uses_requirements_guardian_override() {
);
}
#[test]
fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override() {
#[tokio::test]
async fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override()
{
let codex_home = tempfile::tempdir().expect("create temp dir");
let workspace = tempfile::tempdir().expect("create temp dir");
let config_layer_stack =
@@ -1149,6 +1151,7 @@ fn guardian_review_session_config_uses_default_guardian_policy_without_requireme
codex_home.path().to_path_buf(),
config_layer_stack,
)
.await
.expect("load config");
let guardian_config =

View File

@@ -143,9 +143,9 @@ fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() {
);
}
#[test]
fn codex_apps_mcp_url_uses_legacy_codex_apps_path() {
let mut config = crate::config::test_config();
#[tokio::test]
async fn codex_apps_mcp_url_uses_legacy_codex_apps_path() {
let mut config = crate::config::test_config().await;
config.chatgpt_base_url = "https://chatgpt.com".to_string();
assert_eq!(
@@ -154,9 +154,9 @@ fn codex_apps_mcp_url_uses_legacy_codex_apps_path() {
);
}
#[test]
fn codex_apps_server_config_uses_legacy_codex_apps_path() {
let mut config = crate::config::test_config();
#[tokio::test]
async fn codex_apps_server_config_uses_legacy_codex_apps_path() {
let mut config = crate::config::test_config().await;
config.chatgpt_base_url = "https://chatgpt.com".to_string();
let mut servers = with_codex_apps_mcp(HashMap::new(), false, None, &config);

View File

@@ -467,7 +467,7 @@ mod phase2 {
impl DispatchHarness {
async fn new() -> Self {
let codex_home = tempfile::tempdir().expect("create temp codex home");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = codex_home.path().to_path_buf();
config.cwd = config.codex_home.clone();
let config = Arc::new(config);
@@ -880,7 +880,7 @@ mod phase2 {
#[tokio::test]
async fn dispatch_marks_job_for_retry_when_spawn_agent_fails() {
let codex_home = tempfile::tempdir().expect("create temp codex home");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = codex_home.path().to_path_buf();
config.cwd = config.codex_home.clone();
let config = Arc::new(config);

View File

@@ -2,10 +2,10 @@ use super::*;
use crate::config::test_config;
use pretty_assertions::assert_eq;
#[test]
fn reasoning_summaries_override_true_enables_support() {
#[tokio::test]
async fn reasoning_summaries_override_true_enables_support() {
let model = model_info_from_slug("unknown-model");
let mut config = test_config();
let mut config = test_config().await;
config.model_supports_reasoning_summaries = Some(true);
let updated = with_config_overrides(model.clone(), &config);
@@ -15,11 +15,11 @@ fn reasoning_summaries_override_true_enables_support() {
assert_eq!(updated, expected);
}
#[test]
fn reasoning_summaries_override_false_does_not_disable_support() {
#[tokio::test]
async fn reasoning_summaries_override_false_does_not_disable_support() {
let mut model = model_info_from_slug("unknown-model");
model.supports_reasoning_summaries = true;
let mut config = test_config();
let mut config = test_config().await;
config.model_supports_reasoning_summaries = Some(false);
let updated = with_config_overrides(model.clone(), &config);
@@ -27,10 +27,10 @@ fn reasoning_summaries_override_false_does_not_disable_support() {
assert_eq!(updated, model);
}
#[test]
fn reasoning_summaries_override_false_is_noop_when_model_is_false() {
#[tokio::test]
async fn reasoning_summaries_override_false_is_noop_when_model_is_false() {
let model = model_info_from_slug("unknown-model");
let mut config = test_config();
let mut config = test_config().await;
config.model_supports_reasoning_summaries = Some(false);
let updated = with_config_overrides(model.clone(), &config);

View File

@@ -5,9 +5,9 @@ use crate::models_manager::manager::ModelsManager;
use codex_features::Features;
use pretty_assertions::assert_eq;
#[test]
fn image_detail_original_feature_enables_explicit_original_without_force() {
let config = test_config();
#[tokio::test]
async fn image_detail_original_feature_enables_explicit_original_without_force() {
let config = test_config().await;
let mut model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
model_info.supports_image_detail_original = true;
@@ -25,9 +25,9 @@ fn image_detail_original_feature_enables_explicit_original_without_force() {
);
}
#[test]
fn explicit_original_is_dropped_without_feature_or_model_support() {
let config = test_config();
#[tokio::test]
async fn explicit_original_is_dropped_without_feature_or_model_support() {
let config = test_config().await;
let mut model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
model_info.supports_image_detail_original = true;
@@ -47,9 +47,9 @@ fn explicit_original_is_dropped_without_feature_or_model_support() {
);
}
#[test]
fn unsupported_non_original_detail_is_dropped() {
let config = test_config();
#[tokio::test]
async fn unsupported_non_original_detail_is_dropped() {
let config = test_config().await;
let mut model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
model_info.supports_image_detail_original = true;

View File

@@ -57,8 +57,8 @@ pub(crate) async fn build_realtime_startup_context(
let history = sess.clone_history().await;
let current_thread_section = build_current_thread_section(history.raw_items());
let recent_threads = load_recent_threads(sess).await;
let recent_work_section = build_recent_work_section(&cwd, &recent_threads);
let workspace_section = build_workspace_section_with_user_root(&cwd, home_dir());
let recent_work_section = build_recent_work_section(&cwd, &recent_threads).await;
let workspace_section = build_workspace_section_with_user_root(&cwd, home_dir()).await;
if current_thread_section.is_none()
&& recent_work_section.is_none()
@@ -141,46 +141,57 @@ async fn load_recent_threads(sess: &Session) -> Vec<ThreadMetadata> {
}
}
fn build_recent_work_section(cwd: &Path, recent_threads: &[ThreadMetadata]) -> Option<String> {
let mut groups: HashMap<PathBuf, Vec<&ThreadMetadata>> = HashMap::new();
async fn build_recent_work_section(
cwd: &Path,
recent_threads: &[ThreadMetadata],
) -> Option<String> {
let mut groups: HashMap<PathBuf, (bool, Vec<&ThreadMetadata>)> = HashMap::new();
for entry in recent_threads {
let group =
resolve_root_git_project_for_trust(&entry.cwd).unwrap_or_else(|| entry.cwd.clone());
groups.entry(group).or_default().push(entry);
let git_root = resolve_root_git_project_for_trust(&entry.cwd).await;
let is_git_repo = git_root.is_some();
let group = git_root.unwrap_or_else(|| entry.cwd.clone());
let (group_is_git_repo, entries) = groups
.entry(group)
.or_insert_with(|| (is_git_repo, Vec::new()));
*group_is_git_repo |= is_git_repo;
entries.push(entry);
}
let current_group =
resolve_root_git_project_for_trust(cwd).unwrap_or_else(|| cwd.to_path_buf());
let current_group = resolve_root_git_project_for_trust(cwd)
.await
.unwrap_or_else(|| cwd.to_path_buf());
let mut groups = groups.into_iter().collect::<Vec<_>>();
groups.sort_by(|(left_group, left_entries), (right_group, right_entries)| {
let left_latest = left_entries
.iter()
.map(|entry| entry.updated_at)
.max()
.unwrap_or_else(Utc::now);
let right_latest = right_entries
.iter()
.map(|entry| entry.updated_at)
.max()
.unwrap_or_else(Utc::now);
(
*left_group != current_group,
Reverse(left_latest),
left_group.as_os_str(),
)
.cmp(&(
*right_group != current_group,
Reverse(right_latest),
right_group.as_os_str(),
))
});
groups.sort_by(
|(left_group, (_, left_entries)), (right_group, (_, right_entries))| {
let left_latest = left_entries
.iter()
.map(|entry| entry.updated_at)
.max()
.unwrap_or_else(Utc::now);
let right_latest = right_entries
.iter()
.map(|entry| entry.updated_at)
.max()
.unwrap_or_else(Utc::now);
(
*left_group != current_group,
Reverse(left_latest),
left_group.as_os_str(),
)
.cmp(&(
*right_group != current_group,
Reverse(right_latest),
right_group.as_os_str(),
))
},
);
let sections = groups
.into_iter()
.take(MAX_RECENT_WORK_GROUPS)
.filter_map(|(group, mut entries)| {
.filter_map(|(group, (is_git_repo, mut entries))| {
entries.sort_by_key(|entry| Reverse(entry.updated_at));
format_thread_group(&current_group, &group, entries)
format_thread_group(&current_group, &group, is_git_repo, entries)
})
.collect::<Vec<_>>();
(!sections.is_empty()).then(|| sections.join("\n\n"))
@@ -270,11 +281,11 @@ fn build_current_thread_section(items: &[ResponseItem]) -> Option<String> {
Some(lines.join("\n"))
}
fn build_workspace_section_with_user_root(
async fn build_workspace_section_with_user_root(
cwd: &Path,
user_root: Option<PathBuf>,
) -> Option<String> {
let git_root = resolve_root_git_project_for_trust(cwd);
let git_root = resolve_root_git_project_for_trust(cwd).await;
let cwd_tree = render_tree(cwd);
let git_root_tree = git_root
.as_ref()
@@ -412,10 +423,11 @@ fn format_section(title: &str, body: Option<String>, budget_tokens: usize) -> Op
fn format_thread_group(
current_group: &Path,
group: &Path,
is_git_repo: bool,
entries: Vec<&ThreadMetadata>,
) -> Option<String> {
let latest = entries.first()?;
let group_label = if resolve_root_git_project_for_trust(latest.cwd.as_path()).is_some() {
let group_label = if is_git_repo {
format!("### Git repo: {}", group.display())
} else {
format!("### Directory: {}", group.display())

View File

@@ -43,30 +43,31 @@ fn thread_metadata(cwd: &str, title: &str, first_user_message: &str) -> ThreadMe
}
}
#[test]
fn workspace_section_requires_meaningful_structure() {
#[tokio::test]
async fn workspace_section_requires_meaningful_structure() {
let cwd = TempDir::new().expect("tempdir");
assert_eq!(
build_workspace_section_with_user_root(cwd.path(), None),
build_workspace_section_with_user_root(cwd.path(), None).await,
None
);
}
#[test]
fn workspace_section_includes_tree_when_entries_exist() {
#[tokio::test]
async fn workspace_section_includes_tree_when_entries_exist() {
let cwd = TempDir::new().expect("tempdir");
fs::create_dir(cwd.path().join("docs")).expect("create docs dir");
fs::write(cwd.path().join("README.md"), "hello").expect("write readme");
let section =
build_workspace_section_with_user_root(cwd.path(), None).expect("workspace section");
let section = build_workspace_section_with_user_root(cwd.path(), None)
.await
.expect("workspace section");
assert!(section.contains("Working directory tree:"));
assert!(section.contains("- docs/"));
assert!(section.contains("- README.md"));
}
#[test]
fn workspace_section_includes_user_root_tree_when_distinct() {
#[tokio::test]
async fn workspace_section_includes_user_root_tree_when_distinct() {
let root = TempDir::new().expect("tempdir");
let cwd = root.path().join("cwd");
let git_root = root.path().join("git");
@@ -80,14 +81,15 @@ fn workspace_section_includes_user_root_tree_when_distinct() {
fs::write(user_root.join(".zshrc"), "export TEST=1").expect("write home file");
let section = build_workspace_section_with_user_root(cwd.as_path(), Some(user_root))
.await
.expect("workspace section");
assert!(section.contains("User root tree:"));
assert!(section.contains("- code/"));
assert!(!section.contains("- .zshrc"));
}
#[test]
fn recent_work_section_groups_threads_by_cwd() {
#[tokio::test]
async fn recent_work_section_groups_threads_by_cwd() {
let root = TempDir::new().expect("tempdir");
let repo = root.path().join("repo");
let workspace_a = repo.join("workspace-a");
@@ -123,6 +125,7 @@ fn recent_work_section_groups_threads_by_cwd() {
let repo = fs::canonicalize(repo).expect("canonicalize repo");
let section = build_recent_work_section(current_cwd.as_path(), &recent_threads)
.await
.expect("recent work section");
assert!(section.contains(&format!("### Git repo: {}", repo.display())));
assert!(section.contains("Recent sessions: 2"));

View File

@@ -197,7 +197,7 @@ async fn backfill_sessions_resumes_from_watermark_and_marks_complete() {
))
.await;
let mut config = crate::config::test_config();
let mut config = crate::config::test_config().await;
config.codex_home = codex_home.clone();
config.model_provider_id = "test-provider".to_string();
backfill_sessions(runtime.as_ref(), &config).await;
@@ -267,7 +267,7 @@ async fn backfill_sessions_preserves_existing_git_branch_and_fills_missing_git_f
.await
.expect("existing metadata upsert");
let mut config = crate::config::test_config();
let mut config = crate::config::test_config().await;
config.codex_home = codex_home.clone();
config.model_provider_id = "test-provider".to_string();
backfill_sessions(runtime.as_ref(), &config).await;
@@ -304,7 +304,7 @@ async fn backfill_sessions_normalizes_cwd_before_upsert() {
.await
.expect("initialize runtime");
let mut config = crate::config::test_config();
let mut config = crate::config::test_config().await;
config.codex_home = codex_home.clone();
config.model_provider_id = "test-provider".to_string();
backfill_sessions(runtime.as_ref(), &config).await;

View File

@@ -234,7 +234,7 @@ async fn ignores_session_prefix_messages_when_truncating() {
#[tokio::test]
async fn shutdown_all_threads_bounded_submits_shutdown_to_every_thread() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -273,7 +273,7 @@ async fn new_uses_configured_openai_provider_for_model_refresh() {
let models_mock = mount_models_once(&server, ModelsResponse { models: vec![] }).await;
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -406,7 +406,7 @@ fn mixed_response_and_legacy_user_event_history_is_mid_turn() {
#[tokio::test]
async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_history() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -503,7 +503,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
#[tokio::test]
async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -589,7 +589,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
#[tokio::test]
async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_source() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");

View File

@@ -54,8 +54,8 @@ fn windows_shell_safety_description() -> String {
format!("\n\n{}", super::windows_destructive_filesystem_guidance())
}
fn search_capable_model_info() -> ModelInfo {
let config = test_config();
async fn search_capable_model_info() -> ModelInfo {
let config = test_config().await;
let mut model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
model_info.supports_search_tool = true;
@@ -358,8 +358,8 @@ fn strip_descriptions_tool(spec: &mut ToolSpec) {
}
}
fn model_info_from_models_json(slug: &str) -> ModelInfo {
let config = test_config();
async fn model_info_from_models_json(slug: &str) -> ModelInfo {
let config = test_config().await;
let response: ModelsResponse =
serde_json::from_str(include_str!("../../models.json")).expect("valid models.json");
let model = response
@@ -394,9 +394,9 @@ fn unified_exec_is_blocked_for_windows_sandboxed_policies_only() {
));
}
#[test]
fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
let mut model_info = model_info_from_models_json("gpt-5-codex");
#[tokio::test]
async fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
let mut model_info = model_info_from_models_json("gpt-5-codex").await;
model_info.shell_type = ConfigShellToolType::UnifiedExec;
let features = Features::with_defaults();
let available_models = Vec::new();
@@ -418,9 +418,9 @@ fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
assert_eq!(config.shell_type, expected_shell_type);
}
#[test]
fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
let model_info = model_info_from_models_json("gpt-5-codex");
#[tokio::test]
async fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
let model_info = model_info_from_models_json("gpt-5-codex").await;
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
let available_models = Vec::new();
@@ -503,9 +503,9 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
}
}
#[test]
fn test_build_specs_collab_tools_enabled() {
let config = test_config();
#[tokio::test]
async fn test_build_specs_collab_tools_enabled() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Collab);
@@ -528,9 +528,9 @@ fn test_build_specs_collab_tools_enabled() {
assert_lacks_tool_name(&tools, "list_agents");
}
#[test]
fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
let config = test_config();
#[tokio::test]
async fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Collab);
@@ -680,9 +680,9 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
assert_lacks_tool_name(&tools, "resume_agent");
}
#[test]
fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() {
let config = test_config();
#[tokio::test]
async fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::SpawnCsv);
@@ -710,9 +710,9 @@ fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() {
);
}
#[test]
fn view_image_tool_omits_detail_without_original_detail_feature() {
let config = test_config();
#[tokio::test]
async fn view_image_tool_omits_detail_without_original_detail_feature() {
let config = test_config().await;
let mut model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
model_info.supports_image_detail_original = true;
@@ -738,9 +738,9 @@ fn view_image_tool_omits_detail_without_original_detail_feature() {
assert!(!properties.contains_key("detail"));
}
#[test]
fn view_image_tool_includes_detail_with_original_detail_feature() {
let config = test_config();
#[tokio::test]
async fn view_image_tool_includes_detail_with_original_detail_feature() {
let config = test_config().await;
let mut model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
model_info.supports_image_detail_original = true;
@@ -775,9 +775,9 @@ fn view_image_tool_includes_detail_with_original_detail_feature() {
assert!(description.contains("omit this field for default resized behavior"));
}
#[test]
fn test_build_specs_artifact_tool_enabled() {
let mut config = test_config();
#[tokio::test]
async fn test_build_specs_artifact_tool_enabled() {
let mut config = test_config().await;
let runtime_root = tempfile::TempDir::new().expect("create temp codex home");
config.codex_home = runtime_root.path().to_path_buf();
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
@@ -797,9 +797,9 @@ fn test_build_specs_artifact_tool_enabled() {
assert_contains_tool_names(&tools, &["artifacts"]);
}
#[test]
fn test_build_specs_agent_job_worker_tools_enabled() {
let config = test_config();
#[tokio::test]
async fn test_build_specs_agent_job_worker_tools_enabled() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::SpawnCsv);
@@ -833,9 +833,9 @@ fn test_build_specs_agent_job_worker_tools_enabled() {
assert_lacks_tool_name(&tools, "request_user_input");
}
#[test]
fn request_user_input_description_reflects_default_mode_feature_flag() {
let config = test_config();
#[tokio::test]
async fn request_user_input_description_reflects_default_mode_feature_flag() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
let available_models = Vec::new();
@@ -876,9 +876,9 @@ fn request_user_input_description_reflects_default_mode_feature_flag() {
);
}
#[test]
fn request_permissions_requires_feature_flag() {
let config = test_config();
#[tokio::test]
async fn request_permissions_requires_feature_flag() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let features = Features::with_defaults();
let available_models = Vec::new();
@@ -914,9 +914,9 @@ fn request_permissions_requires_feature_flag() {
);
}
#[test]
fn request_permissions_tool_is_independent_from_additional_permissions() {
let config = test_config();
#[tokio::test]
async fn request_permissions_tool_is_independent_from_additional_permissions() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::ExecPermissionApprovals);
@@ -935,9 +935,9 @@ fn request_permissions_tool_is_independent_from_additional_permissions() {
assert_lacks_tool_name(&tools, "request_permissions");
}
#[test]
fn get_memory_requires_feature_flag() {
let config = test_config();
#[tokio::test]
async fn get_memory_requires_feature_flag() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.disable(Feature::MemoryTool);
@@ -958,9 +958,9 @@ fn get_memory_requires_feature_flag() {
);
}
#[test]
fn js_repl_requires_feature_flag() {
let config = test_config();
#[tokio::test]
async fn js_repl_requires_feature_flag() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let features = Features::with_defaults();
@@ -986,9 +986,9 @@ fn js_repl_requires_feature_flag() {
);
}
#[test]
fn js_repl_enabled_adds_tools() {
let config = test_config();
#[tokio::test]
async fn js_repl_enabled_adds_tools() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::JsRepl);
@@ -1007,9 +1007,9 @@ fn js_repl_enabled_adds_tools() {
assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]);
}
#[test]
fn image_generation_tools_require_feature_and_supported_model() {
let config = test_config();
#[tokio::test]
async fn image_generation_tools_require_feature_and_supported_model() {
let config = test_config().await;
let mut supported_model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5.2", &config);
supported_model_info.slug = "custom/gpt-5.2-variant".to_string();
@@ -1090,14 +1090,14 @@ fn js_repl_freeform_grammar_blocks_common_non_js_prefixes() {
assert!(!format.definition.contains("(?!"));
}
fn assert_model_tools(
async fn assert_model_tools(
model_slug: &str,
features: &Features,
web_search_mode: Option<WebSearchMode>,
expected_tools: &[&str],
) {
let _config = test_config();
let model_info = model_info_from_models_json(model_slug);
let _config = test_config().await;
let model_info = model_info_from_models_json(model_slug).await;
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
@@ -1125,7 +1125,7 @@ fn assert_model_tools(
assert_eq!(&tool_names, &expected_tools,);
}
fn assert_default_model_tools(
async fn assert_default_model_tools(
model_slug: &str,
features: &Features,
web_search_mode: Option<WebSearchMode>,
@@ -1138,12 +1138,12 @@ fn assert_default_model_tools(
vec![shell_tool]
};
expected.extend(expected_tail);
assert_model_tools(model_slug, features, web_search_mode, &expected);
assert_model_tools(model_slug, features, web_search_mode, &expected).await;
}
#[test]
fn web_search_mode_cached_sets_external_web_access_false() {
let config = test_config();
#[tokio::test]
async fn web_search_mode_cached_sets_external_web_access_false() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let features = Features::with_defaults();
@@ -1172,9 +1172,9 @@ fn web_search_mode_cached_sets_external_web_access_false() {
);
}
#[test]
fn web_search_mode_live_sets_external_web_access_true() {
let config = test_config();
#[tokio::test]
async fn web_search_mode_live_sets_external_web_access_true() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let features = Features::with_defaults();
@@ -1203,9 +1203,9 @@ fn web_search_mode_live_sets_external_web_access_true() {
);
}
#[test]
fn web_search_config_is_forwarded_to_tool_spec() {
let config = test_config();
#[tokio::test]
async fn web_search_config_is_forwarded_to_tool_spec() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let features = Features::with_defaults();
let web_search_config = WebSearchConfig {
@@ -1252,9 +1252,9 @@ fn web_search_config_is_forwarded_to_tool_spec() {
);
}
#[test]
fn web_search_tool_type_text_and_image_sets_search_content_types() {
let config = test_config();
#[tokio::test]
async fn web_search_tool_type_text_and_image_sets_search_content_types() {
let config = test_config().await;
let mut model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
model_info.web_search_tool_type = WebSearchToolType::TextAndImage;
@@ -1290,9 +1290,9 @@ fn web_search_tool_type_text_and_image_sets_search_content_types() {
);
}
#[test]
fn mcp_resource_tools_are_hidden_without_mcp_servers() {
let config = test_config();
#[tokio::test]
async fn mcp_resource_tools_are_hidden_without_mcp_servers() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let features = Features::with_defaults();
let available_models = Vec::new();
@@ -1316,9 +1316,9 @@ fn mcp_resource_tools_are_hidden_without_mcp_servers() {
);
}
#[test]
fn mcp_resource_tools_are_included_when_mcp_servers_are_present() {
let config = test_config();
#[tokio::test]
async fn mcp_resource_tools_are_included_when_mcp_servers_are_present() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let features = Features::with_defaults();
let available_models = Vec::new();
@@ -1343,8 +1343,8 @@ fn mcp_resource_tools_are_included_when_mcp_servers_are_present() {
);
}
#[test]
fn test_build_specs_gpt5_codex_default() {
#[tokio::test]
async fn test_build_specs_gpt5_codex_default() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5-codex",
@@ -1363,11 +1363,12 @@ fn test_build_specs_gpt5_codex_default() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_build_specs_gpt51_codex_default() {
#[tokio::test]
async fn test_build_specs_gpt51_codex_default() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5.1-codex",
@@ -1386,11 +1387,12 @@ fn test_build_specs_gpt51_codex_default() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_build_specs_gpt5_codex_unified_exec_web_search() {
#[tokio::test]
async fn test_build_specs_gpt5_codex_unified_exec_web_search() {
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
assert_model_tools(
@@ -1411,11 +1413,12 @@ fn test_build_specs_gpt5_codex_unified_exec_web_search() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_build_specs_gpt51_codex_unified_exec_web_search() {
#[tokio::test]
async fn test_build_specs_gpt51_codex_unified_exec_web_search() {
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
assert_model_tools(
@@ -1436,11 +1439,12 @@ fn test_build_specs_gpt51_codex_unified_exec_web_search() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_gpt_5_1_codex_max_defaults() {
#[tokio::test]
async fn test_gpt_5_1_codex_max_defaults() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5.1-codex-max",
@@ -1459,11 +1463,12 @@ fn test_gpt_5_1_codex_max_defaults() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_codex_5_1_mini_defaults() {
#[tokio::test]
async fn test_codex_5_1_mini_defaults() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5.1-codex-mini",
@@ -1482,11 +1487,12 @@ fn test_codex_5_1_mini_defaults() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_gpt_5_defaults() {
#[tokio::test]
async fn test_gpt_5_defaults() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5",
@@ -1504,11 +1510,12 @@ fn test_gpt_5_defaults() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_gpt_5_1_defaults() {
#[tokio::test]
async fn test_gpt_5_1_defaults() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5.1",
@@ -1527,11 +1534,12 @@ fn test_gpt_5_1_defaults() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_gpt_5_1_codex_max_unified_exec_web_search() {
#[tokio::test]
async fn test_gpt_5_1_codex_max_unified_exec_web_search() {
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
assert_model_tools(
@@ -1552,12 +1560,13 @@ fn test_gpt_5_1_codex_max_unified_exec_web_search() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_build_specs_default_shell_present() {
let config = test_config();
#[tokio::test]
async fn test_build_specs_default_shell_present() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1581,9 +1590,9 @@ fn test_build_specs_default_shell_present() {
assert_contains_tool_names(&tools, &subset);
}
#[test]
fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
let config = test_config();
#[tokio::test]
async fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1644,10 +1653,10 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
);
}
#[test]
#[tokio::test]
#[ignore]
fn test_parallel_support_flags() {
let config = test_config();
async fn test_parallel_support_flags() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1670,10 +1679,10 @@ fn test_parallel_support_flags() {
assert!(find_tool(&tools, "read_file").supports_parallel_tool_calls);
}
#[test]
fn test_test_model_info_includes_sync_tool() {
let _config = test_config();
let mut model_info = model_info_from_models_json("gpt-5-codex");
#[tokio::test]
async fn test_test_model_info_includes_sync_tool() {
let _config = test_config().await;
let mut model_info = model_info_from_models_json("gpt-5-codex").await;
model_info.experimental_supported_tools = vec![
"test_sync_tool".to_string(),
"read_file".to_string(),
@@ -1711,9 +1720,9 @@ fn test_test_model_info_includes_sync_tool() {
assert!(tools.iter().any(|tool| tool_name(&tool.spec) == "list_dir"));
}
#[test]
fn test_build_specs_mcp_tools_converted() {
let config = test_config();
#[tokio::test]
async fn test_build_specs_mcp_tools_converted() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1804,9 +1813,9 @@ fn test_build_specs_mcp_tools_converted() {
);
}
#[test]
fn test_build_specs_mcp_tools_sorted_by_name() {
let config = test_config();
#[tokio::test]
async fn test_build_specs_mcp_tools_sorted_by_name() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1853,9 +1862,9 @@ fn test_build_specs_mcp_tools_sorted_by_name() {
assert_eq!(mcp_names, expected);
}
#[test]
fn search_tool_description_lists_each_codex_apps_connector_once() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_description_lists_each_codex_apps_connector_once() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
let available_models = Vec::new();
@@ -1975,9 +1984,9 @@ fn search_tool_description_lists_each_codex_apps_connector_once() {
assert!(!description.contains("mcp__rmcp__echo"));
}
#[test]
fn search_tool_requires_model_capability_only() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_requires_model_capability_only() {
let model_info = search_capable_model_info().await;
let app_tools = Some(HashMap::from([(
"mcp__codex_apps__calendar_create_event".to_string(),
ToolInfo {
@@ -2026,9 +2035,9 @@ fn search_tool_requires_model_capability_only() {
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]);
}
#[test]
fn tool_suggest_is_not_registered_without_feature_flag() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn tool_suggest_is_not_registered_without_feature_flag() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
features.enable(Feature::Plugins);
@@ -2062,9 +2071,9 @@ fn tool_suggest_is_not_registered_without_feature_flag() {
);
}
#[test]
fn tool_suggest_requires_apps_and_plugins_features() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn tool_suggest_requires_apps_and_plugins_features() {
let model_info = search_capable_model_info().await;
let discoverable_tools = Some(vec![discoverable_connector(
"connector_2128aebfecb84f64a069897515042a44",
"Google Calendar",
@@ -2107,10 +2116,9 @@ fn tool_suggest_requires_apps_and_plugins_features() {
);
}
}
#[test]
fn search_tool_description_handles_no_enabled_apps() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_description_handles_no_enabled_apps() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
let available_models = Vec::new();
@@ -2134,9 +2142,9 @@ fn search_tool_description_handles_no_enabled_apps() {
assert!(!description.contains("{{app_descriptions}}"));
}
#[test]
fn search_tool_description_falls_back_to_connector_name_without_description() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_description_falls_back_to_connector_name_without_description() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
let available_models = Vec::new();
@@ -2182,9 +2190,9 @@ fn search_tool_description_falls_back_to_connector_name_without_description() {
assert!(!description.contains("- Calendar:"));
}
#[test]
fn search_tool_registers_namespaced_app_tool_aliases() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_registers_namespaced_app_tool_aliases() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
let available_models = Vec::new();
@@ -2247,9 +2255,9 @@ fn search_tool_registers_namespaced_app_tool_aliases() {
assert!(registry.has_handler(alias.as_str(), None));
}
#[test]
fn tool_suggest_description_lists_discoverable_tools() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn tool_suggest_description_lists_discoverable_tools() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
features.enable(Feature::Plugins);
@@ -2329,9 +2337,9 @@ fn tool_suggest_description_lists_discoverable_tools() {
);
}
#[test]
fn test_mcp_tool_property_missing_type_defaults_to_string() {
let config = test_config();
#[tokio::test]
async fn test_mcp_tool_property_missing_type_defaults_to_string() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -2389,9 +2397,9 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() {
);
}
#[test]
fn test_mcp_tool_integer_normalized_to_number() {
let config = test_config();
#[tokio::test]
async fn test_mcp_tool_integer_normalized_to_number() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -2445,9 +2453,9 @@ fn test_mcp_tool_integer_normalized_to_number() {
);
}
#[test]
fn test_mcp_tool_array_without_items_gets_default_string_items() {
let config = test_config();
#[tokio::test]
async fn test_mcp_tool_array_without_items_gets_default_string_items() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -2505,9 +2513,9 @@ fn test_mcp_tool_array_without_items_gets_default_string_items() {
);
}
#[test]
fn test_mcp_tool_anyof_defaults_to_string() {
let config = test_config();
#[tokio::test]
async fn test_mcp_tool_anyof_defaults_to_string() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -2730,9 +2738,9 @@ Examples of valid command strings:
assert_eq!(description, &expected);
}
#[test]
fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
let config = test_config();
#[tokio::test]
async fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -2840,9 +2848,9 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
);
}
#[test]
fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() {
let config = test_config();
#[tokio::test]
async fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
@@ -2871,9 +2879,9 @@ fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() {
);
}
#[test]
fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
let config = test_config();
#[tokio::test]
async fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
@@ -2923,8 +2931,8 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
);
}
#[test]
fn code_mode_only_restricts_model_tools_to_exec_tools() {
#[tokio::test]
async fn code_mode_only_restricts_model_tools_to_exec_tools() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
features.enable(Feature::CodeModeOnly);
@@ -2934,12 +2942,13 @@ fn code_mode_only_restricts_model_tools_to_exec_tools() {
&features,
Some(WebSearchMode::Live),
&["exec", "wait"],
);
)
.await;
}
#[test]
fn code_mode_only_exec_description_includes_full_nested_tool_details() {
let config = test_config();
#[tokio::test]
async fn code_mode_only_exec_description_includes_full_nested_tool_details() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
@@ -2970,9 +2979,9 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() {
assert!(description.contains("### `view_image` (`view_image`)"));
}
#[test]
fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() {
let config = test_config();
#[tokio::test]
async fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);

View File

@@ -2,9 +2,9 @@ use std::sync::Arc;
use crate::ExecServerClient;
use crate::ExecServerError;
use crate::LOCAL_FS;
use crate::RemoteExecServerConnectArgs;
use crate::file_system::ExecutorFileSystem;
use crate::local_file_system::LocalFileSystem;
use crate::local_process::LocalProcess;
use crate::process::ExecProcess;
use crate::remote_file_system::RemoteFileSystem;
@@ -101,7 +101,7 @@ impl Environment {
if let Some(client) = self.remote_exec_server_client.clone() {
Arc::new(RemoteFileSystem::new(client))
} else {
Arc::new(LocalFileSystem)
Arc::new(LOCAL_FS)
}
}
}

View File

@@ -39,6 +39,11 @@ pub type FileSystemResult<T> = io::Result<T>;
pub trait ExecutorFileSystem: Send + Sync {
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>>;
async fn read_to_string(&self, path: &AbsolutePathBuf) -> FileSystemResult<String> {
let bytes = self.read_file(path).await?;
String::from_utf8(bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
}
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()>;
async fn create_directory(

View File

@@ -39,6 +39,7 @@ pub use file_system::FileMetadata;
pub use file_system::FileSystemResult;
pub use file_system::ReadDirectoryEntry;
pub use file_system::RemoveOptions;
pub use local_file_system::LocalFileSystem;
pub use process::ExecProcess;
pub use process::ExecServerEvent;
pub use protocol::ExecExitedNotification;
@@ -58,3 +59,5 @@ pub use server::DEFAULT_LISTEN_URL;
pub use server::ExecServerListenUrlParseError;
pub use server::run_main;
pub use server::run_main_with_listen_url;
pub const LOCAL_FS: LocalFileSystem = LocalFileSystem;

View File

@@ -18,7 +18,7 @@ use crate::RemoveOptions;
const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024;
#[derive(Clone, Default)]
pub(crate) struct LocalFileSystem;
pub struct LocalFileSystem;
#[async_trait]
impl ExecutorFileSystem for LocalFileSystem {

View File

@@ -618,7 +618,7 @@ async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option<String> {
/// `[get_git_repo_root]`, but resolves to the root of the main
/// repository. Handles worktrees via filesystem inspection without invoking
/// the `git` executable.
pub fn resolve_root_git_project_for_trust(cwd: &Path) -> Option<PathBuf> {
pub async fn resolve_root_git_project_for_trust(cwd: &Path) -> Option<PathBuf> {
let base = if cwd.is_dir() { cwd } else { cwd.parent()? };
let (repo_root, dot_git) = find_ancestor_git_entry(base)?;
if dot_git.is_dir() {

View File

@@ -2,6 +2,7 @@ use codex_core::AuthManager;
use codex_core::config::Config;
#[cfg(target_os = "windows")]
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_git_utils::resolve_root_git_project_for_trust;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use crossterm::event::KeyCode;
@@ -74,7 +75,7 @@ pub(crate) struct OnboardingResult {
}
impl OnboardingScreen {
pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
pub(crate) async fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
let OnboardingScreenArgs {
show_trust_screen,
show_login_screen,
@@ -117,15 +118,18 @@ impl OnboardingScreen {
WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled;
#[cfg(not(target_os = "windows"))]
let show_windows_create_sandbox_hint = false;
let highlighted = TrustDirectorySelection::Trust;
if show_trust_screen {
let trust_target = resolve_root_git_project_for_trust(&cwd)
.await
.unwrap_or_else(|| cwd.clone());
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
cwd,
codex_home,
cwd,
trust_target,
show_windows_create_sandbox_hint,
should_quit: false,
selection: None,
highlighted,
highlighted: TrustDirectorySelection::Trust,
error: None,
}))
}
@@ -398,7 +402,7 @@ pub(crate) async fn run_onboarding_app(
) -> Result<OnboardingResult> {
use tokio_stream::StreamExt;
let mut onboarding_screen = OnboardingScreen::new(tui, args);
let mut onboarding_screen = OnboardingScreen::new(tui, args).await;
// One-time guard to fully clear the screen after ChatGPT login success message is shown
let mut did_full_clear_after_success = false;

View File

@@ -1,7 +1,6 @@
use std::path::PathBuf;
use codex_core::config::set_project_trust_level;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_protocol::config_types::TrustLevel;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -27,6 +26,7 @@ use super::onboarding_screen::StepState;
pub(crate) struct TrustDirectoryWidget {
pub codex_home: PathBuf,
pub cwd: PathBuf,
pub trust_target: PathBuf,
pub show_windows_create_sandbox_hint: bool,
pub should_quit: bool,
pub selection: Option<TrustDirectorySelection>,
@@ -142,13 +142,18 @@ impl StepStateProvider for TrustDirectoryWidget {
impl TrustDirectoryWidget {
fn handle_trust(&mut self) {
let target =
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) {
if let Err(e) =
set_project_trust_level(&self.codex_home, &self.trust_target, TrustLevel::Trusted)
{
tracing::error!("Failed to set project trusted: {e:?}");
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
self.error = Some(format!(
"Failed to set trust for {}: {e}",
self.trust_target.display()
));
return;
}
self.error = None;
self.selection = Some(TrustDirectorySelection::Trust);
}
@@ -182,6 +187,7 @@ mod tests {
let mut widget = TrustDirectoryWidget {
codex_home: codex_home.path().to_path_buf(),
cwd: PathBuf::from("."),
trust_target: PathBuf::from("."),
show_windows_create_sandbox_hint: false,
should_quit: false,
selection: None,
@@ -207,6 +213,7 @@ mod tests {
let widget = TrustDirectoryWidget {
codex_home: codex_home.path().to_path_buf(),
cwd: PathBuf::from("/workspace/project"),
trust_target: PathBuf::from("/workspace/project"),
show_windows_create_sandbox_hint: false,
should_quit: false,
selection: None,

View File

@@ -4,6 +4,7 @@ use codex_app_server_protocol::ServerNotification;
use codex_core::config::Config;
#[cfg(target_os = "windows")]
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_git_utils::resolve_root_git_project_for_trust;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use crossterm::event::KeyCode;
@@ -77,7 +78,7 @@ pub(crate) struct OnboardingResult {
}
impl OnboardingScreen {
pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
pub(crate) async fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
let OnboardingScreenArgs {
show_trust_screen,
show_login_screen,
@@ -124,15 +125,18 @@ impl OnboardingScreen {
WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled;
#[cfg(not(target_os = "windows"))]
let show_windows_create_sandbox_hint = false;
let highlighted = TrustDirectorySelection::Trust;
if show_trust_screen {
let trust_target = resolve_root_git_project_for_trust(&cwd)
.await
.unwrap_or_else(|| cwd.clone());
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
cwd,
codex_home,
cwd,
trust_target,
show_windows_create_sandbox_hint,
should_quit: false,
selection: None,
highlighted,
highlighted: TrustDirectorySelection::Trust,
error: None,
}))
}
@@ -438,7 +442,7 @@ pub(crate) async fn run_onboarding_app(
) -> Result<OnboardingResult> {
use tokio_stream::StreamExt;
let mut onboarding_screen = OnboardingScreen::new(tui, args);
let mut onboarding_screen = OnboardingScreen::new(tui, args).await;
// One-time guard to fully clear the screen after ChatGPT login success message is shown
let mut did_full_clear_after_success = false;

View File

@@ -1,7 +1,6 @@
use std::path::PathBuf;
use codex_core::config::set_project_trust_level;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_protocol::config_types::TrustLevel;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -27,6 +26,7 @@ use super::onboarding_screen::StepState;
pub(crate) struct TrustDirectoryWidget {
pub codex_home: PathBuf,
pub cwd: PathBuf,
pub trust_target: PathBuf,
pub show_windows_create_sandbox_hint: bool,
pub should_quit: bool,
pub selection: Option<TrustDirectorySelection>,
@@ -142,13 +142,18 @@ impl StepStateProvider for TrustDirectoryWidget {
impl TrustDirectoryWidget {
fn handle_trust(&mut self) {
let target =
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) {
if let Err(e) =
set_project_trust_level(&self.codex_home, &self.trust_target, TrustLevel::Trusted)
{
tracing::error!("Failed to set project trusted: {e:?}");
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
self.error = Some(format!(
"Failed to set trust for {}: {e}",
self.trust_target.display()
));
return;
}
self.error = None;
self.selection = Some(TrustDirectorySelection::Trust);
}
@@ -182,6 +187,7 @@ mod tests {
let mut widget = TrustDirectoryWidget {
codex_home: codex_home.path().to_path_buf(),
cwd: PathBuf::from("."),
trust_target: PathBuf::from("."),
show_windows_create_sandbox_hint: false,
should_quit: false,
selection: None,
@@ -207,6 +213,7 @@ mod tests {
let widget = TrustDirectoryWidget {
codex_home: codex_home.path().to_path_buf(),
cwd: PathBuf::from("/workspace/project"),
trust_target: PathBuf::from("/workspace/project"),
show_windows_create_sandbox_hint: false,
should_quit: false,
selection: None,