Async config loading (#18022)

Parts of config will come from executor. Prepare for that by making
config loading methods async.
This commit is contained in:
pakrym-oai
2026-04-15 19:18:38 -07:00
committed by GitHub
Unverified
parent d97bad1272
commit bd61737e8a
25 changed files with 624 additions and 471 deletions
+1
View File
@@ -971,6 +971,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"),
}
}
@@ -2442,6 +2442,7 @@ impl CodexMessageProcessor {
))
{
let trust_target = resolve_root_git_project_for_trust(config.cwd.as_path())
.await
.unwrap_or_else(|| config.cwd.to_path_buf());
let cli_overrides_with_trust;
let cli_overrides_for_reload = if let Err(err) =
+1
View File
@@ -716,6 +716,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"),
}
}
+8 -6
View File
@@ -432,12 +432,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}"),
)
})?
}
};
@@ -717,6 +717,7 @@ model_reasoning_effort = "high"
let config_toml = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
let trusted_root = resolve_root_git_project_for_trust(workspace.path())
.await
.unwrap_or_else(|| workspace.path().to_path_buf());
assert!(config_toml.contains(&persisted_trust_path(&trusted_root)));
assert!(config_toml.contains("trust_level = \"trusted\""));
@@ -753,8 +754,9 @@ async fn thread_start_with_nested_git_cwd_trusts_repo_root() -> Result<()> {
.await??;
let config_toml = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
let trusted_root =
resolve_root_git_project_for_trust(&nested).expect("git root should resolve");
let trusted_root = resolve_root_git_project_for_trust(&nested)
.await
.expect("git root should resolve");
assert!(config_toml.contains(&persisted_trust_path(&trusted_root)));
assert!(!config_toml.contains(&persisted_trust_path(&nested)));
+9 -6
View File
@@ -596,7 +596,7 @@ pub struct GhostSnapshotToml {
impl ConfigToml {
/// Derive the effective sandbox policy from the configuration.
pub fn derive_sandbox_policy(
pub async fn derive_sandbox_policy(
&self,
sandbox_mode_override: Option<SandboxMode>,
profile_sandbox_mode: Option<SandboxMode>,
@@ -610,11 +610,13 @@ impl ConfigToml {
let resolved_sandbox_mode = sandbox_mode_override
.or(profile_sandbox_mode)
.or(self.sandbox_mode)
.or_else(|| {
.or(if sandbox_mode_was_explicit {
None
} 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| {
self.get_active_project(resolved_cwd).await.and_then(|p| {
if p.is_trusted() || p.is_untrusted() {
if cfg!(target_os = "windows")
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
@@ -676,7 +678,8 @@ 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 repo_root = resolve_root_git_project_for_trust(resolved_cwd).await;
let projects = self.projects.clone().unwrap_or_default();
let resolved_cwd_key = project_trust_key(resolved_cwd);
@@ -691,8 +694,8 @@ 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) {
let repo_root_key = project_trust_key(repo_root.as_path());
if let Some(repo_root) = repo_root.as_deref() {
let repo_root_key = project_trust_key(repo_root);
let repo_root_raw_key = repo_root.to_string_lossy().to_string();
if let Some(project_config_for_root) = projects
.get(&repo_root_key)
+5 -3
View File
@@ -78,7 +78,8 @@ async fn apply_role_to_config_inner(
role_layer_toml,
preserve_current_profile,
preserve_current_provider,
)?;
)
.await?;
Ok(())
}
@@ -150,7 +151,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,
@@ -171,7 +172,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();
}
+15 -15
View File
@@ -457,8 +457,8 @@ fn numbered_mcp_tools(count: usize) -> HashMap<String, ToolInfo> {
.collect()
}
fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
let config = test_config();
async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests(
"gpt-5-codex",
&config.to_models_manager_config(),
@@ -870,7 +870,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);
@@ -1035,10 +1035,10 @@ fn collect_explicit_app_ids_from_skill_items_skips_plain_mentions_with_skill_con
assert_eq!(connector_ids, HashSet::<String>::new());
}
#[test]
fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
let config = test_config();
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
#[tokio::test]
async fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
let exposure = build_mcp_tool_exposure(
@@ -1057,10 +1057,10 @@ fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
assert!(exposure.deferred_tools.is_none());
}
#[test]
fn mcp_tool_exposure_searches_large_effective_tool_sets() {
let config = test_config();
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
#[tokio::test]
async fn mcp_tool_exposure_searches_large_effective_tool_sets() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
let exposure = build_mcp_tool_exposure(
@@ -1083,10 +1083,10 @@ fn mcp_tool_exposure_searches_large_effective_tool_sets() {
assert_eq!(deferred_tool_names, expected_tool_names);
}
#[test]
fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overlap() {
let config = test_config();
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
#[tokio::test]
async fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overlap() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mut mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
mcp_tools.extend([(
"mcp__codex_apps__calendar_create_event".to_string(),
File diff suppressed because it is too large Load Diff
+24 -13
View File
@@ -169,13 +169,14 @@ fn resolve_mcp_oauth_credentials_store_mode(
}
#[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(),
AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("temp dir should resolve"),
)
.await
.expect("load default test config")
}
@@ -726,6 +727,7 @@ impl ConfigBuilder {
codex_home,
config_layer_stack,
)
.await
}
#[cfg(test)]
@@ -786,7 +788,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()?;
@@ -794,11 +796,12 @@ impl Config {
codex_home.to_path_buf(),
cli_overrides,
)
.await
}
/// Load a default configuration for a specific Codex home without reading
/// user, project, or system config layers.
pub fn load_default_with_cli_overrides_for_codex_home(
pub async fn load_default_with_cli_overrides_for_codex_home(
codex_home: PathBuf,
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<Self> {
@@ -818,6 +821,7 @@ impl Config {
codex_home,
ConfigLayerStack::default(),
)
.await
}
/// This is a secondary way of creating [Config], which is appropriate when
@@ -1413,22 +1417,24 @@ 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: AbsolutePathBuf,
) -> 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: AbsolutePathBuf,
config_layer_stack: ConfigLayerStack,
) -> std::io::Result<Self> {
// Keep the large config-construction future off small test thread stacks.
Box::pin(async move {
validate_model_providers(&cfg.model_providers)
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
// Ensure that every field of ConfigRequirements is applied to the final
@@ -1547,6 +1553,7 @@ impl Config {
.collect();
let active_project = cfg
.get_active_project(resolved_cwd.as_path())
.await
.unwrap_or(ProjectConfig { trust_level: None });
let permission_config_syntax = resolve_permission_config_syntax(
&config_layer_stack,
@@ -1636,13 +1643,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.as_path(),
Some(&constrained_sandbox_policy),
);
let mut sandbox_policy = cfg
.derive_sandbox_policy(
sandbox_mode,
config_profile.sandbox_mode,
windows_sandbox_level,
resolved_cwd.as_path(),
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) {
@@ -2207,6 +2216,8 @@ impl Config {
},
};
Ok(config)
})
.await
}
fn load_instructions(codex_dir: Option<&AbsolutePathBuf>) -> Option<LoadedUserInstructions> {
@@ -24,8 +24,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");
@@ -64,7 +64,8 @@ fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()>
..Default::default()
},
AbsolutePathBuf::from_absolute_path(&codex_home)?,
)?;
)
.await?;
let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?;
let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?;
+1 -1
View File
@@ -650,7 +650,7 @@ async fn project_trust_context(
let projects = project_trust_config.projects.unwrap_or_default();
let project_root_key = project_trust_key(project_root.as_path());
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| project_trust_key(root));
let projects_trust = projects
+30 -17
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(|p| std::fs::canonicalize(p).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(|p| std::fs::canonicalize(p).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]
+6 -6
View File
@@ -823,9 +823,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,
/*live_network_config*/ None,
@@ -855,9 +855,9 @@ mod tests {
);
}
#[test]
fn guardian_review_session_config_disables_hooks() {
let mut parent_config = crate::config::test_config();
#[tokio::test]
async fn guardian_review_session_config_disables_hooks() {
let mut parent_config = crate::config::test_config().await;
parent_config
.features
.enable(Feature::CodexHooks)
+24 -21
View File
@@ -1332,8 +1332,8 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() ->
Ok(())
}
#[test]
fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow::Result<()> {
#[tokio::test]
async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow::Result<()> {
const TEST_STACK_SIZE_BYTES: usize = 2 * 1024 * 1024;
let handle =
@@ -1569,9 +1569,9 @@ fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow:
)),
}
}
#[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 {
@@ -1616,9 +1616,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());
@@ -1636,9 +1636,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
@@ -1680,9 +1680,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 {
@@ -1708,9 +1708,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 = build_guardian_review_session_config_for_test(
@@ -1724,8 +1724,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_policy_config() {
#[tokio::test]
async fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
let codex_home = tempfile::tempdir().expect("create temp dir");
let workspace = tempfile::tempdir().expect("create temp dir");
let config_layer_stack = ConfigLayerStack::new(
@@ -1748,6 +1748,7 @@ fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
codex_home.abs(),
config_layer_stack,
)
.await
.expect("load config");
let guardian_config = build_guardian_review_session_config_for_test(
@@ -1766,8 +1767,9 @@ fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
);
}
#[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 =
@@ -1782,6 +1784,7 @@ fn guardian_review_session_config_uses_default_guardian_policy_without_requireme
codex_home.abs(),
config_layer_stack,
)
.await
.expect("load config");
let guardian_config = build_guardian_review_session_config_for_test(
+2 -2
View File
@@ -468,7 +468,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_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path())
.expect("codex home is absolute");
@@ -895,7 +895,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_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path())
.expect("codex home is absolute");
+1 -1
View File
@@ -106,7 +106,7 @@ mod tests {
async fn build_prompt_input_includes_context_and_user_message() {
let codex_home = tempfile::tempdir().expect("create codex home");
let cwd = tempfile::tempdir().expect("create cwd");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home =
AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("codex home is absolute");
config.cwd = AbsolutePathBuf::try_from(cwd.path().to_path_buf()).expect("absolute cwd");
+23 -19
View File
@@ -62,8 +62,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()
@@ -144,16 +144,18 @@ async fn load_recent_threads(sess: &Session) -> Vec<StoredThread> {
}
}
fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Option<String> {
async fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Option<String> {
let mut groups: HashMap<PathBuf, Vec<&StoredThread>> = HashMap::new();
for entry in recent_threads {
let group =
resolve_root_git_project_for_trust(&entry.cwd).unwrap_or_else(|| entry.cwd.clone());
let group = resolve_root_git_project_for_trust(&entry.cwd)
.await
.unwrap_or_else(|| entry.cwd.clone());
groups.entry(group).or_default().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
@@ -178,14 +180,13 @@ fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Opt
))
});
let sections = groups
.into_iter()
.take(MAX_RECENT_WORK_GROUPS)
.filter_map(|(group, mut entries)| {
entries.sort_by_key(|entry| Reverse(entry.updated_at));
format_thread_group(&current_group, &group, entries)
})
.collect::<Vec<_>>();
let mut sections = Vec::new();
for (group, mut entries) in groups.into_iter().take(MAX_RECENT_WORK_GROUPS) {
entries.sort_by_key(|entry| Reverse(entry.updated_at));
if let Some(section) = format_thread_group(&current_group, &group, entries).await {
sections.push(section);
}
}
(!sections.is_empty()).then(|| sections.join("\n\n"))
}
@@ -306,11 +307,11 @@ pub(crate) fn truncate_realtime_text_to_token_budget(text: &str, budget_tokens:
}
}
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()
@@ -466,13 +467,16 @@ fn format_startup_context_blob(body: &str, budget_tokens: usize) -> String {
}
}
fn format_thread_group(
async fn format_thread_group(
current_group: &Path,
group: &Path,
entries: Vec<&StoredThread>,
) -> 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 resolve_root_git_project_for_trust(latest.cwd.as_path())
.await
.is_some()
{
format!("### Git repo: {}", group.display())
} else {
format!("### Directory: {}", group.display())
+12 -9
View File
@@ -188,30 +188,31 @@ fn startup_context_blob_is_wrapped_in_tags_and_fits_budget() {
assert!(wrapped.len().div_ceil(4) <= 200);
}
#[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(), /*user_root*/ None),
build_workspace_section_with_user_root(cwd.path(), /*user_root*/ 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(), /*user_root*/ 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");
@@ -225,14 +226,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");
@@ -268,6 +270,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"));
+5 -5
View File
@@ -237,7 +237,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").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -279,7 +279,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").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -422,7 +422,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").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -525,7 +525,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").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -618,7 +618,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").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
+106 -93
View File
@@ -107,8 +107,8 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab
}))
}
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 = construct_model_info_offline("gpt-5-codex", &config);
model_info.supports_search_tool = true;
model_info
@@ -216,8 +216,8 @@ fn find_namespace_function_tool<'a>(
.unwrap_or_else(|| panic!("expected tool {expected_namespace}{expected_name} in namespace"))
}
fn multi_agent_v2_tools_config() -> ToolsConfig {
let config = test_config();
async fn multi_agent_v2_tools_config() -> ToolsConfig {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Collab);
@@ -250,8 +250,8 @@ fn multi_agent_v2_spawn_agent_description(tools_config: &ToolsConfig) -> String
description.clone()
}
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 = bundled_models_response()
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
let model = response
@@ -295,9 +295,9 @@ fn build_specs_with_unavailable_tools(
)
}
#[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();
@@ -320,9 +320,9 @@ fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
assert_eq!(config.shell_type, expected_shell_type);
}
#[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 = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.disable(Feature::MemoryTool);
@@ -350,14 +350,14 @@ fn get_memory_requires_feature_flag() {
);
}
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,
@@ -388,7 +388,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>,
@@ -401,11 +401,11 @@ 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 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",
@@ -424,11 +424,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",
@@ -447,11 +448,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(
@@ -472,11 +474,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(
@@ -497,11 +500,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",
@@ -520,11 +524,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",
@@ -543,11 +548,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",
@@ -565,11 +571,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",
@@ -588,11 +595,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(
@@ -613,12 +621,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 = construct_model_info_offline("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -649,9 +658,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 = construct_model_info_offline("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -713,9 +722,10 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
);
}
#[test]
fn spawn_agent_description_omits_usage_hint_when_disabled() {
#[tokio::test]
async fn spawn_agent_description_omits_usage_hint_when_disabled() {
let tools_config = multi_agent_v2_tools_config()
.await
.with_spawn_agent_usage_hint(/*spawn_agent_usage_hint*/ false);
let description = multi_agent_v2_spawn_agent_description(&tools_config);
@@ -734,11 +744,13 @@ fn spawn_agent_description_omits_usage_hint_when_disabled() {
);
}
#[test]
fn spawn_agent_description_uses_configured_usage_hint_text() {
let tools_config = multi_agent_v2_tools_config().with_spawn_agent_usage_hint_text(Some(
/*spawn_agent_usage_hint_text*/ "Custom delegation guidance only.".to_string(),
));
#[tokio::test]
async fn spawn_agent_description_uses_configured_usage_hint_text() {
let tools_config = multi_agent_v2_tools_config()
.await
.with_spawn_agent_usage_hint_text(Some(
/*spawn_agent_usage_hint_text*/ "Custom delegation guidance only.".to_string(),
));
let description = multi_agent_v2_spawn_agent_description(&tools_config);
assert_regex_match(
@@ -757,9 +769,9 @@ fn spawn_agent_description_uses_configured_usage_hint_text() {
);
}
#[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",
@@ -804,9 +816,9 @@ fn tool_suggest_requires_apps_and_plugins_features() {
}
}
#[test]
fn search_tool_description_handles_no_enabled_mcp_tools() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_description_handles_no_enabled_mcp_tools() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
features.enable(Feature::ToolSearch);
@@ -838,9 +850,9 @@ fn search_tool_description_handles_no_enabled_mcp_tools() {
assert!(!description.contains("{{source_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);
features.enable(Feature::ToolSearch);
@@ -889,9 +901,9 @@ fn search_tool_description_falls_back_to_connector_name_without_description() {
assert!(!description.contains("- Calendar:"));
}
#[test]
fn search_tool_registers_namespaced_mcp_tool_aliases() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_registers_namespaced_mcp_tool_aliases() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
features.enable(Feature::ToolSearch);
@@ -974,9 +986,9 @@ fn search_tool_registers_namespaced_mcp_tool_aliases() {
assert!(registry.has_handler(&mcp_alias));
}
#[test]
fn direct_mcp_tools_register_namespaced_handlers() {
let config = test_config();
#[tokio::test]
async fn direct_mcp_tools_register_namespaced_handlers() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1011,9 +1023,9 @@ fn direct_mcp_tools_register_namespaced_handlers() {
assert!(!registry.has_handler(&ToolName::plain("mcp__test_server__echo")));
}
#[test]
fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
let config = test_config();
#[tokio::test]
async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1060,9 +1072,9 @@ fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
assert!(!registry.has_handler(&ToolName::plain("mcp__codex_apps__calendar_create_event")));
}
#[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 = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1123,9 +1135,9 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() {
);
}
#[test]
fn test_mcp_tool_preserves_integer_schema() {
let config = test_config();
#[tokio::test]
async fn test_mcp_tool_preserves_integer_schema() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1184,9 +1196,9 @@ fn test_mcp_tool_preserves_integer_schema() {
);
}
#[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 = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1249,9 +1261,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 = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1318,9 +1330,9 @@ fn test_mcp_tool_anyof_defaults_to_string() {
);
}
#[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 = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1433,8 +1445,8 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
);
}
#[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);
@@ -1444,5 +1456,6 @@ fn code_mode_only_restricts_model_tools_to_exec_tools() {
&features,
Some(WebSearchMode::Live),
&["exec", "wait"],
);
)
.await;
}
+1 -1
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() {
@@ -10,6 +10,7 @@ pub(super) async fn test_config() -> Config {
.keep();
let mut config =
Config::load_default_with_cli_overrides_for_codex_home(codex_home.clone(), Vec::new())
.await
.expect("config");
config.codex_home = codex_home.abs();
config.sqlite_home = codex_home.clone();
@@ -4,6 +4,7 @@ use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_app_server_client::AppServerEvent;
use codex_app_server_client::AppServerRequestHandle;
use codex_app_server_protocol::ServerNotification;
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,
@@ -122,8 +123,12 @@ impl OnboardingScreen {
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,
trust_target,
codex_home,
show_windows_create_sandbox_hint,
should_quit: false,
@@ -452,7 +457,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;
@@ -1,7 +1,6 @@
use std::path::PathBuf;
use crate::legacy_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,8 +142,7 @@ 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());
let target = self.trust_target.clone();
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) {
tracing::error!("Failed to set project trusted: {e:?}");
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
@@ -182,6 +181,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 +207,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,