mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Refactor config loading to use filesystem abstraction (#18209)
Initial pass propagating FileSystem through config loading.
This commit is contained in:
committed by
GitHub
Unverified
parent
2967900d81
commit
9effa0509f
Generated
+3
-4
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(¤t_cwd.abs(), &recent_threads)
|
||||
.await
|
||||
.expect("recent work section");
|
||||
assert!(section.contains(&format!("### Git repo: {}", repo.display())));
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user