Refactor config loading to use filesystem abstraction (#18209)

Initial pass propagating FileSystem through config loading.
This commit is contained in:
pakrym-oai
2026-04-16 17:51:21 -07:00
committed by GitHub
Unverified
parent 2967900d81
commit 9effa0509f
30 changed files with 507 additions and 315 deletions
+3 -4
View File
@@ -1540,7 +1540,6 @@ dependencies = [
"anyhow",
"clap",
"codex-experimental-api-macros",
"codex-git-utils",
"codex-protocol",
"codex-shell-command",
"codex-utils-absolute-path",
@@ -1866,12 +1865,11 @@ dependencies = [
"codex-app-server-protocol",
"codex-execpolicy",
"codex-features",
"codex-git-utils",
"codex-model-provider-info",
"codex-network-proxy",
"codex-protocol",
"codex-utils-absolute-path",
"dunce",
"codex-utils-path",
"futures",
"multimap",
"pretty_assertions",
@@ -2264,6 +2262,8 @@ name = "codex-git-utils"
version = "0.0.0"
dependencies = [
"assert_matches",
"codex-exec-server",
"codex-protocol",
"codex-utils-absolute-path",
"futures",
"once_cell",
@@ -2621,7 +2621,6 @@ dependencies = [
"chrono",
"codex-async-utils",
"codex-execpolicy",
"codex-git-utils",
"codex-network-proxy",
"codex-utils-absolute-path",
"codex-utils-image",
-1
View File
@@ -15,7 +15,6 @@ workspace = true
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
codex-experimental-api-macros = { workspace = true }
codex-git-utils = { workspace = true }
codex-protocol = { workspace = true }
codex-shell-command = { workspace = true }
codex-utils-absolute-path = { workspace = true }
+1 -1
View File
@@ -4,7 +4,6 @@ mod jsonrpc_lite;
mod protocol;
mod schema_fixtures;
pub use codex_git_utils::GitSha;
pub use experimental_api::*;
pub use export::GenerateTsOptions;
pub use export::generate_internal_json_schema;
@@ -30,6 +29,7 @@ pub use protocol::v1::GetConversationSummaryParams;
pub use protocol::v1::GetConversationSummaryResponse;
pub use protocol::v1::GitDiffToRemoteParams;
pub use protocol::v1::GitDiffToRemoteResponse;
pub use protocol::v1::GitSha;
pub use protocol::v1::InitializeCapabilities;
pub use protocol::v1::InitializeParams;
pub use protocol::v1::InitializeResponse;
@@ -1,7 +1,6 @@
use std::collections::HashMap;
use std::path::PathBuf;
use codex_git_utils::GitSha;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ReasoningSummary;
@@ -11,6 +10,7 @@ use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::FileChange;
pub use codex_protocol::protocol::GitSha;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
@@ -2441,9 +2441,9 @@ impl CodexMessageProcessor {
| codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. }
))
{
let trust_target = resolve_root_git_project_for_trust(config.cwd.as_path())
let trust_target = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &config.cwd)
.await
.unwrap_or_else(|| config.cwd.to_path_buf());
.unwrap_or_else(|| config.cwd.clone());
let cli_overrides_with_trust;
let cli_overrides_for_reload = if let Err(err) =
codex_core::config::set_project_trust_level(
@@ -6155,6 +6155,7 @@ impl CodexMessageProcessor {
}
};
let config_layer_stack = match load_config_layers_state(
LOCAL_FS.as_ref(),
&self.config.codex_home,
Some(cwd_abs.clone()),
&cli_overrides,
@@ -1,6 +1,7 @@
use anyhow::Result;
use app_test_support::ChatGptAuthFixture;
use app_test_support::McpProcess;
use app_test_support::PathBufExt;
use app_test_support::create_mock_responses_server_repeating_assistant;
use app_test_support::to_response;
use app_test_support::write_chatgpt_auth;
@@ -20,6 +21,7 @@ use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::ThreadStatusChangedNotification;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::config::set_project_trust_level;
use codex_exec_server::LOCAL_FS;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
use codex_protocol::config_types::ServiceTier;
@@ -716,10 +718,11 @@ model_reasoning_effort = "high"
assert_eq!(reasoning_effort, Some(ReasoningEffort::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())
let workspace_abs = workspace.path().to_path_buf().abs();
let trusted_root = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &workspace_abs)
.await
.unwrap_or_else(|| workspace.path().to_path_buf());
assert!(config_toml.contains(&persisted_trust_path(&trusted_root)));
.unwrap_or(workspace_abs);
assert!(config_toml.contains(&persisted_trust_path(trusted_root.as_path())));
assert!(config_toml.contains("trust_level = \"trusted\""));
Ok(())
@@ -754,10 +757,11 @@ 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)
let nested_abs = nested.abs();
let trusted_root = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &nested_abs)
.await
.expect("git root should resolve");
assert!(config_toml.contains(&persisted_trust_path(&trusted_root)));
assert!(config_toml.contains(&persisted_trust_path(trusted_root.as_path())));
assert!(!config_toml.contains(&persisted_trust_path(&nested)));
Ok(())
+1 -2
View File
@@ -12,12 +12,11 @@ anyhow = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-execpolicy = { workspace = true }
codex-features = { workspace = true }
codex-git-utils = { workspace = true }
codex-model-provider-info = { workspace = true }
codex-network-proxy = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
dunce = { workspace = true }
codex-utils-path = { workspace = true }
futures = { workspace = true, features = ["alloc", "std"] }
multimap = { workspace = true }
schemars = { workspace = true }
+12 -11
View File
@@ -29,7 +29,6 @@ use crate::types::WindowsToml;
use codex_app_server_protocol::Tools;
use codex_app_server_protocol::UserSavedConfig;
use codex_features::FeaturesToml;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
use codex_model_provider_info::ModelProviderInfo;
@@ -51,6 +50,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ReadOnlyAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path::normalize_for_path_comparison;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Deserializer;
@@ -601,7 +601,7 @@ impl ConfigToml {
sandbox_mode_override: Option<SandboxMode>,
profile_sandbox_mode: Option<SandboxMode>,
windows_sandbox_level: WindowsSandboxLevel,
resolved_cwd: &Path,
active_project: Option<&ProjectConfig>,
sandbox_policy_constraint: Option<&crate::Constrained<SandboxPolicy>>,
) -> SandboxPolicy {
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
@@ -616,7 +616,7 @@ impl ConfigToml {
// 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).await.and_then(|p| {
active_project.and_then(|p| {
if p.is_trusted() || p.is_untrusted() {
if cfg!(target_os = "windows")
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
@@ -677,9 +677,13 @@ 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 async fn get_active_project(&self, resolved_cwd: &Path) -> Option<ProjectConfig> {
let repo_root = resolve_root_git_project_for_trust(resolved_cwd).await;
/// does not contain a project corresponding to cwd or the resolved git repo
/// root for cwd.
pub fn get_active_project(
&self,
resolved_cwd: &Path,
repo_root: Option<&Path>,
) -> Option<ProjectConfig> {
let projects = self.projects.clone().unwrap_or_default();
let resolved_cwd_key = project_trust_key(resolved_cwd);
@@ -691,10 +695,7 @@ impl ConfigToml {
return Some(project_config.clone());
}
// 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) = repo_root.as_deref() {
if let Some(repo_root) = repo_root {
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
@@ -734,7 +735,7 @@ impl ConfigToml {
/// projects trust map. On Windows, strips UNC, when possible, to try to ensure
/// that different paths that point to the same location have the same key.
fn project_trust_key(project_path: &Path) -> String {
dunce::canonicalize(project_path)
normalize_for_path_comparison(project_path)
.unwrap_or_else(|_| project_path.to_path_buf())
.to_string_lossy()
.to_string()
+2
View File
@@ -18,6 +18,7 @@ use crate::config_loader::resolve_relative_paths_in_config_toml;
use anyhow::anyhow;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::config_toml::ConfigToml;
use codex_exec_server::LOCAL_FS;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::Path;
@@ -168,6 +169,7 @@ mod reload {
}
let mut next_config = Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
merged_config,
reload_overrides(config, preserve_current_provider),
config.codex_home.clone(),
+2
View File
@@ -22,6 +22,7 @@ use crate::realtime_context::REALTIME_TURN_TOKEN_BUDGET;
use crate::realtime_context::truncate_realtime_text_to_token_budget;
use crate::realtime_conversation::REALTIME_USER_TEXT_PREFIX;
use crate::realtime_conversation::prefix_realtime_v2_text;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -526,6 +527,7 @@ pub async fn list_skills(sess: &Session, sub_id: String, cwds: Vec<PathBuf>, for
}
};
let config_layer_stack = match load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd_abs.clone()),
empty_cli_overrides,
+94 -72
View File
@@ -4,6 +4,7 @@ use crate::config_loader::ConfigLayerStackOrdering;
use codex_config::config_toml::AgentRoleToml;
use codex_config::config_toml::AgentsToml;
use codex_config::config_toml::ConfigToml;
use codex_exec_server::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use serde::Deserialize;
@@ -14,7 +15,8 @@ use std::path::Path;
use std::path::PathBuf;
use toml::Value as TomlValue;
pub(crate) fn load_agent_roles(
pub(crate) async fn load_agent_roles(
fs: &dyn ExecutorFileSystem,
cfg: &ConfigToml,
config_layer_stack: &ConfigLayerStack,
startup_warnings: &mut Vec<String>,
@@ -24,7 +26,7 @@ pub(crate) fn load_agent_roles(
/*include_disabled*/ false,
);
if layers.is_empty() {
return load_agent_roles_without_layers(cfg);
return load_agent_roles_without_layers(fs, cfg).await;
}
let mut roles: BTreeMap<String, AgentRoleConfig> = BTreeMap::new();
@@ -40,13 +42,14 @@ pub(crate) fn load_agent_roles(
};
if let Some(agents_toml) = agents_toml {
for (declared_role_name, role_toml) in &agents_toml.roles {
let (role_name, role) = match read_declared_role(declared_role_name, role_toml) {
Ok(role) => role,
Err(err) => {
push_agent_role_warning(startup_warnings, err);
continue;
}
};
let (role_name, role) =
match read_declared_role(fs, declared_role_name, role_toml).await {
Ok(role) => role,
Err(err) => {
push_agent_role_warning(startup_warnings, err);
continue;
}
};
if let Some(config_file) = role.config_file.clone() {
declared_role_files.insert(config_file);
}
@@ -68,10 +71,13 @@ pub(crate) fn load_agent_roles(
if let Some(config_folder) = layer.config_folder() {
for (role_name, role) in discover_agent_roles_in_dir(
config_folder.as_path().join("agents").as_path(),
fs,
&config_folder.join("agents"),
&declared_role_files,
startup_warnings,
)? {
)
.await?
{
if layer_roles.contains_key(&role_name) {
push_agent_role_warning(
startup_warnings,
@@ -113,13 +119,14 @@ fn push_agent_role_warning(startup_warnings: &mut Vec<String>, err: std::io::Err
startup_warnings.push(message);
}
fn load_agent_roles_without_layers(
async fn load_agent_roles_without_layers(
fs: &dyn ExecutorFileSystem,
cfg: &ConfigToml,
) -> std::io::Result<BTreeMap<String, AgentRoleConfig>> {
let mut roles = BTreeMap::new();
if let Some(agents_toml) = cfg.agents.as_ref() {
for (declared_role_name, role_toml) in &agents_toml.roles {
let (role_name, role) = read_declared_role(declared_role_name, role_toml)?;
let (role_name, role) = read_declared_role(fs, declared_role_name, role_toml).await?;
validate_required_agent_role_description(&role_name, role.description.as_deref())?;
if roles.insert(role_name.clone(), role).is_some() {
@@ -134,14 +141,17 @@ fn load_agent_roles_without_layers(
Ok(roles)
}
fn read_declared_role(
async fn read_declared_role(
fs: &dyn ExecutorFileSystem,
declared_role_name: &str,
role_toml: &AgentRoleToml,
) -> std::io::Result<(String, AgentRoleConfig)> {
let mut role = agent_role_config_from_toml(declared_role_name, role_toml)?;
let mut role = agent_role_config_from_toml(fs, declared_role_name, role_toml).await?;
let mut role_name = declared_role_name.to_string();
if let Some(config_file) = role.config_file.as_deref() {
let parsed_file = read_resolved_agent_role_file(config_file, Some(declared_role_name))?;
let config_file = AbsolutePathBuf::from_absolute_path(config_file)?;
let parsed_file =
read_resolved_agent_role_file(fs, &config_file, Some(declared_role_name)).await?;
role_name = parsed_file.role_name;
role.description = parsed_file.description.or(role.description);
role.nickname_candidates = parsed_file.nickname_candidates.or(role.nickname_candidates);
@@ -171,12 +181,17 @@ fn agents_toml_from_layer(layer_toml: &TomlValue) -> std::io::Result<Option<Agen
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
}
fn agent_role_config_from_toml(
async fn agent_role_config_from_toml(
fs: &dyn ExecutorFileSystem,
role_name: &str,
role: &AgentRoleToml,
) -> std::io::Result<AgentRoleConfig> {
let config_file = role.config_file.as_ref().map(AbsolutePathBuf::to_path_buf);
validate_agent_role_config_file(role_name, config_file.as_deref())?;
let config_file = role
.config_file
.as_ref()
.map(AbsolutePathBuf::from_absolute_path)
.transpose()?;
validate_agent_role_config_file(fs, role_name, config_file.as_ref()).await?;
let description = normalize_agent_role_description(
&format!("agents.{role_name}.description"),
role.description.as_deref(),
@@ -188,7 +203,7 @@ fn agent_role_config_from_toml(
Ok(AgentRoleConfig {
description,
config_file,
config_file: config_file.map(AbsolutePathBuf::into_path_buf),
nickname_candidates,
})
}
@@ -293,15 +308,17 @@ pub(crate) fn parse_agent_role_file_contents(
})
}
fn read_resolved_agent_role_file(
path: &Path,
async fn read_resolved_agent_role_file(
fs: &dyn ExecutorFileSystem,
path: &AbsolutePathBuf,
role_name_hint: Option<&str>,
) -> std::io::Result<ResolvedAgentRoleFile> {
let contents = std::fs::read_to_string(path)?;
let contents = fs.read_file_text(path, /*sandbox*/ None).await?;
let config_base_dir = path.parent().unwrap_or_else(|| path.clone());
parse_agent_role_file_contents(
&contents,
path,
path.parent().unwrap_or(path),
path.as_path(),
config_base_dir.as_path(),
role_name_hint,
)
}
@@ -359,31 +376,35 @@ fn validate_agent_role_file_developer_instructions(
}
}
fn validate_agent_role_config_file(
async fn validate_agent_role_config_file(
fs: &dyn ExecutorFileSystem,
role_name: &str,
config_file: Option<&Path>,
config_file: Option<&AbsolutePathBuf>,
) -> std::io::Result<()> {
let Some(config_file) = config_file else {
return Ok(());
};
let metadata = std::fs::metadata(config_file).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"agents.{role_name}.config_file must point to an existing file at {}: {e}",
config_file.display()
),
)
})?;
if metadata.is_file() {
let metadata = fs
.get_metadata(config_file, /*sandbox*/ None)
.await
.map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"agents.{role_name}.config_file must point to an existing file at {}: {e}",
config_file.as_path().display()
),
)
})?;
if metadata.is_file {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"agents.{role_name}.config_file must point to a file: {}",
config_file.display()
config_file.as_path().display()
),
))
}
@@ -441,19 +462,20 @@ fn normalize_agent_role_nickname_candidates(
Ok(Some(normalized_candidates))
}
fn discover_agent_roles_in_dir(
agents_dir: &Path,
async fn discover_agent_roles_in_dir(
fs: &dyn ExecutorFileSystem,
agents_dir: &AbsolutePathBuf,
declared_role_files: &BTreeSet<PathBuf>,
startup_warnings: &mut Vec<String>,
) -> std::io::Result<BTreeMap<String, AgentRoleConfig>> {
let mut roles = BTreeMap::new();
for agent_file in collect_agent_role_files(agents_dir)? {
if declared_role_files.contains(&agent_file) {
for agent_file in collect_agent_role_files(fs, agents_dir).await? {
if declared_role_files.contains(agent_file.as_path()) {
continue;
}
let parsed_file =
match read_resolved_agent_role_file(&agent_file, /*role_name_hint*/ None) {
match read_resolved_agent_role_file(fs, &agent_file, /*role_name_hint*/ None).await {
Ok(parsed_file) => parsed_file,
Err(err) => {
push_agent_role_warning(startup_warnings, err);
@@ -468,7 +490,7 @@ fn discover_agent_roles_in_dir(
std::io::ErrorKind::InvalidInput,
format!(
"duplicate agent role name `{role_name}` discovered in {}",
agents_dir.display()
agents_dir.as_path().display()
),
),
);
@@ -478,7 +500,7 @@ fn discover_agent_roles_in_dir(
role_name,
AgentRoleConfig {
description: parsed_file.description,
config_file: Some(agent_file),
config_file: Some(agent_file.to_path_buf()),
nickname_candidates: parsed_file.nickname_candidates,
},
);
@@ -487,36 +509,36 @@ fn discover_agent_roles_in_dir(
Ok(roles)
}
fn collect_agent_role_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
async fn collect_agent_role_files(
fs: &dyn ExecutorFileSystem,
dir: &AbsolutePathBuf,
) -> std::io::Result<Vec<AbsolutePathBuf>> {
let mut files = Vec::new();
collect_agent_role_files_recursive(dir, &mut files)?;
files.sort();
Ok(files)
}
let mut dirs = vec![dir.clone()];
while let Some(dir) = dirs.pop() {
let entries = match fs.read_directory(&dir, /*sandbox*/ None).await {
Ok(entries) => entries,
Err(err) if err.kind() == ErrorKind::NotFound => continue,
Err(err) => return Err(err),
};
fn collect_agent_role_files_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
let read_dir = match std::fs::read_dir(dir) {
Ok(read_dir) => read_dir,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err),
};
for entry in read_dir {
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
if file_type.is_dir() {
collect_agent_role_files_recursive(&path, files)?;
continue;
}
if file_type.is_file()
&& path
.extension()
.is_some_and(|extension| extension == "toml")
{
files.push(path);
for entry in entries {
let path = dir.join(entry.file_name);
if entry.is_directory {
dirs.push(path);
continue;
}
if entry.is_file
&& path
.as_path()
.extension()
.is_some_and(|extension| extension == "toml")
{
files.push(path);
}
}
}
Ok(())
files.sort();
Ok(files)
}
+25 -10
View File
@@ -44,6 +44,7 @@ use codex_config::types::SkillsConfig;
use codex_config::types::ToolSuggestDiscoverableType;
use codex_config::types::Tui;
use codex_config::types::TuiNotificationSettings;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
use codex_features::FeaturesToml;
use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
@@ -1194,7 +1195,7 @@ network_access = false # This should be ignored.
sandbox_mode_override,
/*profile_sandbox_mode*/ None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
/*active_project*/ None,
/*sandbox_policy_constraint*/ None,
)
.await;
@@ -1215,7 +1216,7 @@ network_access = true # This should be ignored.
sandbox_mode_override,
/*profile_sandbox_mode*/ None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
/*active_project*/ None,
/*sandbox_policy_constraint*/ None,
)
.await;
@@ -1232,6 +1233,9 @@ writable_roots = [
]
exclude_tmpdir_env_var = true
exclude_slash_tmp = true
[projects."/tmp/test"]
trust_level = "trusted"
"#,
serde_json::json!(writable_root)
);
@@ -1244,7 +1248,7 @@ exclude_slash_tmp = true
sandbox_mode_override,
/*profile_sandbox_mode*/ None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
/*active_project*/ None,
/*sandbox_policy_constraint*/ None,
)
.await;
@@ -1273,9 +1277,6 @@ writable_roots = [
]
exclude_tmpdir_env_var = true
exclude_slash_tmp = true
[projects."/tmp/test"]
trust_level = "trusted"
"#,
serde_json::json!(writable_root)
);
@@ -1288,7 +1289,7 @@ trust_level = "trusted"
sandbox_mode_override,
/*profile_sandbox_mode*/ None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
/*active_project*/ None,
/*sandbox_policy_constraint*/ None,
)
.await;
@@ -2085,6 +2086,7 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> {
let cwd = codex_home.path().abs();
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home.path(),
Some(cwd),
&Vec::new(),
@@ -2218,6 +2220,7 @@ async fn managed_config_wins_over_cli_overrides() -> anyhow::Result<()> {
let cwd = codex_home.path().abs();
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home.path(),
Some(cwd),
&[("model".to_string(), TomlValue::String("cli".to_string()))],
@@ -3486,6 +3489,7 @@ async fn load_config_uses_requirements_guardian_policy_config() -> std::io::Resu
.map_err(std::io::Error::other)?;
let config = Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
ConfigToml::default(),
ConfigOverrides {
cwd: Some(codex_home.path().to_path_buf()),
@@ -3518,6 +3522,7 @@ async fn load_config_ignores_empty_requirements_guardian_policy_config() -> std:
.map_err(std::io::Error::other)?;
let config = Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
ConfigToml::default(),
ConfigOverrides {
cwd: Some(codex_home.path().to_path_buf()),
@@ -5330,6 +5335,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
.expect("config layer stack");
let config = Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
fixture.cfg.clone(),
ConfigOverrides {
cwd: Some(fixture.cwd_path()),
@@ -5537,13 +5543,16 @@ trust_level = "untrusted"
let cfg = toml::from_str::<ConfigToml>(config_with_untrusted)
.expect("TOML deserialization should succeed");
let active_project = ProjectConfig {
trust_level: Some(TrustLevel::Untrusted),
};
let resolution = cfg
.derive_sandbox_policy(
/*sandbox_mode_override*/ None,
/*profile_sandbox_mode*/ None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
Some(&active_project),
/*sandbox_policy_constraint*/ None,
)
.await;
@@ -5579,6 +5588,9 @@ async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defau
)])),
..Default::default()
};
let active_project = ProjectConfig {
trust_level: Some(TrustLevel::Trusted),
};
let constrained = Constrained::new(SandboxPolicy::DangerFullAccess, |candidate| {
if matches!(candidate, SandboxPolicy::DangerFullAccess) {
Ok(())
@@ -5597,7 +5609,7 @@ async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defau
/*sandbox_mode_override*/ None,
/*profile_sandbox_mode*/ None,
WindowsSandboxLevel::Disabled,
&project_path,
Some(&active_project),
Some(&constrained),
)
.await;
@@ -5621,6 +5633,9 @@ async fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallb
)])),
..Default::default()
};
let active_project = ProjectConfig {
trust_level: Some(TrustLevel::Trusted),
};
let constrained = Constrained::new(SandboxPolicy::new_workspace_write_policy(), |candidate| {
if matches!(candidate, SandboxPolicy::WorkspaceWrite { .. }) {
Ok(())
@@ -5639,7 +5654,7 @@ async fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallb
/*sandbox_mode_override*/ None,
/*profile_sandbox_mode*/ None,
WindowsSandboxLevel::Disabled,
&project_path,
Some(&active_project),
Some(&constrained),
)
.await;
+45 -15
View File
@@ -47,6 +47,8 @@ use codex_config::types::ToolSuggestDiscoverable;
use codex_config::types::TuiNotificationSettings;
use codex_config::types::UriBasedFileOpener;
use codex_config::types::WindowsSandboxModeToml;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
use codex_features::FeatureConfigSource;
use codex_features::FeatureOverrides;
@@ -54,6 +56,7 @@ use codex_features::FeatureToml;
use codex_features::Features;
use codex_features::FeaturesToml;
use codex_features::MultiAgentV2ConfigToml;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_login::AuthManagerConfig;
use codex_mcp::McpConfig;
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
@@ -689,6 +692,7 @@ impl ConfigBuilder {
};
harness_overrides.cwd = Some(cwd.to_path_buf());
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&cli_overrides,
@@ -718,6 +722,7 @@ impl ConfigBuilder {
}
};
Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
config_toml,
harness_overrides,
codex_home,
@@ -812,6 +817,7 @@ impl Config {
let codex_home = AbsolutePathBuf::from_absolute_path_checked(codex_home)?;
let config_toml = deserialize_config_toml_with_base(merged, &codex_home)?;
Self::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
config_toml,
ConfigOverrides::default(),
codex_home,
@@ -849,6 +855,7 @@ pub async fn load_config_as_toml_with_cli_overrides(
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<ConfigToml> {
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home,
cwd.cloned(),
&cli_overrides,
@@ -1019,6 +1026,7 @@ pub async fn load_global_mcp_servers(
// MCP servers defined in in-repo .codex/ folders.
let cwd: Option<AbsolutePathBuf> = None;
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home,
cwd,
&cli_overrides,
@@ -1420,10 +1428,18 @@ impl Config {
) -> 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).await
Self::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
cfg,
overrides,
codex_home,
config_layer_stack,
)
.await
}
pub(crate) async fn load_config_with_layer_stack(
fs: &dyn ExecutorFileSystem,
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
@@ -1545,9 +1561,12 @@ impl Config {
.into_iter()
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()))
.collect();
let repo_root = resolve_root_git_project_for_trust(fs, &resolved_cwd).await;
let active_project = cfg
.get_active_project(resolved_cwd.as_path())
.await
.get_active_project(
resolved_cwd.as_path(),
repo_root.as_ref().map(AbsolutePathBuf::as_path),
)
.unwrap_or(ProjectConfig { trust_level: None });
let permission_config_syntax = resolve_permission_config_syntax(
&config_layer_stack,
@@ -1643,7 +1662,7 @@ impl Config {
sandbox_mode,
config_profile.sandbox_mode,
windows_sandbox_level,
resolved_cwd.as_path(),
Some(&active_project),
Some(&constrained_sandbox_policy),
)
.await;
@@ -1712,7 +1731,8 @@ impl Config {
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg, &config_profile);
let agent_roles =
agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)?;
agent_roles::load_agent_roles(fs, &cfg, &config_layer_stack, &mut startup_warnings)
.await?;
let openai_base_url = cfg
.openai_base_url
@@ -1861,8 +1881,12 @@ impl Config {
.model_instructions_file
.as_ref()
.or(cfg.model_instructions_file.as_ref());
let file_base_instructions =
Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?;
let file_base_instructions = Self::try_read_non_empty_file(
fs,
model_instructions_path,
"model instructions file",
)
.await?;
let base_instructions = base_instructions.or(file_base_instructions);
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
let include_permissions_instructions = config_profile
@@ -1893,9 +1917,11 @@ impl Config {
.as_ref()
.or(cfg.experimental_compact_prompt_file.as_ref());
let file_compact_prompt = Self::try_read_non_empty_file(
fs,
experimental_compact_prompt_path,
"experimental compact prompt file",
)?;
)
.await?;
let compact_prompt = compact_prompt.or(file_compact_prompt);
let js_repl_node_path = js_repl_node_path_override
.or(config_profile.js_repl_node_path.map(Into::into))
@@ -2218,7 +2244,8 @@ impl Config {
/// If `path` is `Some`, attempts to read the file at the given path and
/// returns its contents as a trimmed `String`. If the file is empty, or
/// is `Some` but cannot be read, returns an `Err`.
fn try_read_non_empty_file(
async fn try_read_non_empty_file(
fs: &dyn ExecutorFileSystem,
path: Option<&AbsolutePathBuf>,
context: &str,
) -> std::io::Result<Option<String>> {
@@ -2226,12 +2253,15 @@ impl Config {
return Ok(None);
};
let contents = std::fs::read_to_string(path).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to read {context} {}: {e}", path.display()),
)
})?;
let contents = fs
.read_file_text(path, /*sandbox*/ None)
.await
.map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to read {context} {}: {e}", path.display()),
)
})?;
let s = contents.trim().to_string();
if s.is_empty() {
+2
View File
@@ -29,6 +29,7 @@ use codex_app_server_protocol::OverriddenMetadata;
use codex_app_server_protocol::WriteStatus;
use codex_config::CONFIG_TOML_FILE;
use codex_config::config_toml::ConfigToml;
use codex_exec_server::LOCAL_FS;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde_json::Value as JsonValue;
use std::borrow::Cow;
@@ -424,6 +425,7 @@ impl ConfigService {
async fn load_thread_agnostic_config(&self) -> std::io::Result<ConfigLayerStack> {
let cwd: Option<AbsolutePathBuf> = None;
load_config_layers_state(
LOCAL_FS.as_ref(),
&self.codex_home,
cwd,
&self.cli_overrides,
+7 -3
View File
@@ -10,7 +10,7 @@ This module is the canonical place to **load and describe Codex configuration la
Exported from `codex_core::config_loader`:
- `load_config_layers_state(codex_home, cwd_opt, cli_overrides, overrides, cloud_requirements) -> ConfigLayerStack`
- `load_config_layers_state(fs, codex_home, cwd_opt, cli_overrides, overrides, cloud_requirements) -> ConfigLayerStack`
- `ConfigLayerStack`
- `effective_config() -> toml::Value`
- `origins() -> HashMap<String, ConfigLayerMetadata>`
@@ -38,18 +38,22 @@ computing the effective config and origins metadata. This is what
Most callers want the effective config plus metadata:
```rust
use codex_core::config_loader::{load_config_layers_state, LoaderOverrides};
use codex_core::config_loader::{
CloudRequirementsLoader, LoaderOverrides, load_config_layers_state,
};
use codex_exec_server::LOCAL_FS;
use codex_utils_absolute_path::AbsolutePathBuf;
use toml::Value as TomlValue;
let cli_overrides: Vec<(String, TomlValue)> = Vec::new();
let cwd = AbsolutePathBuf::current_dir()?;
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&cli_overrides,
LoaderOverrides::default(),
None,
CloudRequirementsLoader::default(),
).await?;
let effective = layers.effective_config();
+11 -9
View File
@@ -5,11 +5,11 @@ use super::macos::ManagedAdminConfigLayer;
use super::macos::load_managed_admin_config_layer;
use codex_config::config_error_from_toml;
use codex_config::io_error_from_config_error;
use codex_exec_server::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use tokio::fs;
use toml::Value as TomlValue;
#[cfg(unix)]
@@ -36,6 +36,7 @@ pub(super) struct LoadedConfigLayers {
}
pub(super) async fn load_config_layers_internal(
fs: &dyn ExecutorFileSystem,
codex_home: &Path,
overrides: LoaderOverrides,
) -> io::Result<LoadedConfigLayers> {
@@ -57,7 +58,7 @@ pub(super) async fn load_config_layers_internal(
)?;
let managed_config =
read_config_from_path(&managed_config_path, /*log_missing_as_info*/ false)
read_config_from_path(fs, &managed_config_path, /*log_missing_as_info*/ false)
.await?
.map(|managed_config| MangedConfigFromFile {
managed_config,
@@ -89,15 +90,16 @@ fn map_managed_admin_layer(layer: ManagedAdminConfigLayer) -> ManagedConfigFromM
}
pub(super) async fn read_config_from_path(
path: impl AsRef<Path>,
fs: &dyn ExecutorFileSystem,
path: &AbsolutePathBuf,
log_missing_as_info: bool,
) -> io::Result<Option<TomlValue>> {
match fs::read_to_string(path.as_ref()).await {
match fs.read_file_text(path, /*sandbox*/ None).await {
Ok(contents) => match toml::from_str::<TomlValue>(&contents) {
Ok(value) => Ok(Some(value)),
Err(err) => {
tracing::error!("Failed to parse {}: {err}", path.as_ref().display());
let config_error = config_error_from_toml(path.as_ref(), &contents, err.clone());
tracing::error!("Failed to parse {}: {err}", path.as_path().display());
let config_error = config_error_from_toml(path.as_path(), &contents, err.clone());
Err(io_error_from_config_error(
io::ErrorKind::InvalidData,
config_error,
@@ -107,14 +109,14 @@ pub(super) async fn read_config_from_path(
},
Err(err) if err.kind() == io::ErrorKind::NotFound => {
if log_missing_as_info {
tracing::info!("{} not found, using defaults", path.as_ref().display());
tracing::info!("{} not found, using defaults", path.as_path().display());
} else {
tracing::debug!("{} not found", path.as_ref().display());
tracing::debug!("{} not found", path.as_path().display());
}
Ok(None)
}
Err(err) => {
tracing::error!("Failed to read {}: {err}", path.as_ref().display());
tracing::error!("Failed to read {}: {err}", path.as_path().display());
Err(err)
}
}
+53 -35
View File
@@ -11,6 +11,7 @@ use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigRequirementsWithSources;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
use codex_exec_server::ExecutorFileSystem;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::SandboxMode;
@@ -118,6 +119,7 @@ pub(crate) async fn first_layer_config_error_from_entries(
/// thread-agnostic config loading (e.g., for the app server's `/config`
/// endpoint) should `cwd` be `None`.
pub async fn load_config_layers_state(
fs: &dyn ExecutorFileSystem,
codex_home: &Path,
cwd: Option<AbsolutePathBuf>,
cli_overrides: &[(String, TomlValue)],
@@ -142,11 +144,12 @@ pub async fn load_config_layers_state(
// Honor the system requirements.toml location.
let requirements_toml_file = system_requirements_toml_file()?;
load_requirements_toml(&mut config_requirements_toml, requirements_toml_file).await?;
load_requirements_toml(fs, &mut config_requirements_toml, &requirements_toml_file).await?;
// Make a best-effort to support the legacy `managed_config.toml` as a
// requirements specification.
let loaded_config_layers = layer_io::load_config_layers_internal(codex_home, overrides).await?;
let loaded_config_layers =
layer_io::load_config_layers_internal(fs, codex_home, overrides).await?;
load_requirements_from_legacy_scheme(
&mut config_requirements_toml,
loaded_config_layers.clone(),
@@ -173,7 +176,7 @@ pub async fn load_config_layers_state(
// if it exists.
let system_config_toml_file = system_config_toml_file()?;
let system_layer =
load_config_toml_for_required_layer(&system_config_toml_file, |config_toml| {
load_config_toml_for_required_layer(fs, &system_config_toml_file, |config_toml| {
ConfigLayerEntry::new(
ConfigLayerSource::System {
file: system_config_toml_file.clone(),
@@ -188,7 +191,7 @@ pub async fn load_config_layers_state(
// exists, but is malformed, then this error should be propagated to the
// user.
let user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home);
let user_layer = load_config_toml_for_required_layer(&user_file, |config_toml| {
let user_layer = load_config_toml_for_required_layer(fs, &user_file, |config_toml| {
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: user_file.clone(),
@@ -222,6 +225,7 @@ pub async fn load_config_layers_state(
}
};
let project_trust_context = match project_trust_context(
fs,
&merged_so_far,
&cwd,
&project_root_markers,
@@ -247,6 +251,7 @@ pub async fn load_config_layers_state(
}
};
let project_layers = load_project_layers(
fs,
&cwd,
&project_trust_context.project_root,
&project_trust_context,
@@ -320,22 +325,23 @@ pub async fn load_config_layers_state(
/// - If there is an error reading the file or parsing the TOML, returns an
/// error.
async fn load_config_toml_for_required_layer(
config_toml: impl AsRef<Path>,
fs: &dyn ExecutorFileSystem,
toml_file: &AbsolutePathBuf,
create_entry: impl FnOnce(TomlValue) -> ConfigLayerEntry,
) -> io::Result<ConfigLayerEntry> {
let toml_file = config_toml.as_ref();
let toml_value = match tokio::fs::read_to_string(toml_file).await {
let toml_value = match fs.read_file_text(toml_file, /*sandbox*/ None).await {
Ok(contents) => {
let config: TomlValue = toml::from_str(&contents).map_err(|err| {
let config_error = config_error_from_toml(toml_file, &contents, err.clone());
let config_error =
config_error_from_toml(toml_file.as_path(), &contents, err.clone());
io_error_from_config_error(io::ErrorKind::InvalidData, config_error, Some(err))
})?;
let config_parent = toml_file.parent().ok_or_else(|| {
let config_parent = toml_file.as_path().parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Config file {} has no parent directory",
toml_file.display()
toml_file.as_path().display()
),
)
})?;
@@ -347,7 +353,10 @@ async fn load_config_toml_for_required_layer(
} else {
Err(io::Error::new(
e.kind(),
format!("Failed to read config file {}: {e}", toml_file.display()),
format!(
"Failed to read config file {}: {e}",
toml_file.as_path().display()
),
))
}
}
@@ -360,12 +369,14 @@ async fn load_config_toml_for_required_layer(
/// `requirements.toml` location to `config_requirements_toml` by filling in
/// any unset fields.
async fn load_requirements_toml(
fs: &dyn ExecutorFileSystem,
config_requirements_toml: &mut ConfigRequirementsWithSources,
requirements_toml_file: impl AsRef<Path>,
requirements_toml_file: &AbsolutePathBuf,
) -> io::Result<()> {
let requirements_toml_file =
AbsolutePathBuf::from_absolute_path(requirements_toml_file.as_ref())?;
match tokio::fs::read_to_string(&requirements_toml_file).await {
match fs
.read_file_text(requirements_toml_file, /*sandbox*/ None)
.await
{
Ok(contents) => {
let requirements_config: ConfigRequirementsToml =
toml::from_str(&contents).map_err(|e| {
@@ -373,7 +384,7 @@ async fn load_requirements_toml(
io::ErrorKind::InvalidData,
format!(
"Error parsing requirements file {}: {e}",
requirements_toml_file.as_ref().display(),
requirements_toml_file.as_path().display(),
),
)
})?;
@@ -390,7 +401,7 @@ async fn load_requirements_toml(
e.kind(),
format!(
"Failed to read requirements file {}: {e}",
requirements_toml_file.as_ref().display(),
requirements_toml_file.as_path().display(),
),
));
}
@@ -632,6 +643,7 @@ fn project_layer_entry(
}
async fn project_trust_context(
fs: &dyn ExecutorFileSystem,
merged_config: &TomlValue,
cwd: &AbsolutePathBuf,
project_root_markers: &[String],
@@ -646,12 +658,14 @@ async fn project_trust_context(
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?
};
let project_root = find_project_root(cwd, project_root_markers).await?;
let project_root = find_project_root(fs, cwd, project_root_markers).await?;
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()).await;
let repo_root_key = repo_root.as_ref().map(|root| project_trust_key(root));
let repo_root = resolve_root_git_project_for_trust(fs, cwd).await;
let repo_root_key = repo_root
.as_ref()
.map(|root| project_trust_key(root.as_path()));
let projects_trust = projects
.into_iter()
@@ -742,6 +756,7 @@ fn copy_shape_from_original(original: &TomlValue, resolved: &TomlValue) -> TomlV
}
async fn find_project_root(
fs: &dyn ExecutorFileSystem,
cwd: &AbsolutePathBuf,
project_root_markers: &[String],
) -> io::Result<AbsolutePathBuf> {
@@ -749,11 +764,15 @@ async fn find_project_root(
return Ok(cwd.clone());
}
for ancestor in cwd.as_path().ancestors() {
for ancestor in cwd.ancestors() {
for marker in project_root_markers {
let marker_path = ancestor.join(marker);
if tokio::fs::metadata(&marker_path).await.is_ok() {
return AbsolutePathBuf::from_absolute_path(ancestor);
if fs
.get_metadata(&marker_path, /*sandbox*/ None)
.await
.is_ok()
{
return Ok(ancestor);
}
}
}
@@ -766,6 +785,7 @@ async fn find_project_root(
/// starting from folders closest to `project_root` (which is the lowest
/// precedence) to those closest to `cwd` (which is the highest precedence).
async fn load_project_layers(
fs: &dyn ExecutorFileSystem,
cwd: &AbsolutePathBuf,
project_root: &AbsolutePathBuf,
trust_context: &ProjectTrustContext,
@@ -775,13 +795,12 @@ async fn load_project_layers(
let codex_home_normalized =
normalize_path(codex_home_abs.as_path()).unwrap_or_else(|_| codex_home_abs.to_path_buf());
let mut dirs = cwd
.as_path()
.ancestors()
.scan(false, |done, a| {
if *done {
None
} else {
if a == project_root.as_path() {
if &a == project_root {
*done = true;
}
Some(a)
@@ -792,25 +811,24 @@ async fn load_project_layers(
let mut layers = Vec::new();
for dir in dirs {
let dot_codex = dir.join(".codex");
if !tokio::fs::metadata(&dot_codex)
let dot_codex_abs = dir.join(".codex");
if !fs
.get_metadata(&dot_codex_abs, /*sandbox*/ None)
.await
.map(|meta| meta.is_dir())
.map(|metadata| metadata.is_directory)
.unwrap_or(false)
{
continue;
}
let layer_dir = AbsolutePathBuf::from_absolute_path(dir)?;
let decision = trust_context.decision_for_dir(&layer_dir);
let dot_codex_abs = AbsolutePathBuf::from_absolute_path(&dot_codex)?;
let decision = trust_context.decision_for_dir(&dir);
let dot_codex_normalized =
normalize_path(dot_codex_abs.as_path()).unwrap_or_else(|_| dot_codex_abs.to_path_buf());
if dot_codex_abs == codex_home_abs || dot_codex_normalized == codex_home_normalized {
continue;
}
let config_file = dot_codex_abs.join(CONFIG_TOML_FILE);
match tokio::fs::read_to_string(&config_file).await {
match fs.read_file_text(&config_file, /*sandbox*/ None).await {
Ok(contents) => {
let config: TomlValue = match toml::from_str(&contents) {
Ok(config) => config,
@@ -827,7 +845,7 @@ async fn load_project_layers(
layers.push(project_layer_entry(
trust_context,
&dot_codex_abs,
&layer_dir,
&dir,
TomlValue::Table(toml::map::Map::new()),
/*config_toml_exists*/ true,
));
@@ -839,7 +857,7 @@ async fn load_project_layers(
let entry = project_layer_entry(
trust_context,
&dot_codex_abs,
&layer_dir,
&dir,
config,
/*config_toml_exists*/ true,
);
@@ -853,7 +871,7 @@ async fn load_project_layers(
layers.push(project_layer_entry(
trust_context,
&dot_codex_abs,
&layer_dir,
&dir,
TomlValue::Table(toml::map::Map::new()),
/*config_toml_exists*/ false,
));
+33 -2
View File
@@ -16,6 +16,7 @@ use crate::config_loader::version_for_toml;
use codex_config::CONFIG_TOML_FILE;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
use codex_exec_server::LOCAL_FS;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::protocol::AskForApproval;
@@ -92,6 +93,7 @@ async fn returns_config_error_for_invalid_user_config_toml() {
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let err = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -119,6 +121,7 @@ async fn returns_config_error_for_invalid_managed_config_toml() {
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let err = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -203,6 +206,7 @@ extra = true
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let state = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -235,6 +239,7 @@ async fn returns_empty_when_all_layers_missing() {
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -327,6 +332,7 @@ flag = false
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let state = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -428,6 +434,7 @@ allowed_sandbox_modes = ["read-only"]
);
let state = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
@@ -489,6 +496,7 @@ allowed_approval_policies = ["never"]
);
let state = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
@@ -529,8 +537,14 @@ personality = true
)
.await?;
let requirements_file = AbsolutePathBuf::try_from(requirements_file)?;
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?;
load_requirements_toml(
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&requirements_file,
)
.await?;
assert_eq!(
config_requirements_toml
@@ -620,6 +634,7 @@ allowed_approval_policies = ["on-request"]
),
);
let state = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
@@ -691,7 +706,12 @@ allowed_approval_policies = ["on-request"]
guardian_policy_config: None,
},
);
load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?;
load_requirements_toml(
LOCAL_FS.as_ref(),
&mut config_requirements_toml,
&AbsolutePathBuf::try_from(requirements_file)?,
)
.await?;
assert_eq!(
config_requirements_toml
@@ -735,6 +755,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
let cloud_requirements = CloudRequirementsLoader::new(async move { Ok(Some(requirements)) });
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -771,6 +792,7 @@ async fn load_config_layers_fails_when_cloud_requirements_loader_fails() -> anyh
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?;
let err = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -823,6 +845,7 @@ async fn project_layers_prefer_closest_cwd() -> std::io::Result<()> {
.await?;
let cwd = AbsolutePathBuf::from_absolute_path(&nested)?;
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -967,6 +990,7 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s
.await?;
let cwd = AbsolutePathBuf::from_absolute_path(&nested)?;
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -1006,6 +1030,7 @@ async fn codex_home_is_not_loaded_as_project_layer_from_home_dir() -> std::io::R
let cwd = AbsolutePathBuf::from_absolute_path(&home_dir)?;
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -1062,6 +1087,7 @@ async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Resul
let cwd = AbsolutePathBuf::from_absolute_path(&nested)?;
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&project_dot_codex,
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -1132,6 +1158,7 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<
.await?;
let layers_untrusted = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home_untrusted,
Some(cwd.clone()),
&[] as &[(String, TomlValue)],
@@ -1170,6 +1197,7 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<
.await?;
let layers_unknown = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home_unknown,
Some(cwd),
&[] as &[(String, TomlValue)],
@@ -1328,6 +1356,7 @@ async fn invalid_project_config_ignored_when_untrusted_or_unknown() -> std::io::
}
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd.clone()),
&[] as &[(String, TomlValue)],
@@ -1390,6 +1419,7 @@ async fn cli_overrides_with_relative_paths_do_not_break_trust_check() -> std::io
)];
load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&cli_overrides,
@@ -1432,6 +1462,7 @@ async fn project_root_markers_supports_alternate_markers() -> std::io::Result<()
let cwd = AbsolutePathBuf::from_absolute_path(&nested)?;
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
+44 -23
View File
@@ -1,3 +1,4 @@
use codex_exec_server::LOCAL_FS;
use codex_git_utils::GitInfo;
use codex_git_utils::GitSha;
use codex_git_utils::collect_git_info;
@@ -5,6 +6,9 @@ use codex_git_utils::get_has_changes;
use codex_git_utils::git_diff_to_remote;
use codex_git_utils::recent_commits;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_utils_path::normalize_for_path_comparison;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use core_test_support::skip_if_sandbox;
use std::fs;
use std::path::PathBuf;
@@ -430,7 +434,7 @@ async fn test_get_git_working_tree_state_branch_fallback() {
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())
resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &tmp.path().abs())
.await
.is_none()
);
@@ -439,18 +443,17 @@ async fn resolve_root_git_project_for_trust_returns_none_outside_repo() {
#[tokio::test]
async fn resolve_root_git_project_for_trust_regular_repo_returns_repo_root() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let repo_path = create_test_git_repo(&temp_dir).await;
let expected = std::fs::canonicalize(&repo_path).unwrap();
let repo_path = create_test_git_repo(&temp_dir).await.abs();
assert_eq!(
resolve_root_git_project_for_trust(&repo_path).await,
Some(expected.clone())
resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &repo_path).await,
Some(repo_path.clone())
);
let nested = repo_path.join("sub/dir");
std::fs::create_dir_all(&nested).unwrap();
std::fs::create_dir_all(nested.as_path()).unwrap();
assert_eq!(
resolve_root_git_project_for_trust(&nested).await,
Some(expected)
resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &nested).await,
Some(repo_path)
);
}
@@ -473,17 +476,27 @@ async fn resolve_root_git_project_for_trust_detects_worktree_and_returns_main_ro
.output()
.expect("git worktree add");
let expected = std::fs::canonicalize(&repo_path).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 expected = normalize_for_path_comparison(&repo_path).unwrap();
let wt_root = wt_root.abs();
let got = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &wt_root).await;
assert_eq!(
got.as_ref()
.map(normalize_for_path_comparison)
.transpose()
.unwrap(),
Some(expected.clone())
);
let nested = wt_root.join("nested/sub");
std::fs::create_dir_all(&nested).unwrap();
let got_nested = resolve_root_git_project_for_trust(&nested)
.await
.and_then(|p| std::fs::canonicalize(p).ok());
assert_eq!(got_nested, expected);
std::fs::create_dir_all(nested.as_path()).unwrap();
let got_nested = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &nested).await;
assert_eq!(
got_nested
.as_ref()
.map(normalize_for_path_comparison)
.transpose()
.unwrap(),
Some(expected)
);
}
#[tokio::test]
@@ -502,13 +515,15 @@ async fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git
)
.unwrap();
let expected = std::fs::canonicalize(&repo_root).unwrap();
let expected = repo_root.abs();
let worktree_root = worktree_root.abs();
assert_eq!(
resolve_root_git_project_for_trust(&worktree_root).await,
resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &worktree_root).await,
Some(expected.clone())
);
let nested = worktree_root.join("nested");
assert_eq!(
resolve_root_git_project_for_trust(&worktree_root.join("nested")).await,
resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &nested).await,
Some(expected)
);
}
@@ -529,9 +544,15 @@ async fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none()
)
.unwrap();
assert!(resolve_root_git_project_for_trust(&proj).await.is_none());
let proj = proj.abs();
assert!(
resolve_root_git_project_for_trust(&proj.join("nested"))
resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &proj)
.await
.is_none()
);
let nested = proj.join("nested");
assert!(
resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &nested)
.await
.is_none()
);
+3
View File
@@ -16,6 +16,7 @@ use crate::config_loader::RequirementSource;
use crate::config_loader::Sourced;
use crate::test_support;
use codex_config::config_toml::ConfigToml;
use codex_exec_server::LOCAL_FS;
use codex_network_proxy::NetworkProxyConfig;
use codex_protocol::ThreadId;
use codex_protocol::approvals::NetworkApprovalProtocol;
@@ -1740,6 +1741,7 @@ async fn guardian_review_session_config_uses_requirements_guardian_policy_config
)
.expect("config layer stack");
let parent_config = Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
ConfigToml::default(),
ConfigOverrides {
cwd: Some(workspace.path().to_path_buf()),
@@ -1776,6 +1778,7 @@ async fn guardian_review_session_config_uses_default_guardian_policy_without_req
ConfigLayerStack::new(Vec::new(), Default::default(), Default::default())
.expect("config layer stack");
let parent_config = Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
ConfigToml::default(),
ConfigOverrides {
cwd: Some(workspace.path().to_path_buf()),
@@ -16,6 +16,7 @@ use codex_config::CONFIG_TOML_FILE;
use codex_config::permissions_toml::NetworkToml;
use codex_config::permissions_toml::PermissionsToml;
use codex_config::permissions_toml::overlay_network_domain_permissions;
use codex_exec_server::LOCAL_FS;
use codex_network_proxy::ConfigReloader;
use codex_network_proxy::ConfigState;
use codex_network_proxy::NetworkProxyConfig;
@@ -46,6 +47,7 @@ async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec<LayerMtime
let cli_overrides = Vec::new();
let overrides = LoaderOverrides::default();
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
/*cwd*/ None,
&cli_overrides,
+38 -22
View File
@@ -2,12 +2,14 @@ use crate::codex::Session;
use crate::compact::content_items_to_text;
use crate::event_mapping::is_contextual_user_message_content;
use chrono::Utc;
use codex_exec_server::LOCAL_FS;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_protocol::models::ResponseItem;
use codex_thread_store::ListThreadsParams;
use codex_thread_store::StoredThread;
use codex_thread_store::ThreadSortKey;
use codex_thread_store::ThreadStore;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_output_truncation::truncate_text;
use dirs::home_dir;
@@ -145,18 +147,26 @@ async fn load_recent_threads(sess: &Session) -> Vec<StoredThread> {
}
}
async fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Option<String> {
async fn build_recent_work_section(
cwd: &AbsolutePathBuf,
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)
.await
.unwrap_or_else(|| entry.cwd.clone());
let group = match AbsolutePathBuf::from_absolute_path(entry.cwd.as_path()) {
Ok(entry_cwd) => resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &entry_cwd)
.await
.map(AbsolutePathBuf::into_path_buf)
.unwrap_or_else(|| entry.cwd.clone()),
Err(_) => entry.cwd.clone(),
};
groups.entry(group).or_default().push(entry);
}
let current_group = resolve_root_git_project_for_trust(cwd)
let current_group = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), cwd)
.await
.unwrap_or_else(|| cwd.to_path_buf());
.map(AbsolutePathBuf::into_path_buf)
.unwrap_or_else(|| cwd.clone().into_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
@@ -309,18 +319,19 @@ pub(crate) fn truncate_realtime_text_to_token_budget(text: &str, budget_tokens:
}
async fn build_workspace_section_with_user_root(
cwd: &Path,
cwd: &AbsolutePathBuf,
user_root: Option<PathBuf>,
) -> Option<String> {
let git_root = resolve_root_git_project_for_trust(cwd).await;
let cwd_tree = render_tree(cwd);
let cwd_path = cwd.as_path();
let git_root = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), cwd).await;
let cwd_tree = render_tree(cwd_path);
let git_root_tree = git_root
.as_ref()
.filter(|git_root| git_root.as_path() != cwd)
.and_then(|git_root| render_tree(git_root));
.filter(|git_root| git_root.as_path() != cwd_path)
.and_then(|git_root| render_tree(git_root.as_path()));
let user_root_tree = user_root
.as_ref()
.filter(|user_root| user_root.as_path() != cwd)
.filter(|user_root| user_root.as_path() != cwd_path)
.filter(|user_root| {
git_root
.as_ref()
@@ -333,8 +344,8 @@ async fn build_workspace_section_with_user_root(
}
let mut lines = vec![
format!("Current working directory: {}", cwd.display()),
format!("Working directory name: {}", file_name_string(cwd)),
format!("Current working directory: {}", cwd_path.display()),
format!("Working directory name: {}", file_name_string(cwd_path)),
];
if let Some(git_root) = &git_root {
@@ -465,14 +476,19 @@ async fn format_thread_group(
entries: Vec<&StoredThread>,
) -> Option<String> {
let latest = entries.first()?;
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())
};
let group_label =
if let Ok(latest_cwd) = AbsolutePathBuf::from_absolute_path(latest.cwd.as_path()) {
if resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &latest_cwd)
.await
.is_some()
{
format!("### Git repo: {}", group.display())
} else {
format!("### Directory: {}", group.display())
}
} else {
format!("### Directory: {}", group.display())
};
let mut lines = vec![
group_label,
format!("Recent sessions: {}", entries.len()),
+10 -7
View File
@@ -19,6 +19,8 @@ use codex_protocol::protocol::GitInfo;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_thread_store::StoredThread;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::PathBuf;
@@ -234,7 +236,7 @@ fn fixed_section_budgets_apply_per_section_without_total_blob_truncation() {
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).await,
build_workspace_section_with_user_root(&cwd.path().abs(), /*user_root*/ None).await,
None
);
}
@@ -245,9 +247,10 @@ async fn workspace_section_includes_tree_when_entries_exist() {
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");
let section =
build_workspace_section_with_user_root(&cwd.path().abs(), /*user_root*/ None)
.await
.expect("workspace section");
assert!(section.contains("Working directory tree:"));
assert!(section.contains("- docs/"));
assert!(section.contains("- README.md"));
@@ -267,7 +270,7 @@ async fn workspace_section_includes_user_root_tree_when_distinct() {
fs::create_dir_all(user_root.join("code")).expect("create user root child");
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))
let section = build_workspace_section_with_user_root(&cwd.abs(), Some(user_root))
.await
.expect("workspace section");
assert!(section.contains("User root tree:"));
@@ -309,9 +312,9 @@ async fn recent_work_section_groups_threads_by_cwd() {
stored_thread(outside.to_string_lossy().as_ref(), "", "Inspect flaky test"),
];
let current_cwd = workspace_a;
let repo = fs::canonicalize(repo).expect("canonicalize repo");
let repo = repo.abs();
let section = build_recent_work_section(current_cwd.as_path(), &recent_threads)
let section = build_recent_work_section(&current_cwd.abs(), &recent_threads)
.await
.expect("recent work section");
assert!(section.contains(&format!("### Git repo: {}", repo.display())));
+2
View File
@@ -9,6 +9,8 @@ readme = "README.md"
workspace = true
[dependencies]
codex-exec-server = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
futures = { workspace = true, features = ["alloc"] }
once_cell = { workspace = true }
+32 -14
View File
@@ -4,6 +4,7 @@ use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
use codex_exec_server::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use futures::future::join_all;
use schemars::JsonSchema;
@@ -618,30 +619,38 @@ 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 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() {
return Some(canonicalize_or_raw(repo_root));
pub async fn resolve_root_git_project_for_trust(
fs: &dyn ExecutorFileSystem,
cwd: &AbsolutePathBuf,
) -> Option<AbsolutePathBuf> {
let base = match fs.get_metadata(cwd, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_directory => cwd.clone(),
_ => cwd.parent()?,
};
let (repo_root, dot_git) = find_ancestor_git_entry_with_fs(fs, &base).await?;
if fs
.get_metadata(&dot_git, /*sandbox*/ None)
.await
.ok()?
.is_directory
{
return Some(repo_root);
}
let git_dir_s = std::fs::read_to_string(&dot_git).ok()?;
let git_dir_s = fs.read_file_text(&dot_git, /*sandbox*/ None).await.ok()?;
let git_dir_rel = git_dir_s.trim().strip_prefix("gitdir:")?.trim();
if git_dir_rel.is_empty() {
return None;
}
let git_dir_path = canonicalize_or_raw(
AbsolutePathBuf::resolve_path_against_base(git_dir_rel, &repo_root).into_path_buf(),
);
let git_dir_path = AbsolutePathBuf::resolve_path_against_base(git_dir_rel, repo_root.as_path());
let worktrees_dir = git_dir_path.parent()?;
if worktrees_dir.file_name() != Some(OsStr::new("worktrees")) {
if worktrees_dir.as_path().file_name() != Some(OsStr::new("worktrees")) {
return None;
}
let common_dir = worktrees_dir.parent()?;
let main_repo_root = common_dir.parent()?;
Some(canonicalize_or_raw(main_repo_root.to_path_buf()))
common_dir.parent()
}
fn find_ancestor_git_entry(base_dir: &Path) -> Option<(PathBuf, PathBuf)> {
@@ -663,8 +672,17 @@ fn find_ancestor_git_entry(base_dir: &Path) -> Option<(PathBuf, PathBuf)> {
None
}
fn canonicalize_or_raw(path: PathBuf) -> PathBuf {
std::fs::canonicalize(&path).unwrap_or(path)
async fn find_ancestor_git_entry_with_fs(
fs: &dyn ExecutorFileSystem,
base_dir: &AbsolutePathBuf,
) -> Option<(AbsolutePathBuf, AbsolutePathBuf)> {
for dir in base_dir.ancestors() {
let dot_git = dir.join(".git");
if fs.get_metadata(&dot_git, /*sandbox*/ None).await.is_ok() {
return Some((dir, dot_git));
}
}
None
}
/// Returns a list of local git branches.
+2 -72
View File
@@ -1,6 +1,3 @@
use std::fmt;
use std::path::PathBuf;
mod apply;
mod branch;
mod errors;
@@ -16,6 +13,8 @@ pub use apply::extract_paths_from_patch;
pub use apply::parse_git_apply_output;
pub use apply::stage_paths;
pub use branch::merge_base_with_head;
pub use codex_protocol::models::GhostCommit;
pub use codex_protocol::protocol::GitSha;
pub use errors::GitToolingError;
pub use ghost_commits::CreateGhostCommitOptions;
pub use ghost_commits::GhostSnapshotConfig;
@@ -45,72 +44,3 @@ pub use info::local_git_branches;
pub use info::recent_commits;
pub use info::resolve_root_git_project_for_trust;
pub use platform::create_symlink;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
type CommitID = String;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
#[serde(transparent)]
#[ts(type = "string")]
pub struct GitSha(pub String);
impl GitSha {
pub fn new(sha: &str) -> Self {
Self(sha.to_string())
}
}
/// Details of a ghost commit created from a repository state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct GhostCommit {
id: CommitID,
parent: Option<CommitID>,
preexisting_untracked_files: Vec<PathBuf>,
preexisting_untracked_dirs: Vec<PathBuf>,
}
impl GhostCommit {
/// Create a new ghost commit wrapper from a raw commit ID and optional parent.
pub fn new(
id: CommitID,
parent: Option<CommitID>,
preexisting_untracked_files: Vec<PathBuf>,
preexisting_untracked_dirs: Vec<PathBuf>,
) -> Self {
Self {
id,
parent,
preexisting_untracked_files,
preexisting_untracked_dirs,
}
}
/// Commit ID for the snapshot.
pub fn id(&self) -> &str {
&self.id
}
/// Parent commit ID, if the repository had a `HEAD` at creation time.
pub fn parent(&self) -> Option<&str> {
self.parent.as_deref()
}
/// Untracked or ignored files that already existed when the snapshot was captured.
pub fn preexisting_untracked_files(&self) -> &[PathBuf] {
&self.preexisting_untracked_files
}
/// Untracked or ignored directories that already existed when the snapshot was captured.
pub fn preexisting_untracked_dirs(&self) -> &[PathBuf] {
&self.preexisting_untracked_dirs
}
}
impl fmt::Display for GhostCommit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.id)
}
}
-1
View File
@@ -16,7 +16,6 @@ chardetng = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
codex-async-utils = { workspace = true }
codex-execpolicy = { workspace = true }
codex-git-utils = { workspace = true }
codex-network-proxy = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-image = { workspace = true }
+56 -1
View File
@@ -1,5 +1,7 @@
use std::collections::HashMap;
use std::fmt;
use std::path::Path;
use std::path::PathBuf;
use std::sync::LazyLock;
use codex_utils_image::PromptImageMode;
@@ -25,7 +27,6 @@ use crate::protocol::SandboxPolicy;
use crate::protocol::WritableRoot;
use crate::user_input::UserInput;
use codex_execpolicy::Policy;
use codex_git_utils::GhostCommit;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_image::ImageProcessingError;
use schemars::JsonSchema;
@@ -45,6 +46,60 @@ static SANDBOX_MODE_READ_ONLY_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
.unwrap_or_else(|err| panic!("read-only sandbox template must parse: {err}"))
});
type CommitID = String;
/// Details of a ghost commit created from a repository state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct GhostCommit {
id: CommitID,
parent: Option<CommitID>,
preexisting_untracked_files: Vec<PathBuf>,
preexisting_untracked_dirs: Vec<PathBuf>,
}
impl GhostCommit {
/// Create a new ghost commit wrapper from a raw commit ID and optional parent.
pub fn new(
id: CommitID,
parent: Option<CommitID>,
preexisting_untracked_files: Vec<PathBuf>,
preexisting_untracked_dirs: Vec<PathBuf>,
) -> Self {
Self {
id,
parent,
preexisting_untracked_files,
preexisting_untracked_dirs,
}
}
/// Commit ID for the snapshot.
pub fn id(&self) -> &str {
&self.id
}
/// Parent commit ID, if the repository had a `HEAD` at creation time.
pub fn parent(&self) -> Option<&str> {
self.parent.as_deref()
}
/// Untracked or ignored files that already existed when the snapshot was captured.
pub fn preexisting_untracked_files(&self) -> &[PathBuf] {
&self.preexisting_untracked_files
}
/// Untracked or ignored directories that already existed when the snapshot was captured.
pub fn preexisting_untracked_dirs(&self) -> &[PathBuf] {
&self.preexisting_untracked_dirs
}
}
impl fmt::Display for GhostCommit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.id)
}
}
/// Controls the per-command sandbox override requested by a shell-like tool call.
#[derive(
Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS,
+11 -1
View File
@@ -49,7 +49,6 @@ use crate::request_permissions::RequestPermissionsEvent;
use crate::request_permissions::RequestPermissionsResponse;
use crate::request_user_input::RequestUserInputResponse;
use crate::user_input::UserInput;
use codex_git_utils::GitSha;
use codex_utils_absolute_path::AbsolutePathBuf;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -103,6 +102,17 @@ pub const REALTIME_CONVERSATION_OPEN_TAG: &str = "<realtime_conversation>";
pub const REALTIME_CONVERSATION_CLOSE_TAG: &str = "</realtime_conversation>";
pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
#[serde(transparent)]
#[ts(type = "string")]
pub struct GitSha(pub String);
impl GitSha {
pub fn new(sha: &str) -> Self {
Self(sha.to_string())
}
}
/// Submission Queue Entry - requests from user
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Submission {
@@ -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_exec_server::LOCAL_FS;
use codex_git_utils::resolve_root_git_project_for_trust;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
@@ -123,8 +124,9 @@ 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)
let trust_target = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &config.cwd)
.await
.map(Into::into)
.unwrap_or_else(|| cwd.clone());
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
cwd,