mirror of
https://github.com/openai/codex.git
synced 2026-05-02 02:17:22 +00:00
Compare commits
2 Commits
iceweasel/
...
dev/rasmus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e5f10f1b6 | ||
|
|
ce0a0d2c60 |
15
codex-rs/Cargo.lock
generated
15
codex-rs/Cargo.lock
generated
@@ -1875,6 +1875,20 @@ dependencies = [
|
||||
"wildmatch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-config-store"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-connectors"
|
||||
version = "0.0.0"
|
||||
@@ -1908,6 +1922,7 @@ dependencies = [
|
||||
"codex-async-utils",
|
||||
"codex-code-mode",
|
||||
"codex-config",
|
||||
"codex-config-store",
|
||||
"codex-connectors",
|
||||
"codex-core-skills",
|
||||
"codex-exec-server",
|
||||
|
||||
@@ -23,6 +23,7 @@ members = [
|
||||
"collaboration-mode-templates",
|
||||
"connectors",
|
||||
"config",
|
||||
"config-store",
|
||||
"shell-command",
|
||||
"shell-escalation",
|
||||
"skills",
|
||||
@@ -125,6 +126,7 @@ codex-cloud-tasks-client = { path = "cloud-tasks-client" }
|
||||
codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" }
|
||||
codex-code-mode = { path = "code-mode" }
|
||||
codex-config = { path = "config" }
|
||||
codex-config-store = { path = "config-store" }
|
||||
codex-connectors = { path = "connectors" }
|
||||
codex-core = { path = "core" }
|
||||
codex-core-skills = { path = "core-skills" }
|
||||
|
||||
6
codex-rs/config-store/BUILD.bazel
Normal file
6
codex-rs/config-store/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "config-store",
|
||||
crate_name = "codex_config_store",
|
||||
)
|
||||
25
codex-rs/config-store/Cargo.toml
Normal file
25
codex-rs/config-store/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "codex-config-store"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_config_store"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
toml = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||
27
codex-rs/config-store/src/error.rs
Normal file
27
codex-rs/config-store/src/error.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
/// Result type returned by config-store operations.
|
||||
pub type ConfigStoreResult<T> = Result<T, ConfigStoreError>;
|
||||
|
||||
/// Error type shared by config-store implementations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigStoreError {
|
||||
/// The caller supplied invalid request data.
|
||||
#[error("invalid config-store request: {message}")]
|
||||
InvalidRequest {
|
||||
/// User-facing explanation of the invalid request.
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// A backing source could not be queried.
|
||||
#[error("config-store read failed: {message}")]
|
||||
ReadFailed {
|
||||
/// User-facing explanation of the read failure.
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Catch-all for implementation failures that do not fit a more specific category.
|
||||
#[error("config-store internal error: {message}")]
|
||||
Internal {
|
||||
/// User-facing explanation of the implementation failure.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
21
codex-rs/config-store/src/lib.rs
Normal file
21
codex-rs/config-store/src/lib.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
//! Storage-neutral interfaces for loading config-layer documents.
|
||||
//!
|
||||
//! Implementations should report observations from their backing store. Codex config loading
|
||||
//! remains responsible for applying precedence, project trust, path resolution, requirements, and
|
||||
//! final layer merging.
|
||||
//!
|
||||
//! The request and response types in this crate may cross process or network boundaries. Keep them
|
||||
//! wire-friendly: prefer primitive fields over Rust-specific error or filesystem types.
|
||||
|
||||
mod error;
|
||||
mod local;
|
||||
mod store;
|
||||
mod types;
|
||||
|
||||
pub use error::ConfigStoreError;
|
||||
pub use error::ConfigStoreResult;
|
||||
pub use local::LocalConfigStore;
|
||||
pub use store::ConfigDocumentStore;
|
||||
pub use types::ConfigDocumentErrorSpan;
|
||||
pub use types::ConfigDocumentRead;
|
||||
pub use types::ReadConfigDocumentParams;
|
||||
125
codex-rs/config-store/src/local.rs
Normal file
125
codex-rs/config-store/src/local.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::ConfigDocumentErrorSpan;
|
||||
use crate::ConfigDocumentRead;
|
||||
use crate::ConfigDocumentStore;
|
||||
use crate::ConfigStoreResult;
|
||||
use crate::ReadConfigDocumentParams;
|
||||
|
||||
/// Filesystem-backed implementation of [ConfigDocumentStore].
|
||||
///
|
||||
/// This implementation reads the requested path from the local filesystem and parses it as TOML.
|
||||
/// It does not apply config-layer ordering, project trust, relative-path resolution, or fallback
|
||||
/// behavior for missing documents.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct LocalConfigStore;
|
||||
|
||||
#[async_trait]
|
||||
impl ConfigDocumentStore for LocalConfigStore {
|
||||
async fn read_config_document(
|
||||
&self,
|
||||
params: ReadConfigDocumentParams,
|
||||
) -> ConfigStoreResult<ConfigDocumentRead> {
|
||||
match tokio::fs::read_to_string(params.path.as_path()).await {
|
||||
Ok(raw_toml) => match toml::from_str(&raw_toml) {
|
||||
Ok(value) => Ok(ConfigDocumentRead::Present { value }),
|
||||
Err(error) => Ok(ConfigDocumentRead::ParseError {
|
||||
raw_toml,
|
||||
message: error.message().to_string(),
|
||||
span: error.span().map(ConfigDocumentErrorSpan::from),
|
||||
}),
|
||||
},
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
Ok(ConfigDocumentRead::Missing)
|
||||
}
|
||||
Err(err) => Ok(ConfigDocumentRead::ReadError {
|
||||
kind: read_error_kind_name(err.kind()).to_string(),
|
||||
message: err.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_error_kind_name(kind: std::io::ErrorKind) -> &'static str {
|
||||
match kind {
|
||||
std::io::ErrorKind::PermissionDenied => "permission_denied",
|
||||
std::io::ErrorKind::InvalidData => "invalid_data",
|
||||
std::io::ErrorKind::InvalidInput => "invalid_input",
|
||||
std::io::ErrorKind::TimedOut => "timed_out",
|
||||
std::io::ErrorKind::Interrupted => "interrupted",
|
||||
std::io::ErrorKind::Unsupported => "unsupported",
|
||||
_ => "other",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
use super::*;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
fn read_params(path: AbsolutePathBuf) -> ReadConfigDocumentParams {
|
||||
ReadConfigDocumentParams { path }
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reads_present_config_document() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let path = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("config.toml"))?;
|
||||
tokio::fs::write(path.as_path(), "model = \"gpt-5\"\n").await?;
|
||||
|
||||
let got = LocalConfigStore
|
||||
.read_config_document(read_params(path))
|
||||
.await?;
|
||||
|
||||
let expected = ConfigDocumentRead::Present {
|
||||
value: TomlValue::Table(toml::map::Map::from_iter([(
|
||||
"model".to_string(),
|
||||
TomlValue::String("gpt-5".to_string()),
|
||||
)])),
|
||||
};
|
||||
assert_eq!(got, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reports_missing_config_document() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let path = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("config.toml"))?;
|
||||
|
||||
let got = LocalConfigStore
|
||||
.read_config_document(read_params(path))
|
||||
.await?;
|
||||
|
||||
assert_eq!(got, ConfigDocumentRead::Missing);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reports_parse_error_with_raw_toml() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let path = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("config.toml"))?;
|
||||
let raw_toml = "model = [";
|
||||
tokio::fs::write(path.as_path(), raw_toml).await?;
|
||||
|
||||
let got = LocalConfigStore
|
||||
.read_config_document(read_params(path))
|
||||
.await?;
|
||||
|
||||
match got {
|
||||
ConfigDocumentRead::ParseError {
|
||||
raw_toml: got_raw,
|
||||
message,
|
||||
span,
|
||||
} => {
|
||||
assert_eq!(got_raw, raw_toml);
|
||||
assert!(!message.is_empty());
|
||||
assert!(span.is_some());
|
||||
}
|
||||
other => panic!("expected parse error, got {other:?}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
20
codex-rs/config-store/src/store.rs
Normal file
20
codex-rs/config-store/src/store.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::ConfigDocumentRead;
|
||||
use crate::ConfigStoreResult;
|
||||
use crate::ReadConfigDocumentParams;
|
||||
|
||||
/// Storage-neutral reader for path-addressed config documents.
|
||||
///
|
||||
/// Implementations should only read and parse the requested document. Codex config loading remains
|
||||
/// responsible for deciding what the document represents, how missing documents are handled, how
|
||||
/// parse errors interact with project trust, how relative paths are resolved, and how layers are
|
||||
/// ordered and merged.
|
||||
#[async_trait]
|
||||
pub trait ConfigDocumentStore: Send + Sync {
|
||||
/// Reads one config document addressed by path.
|
||||
async fn read_config_document(
|
||||
&self,
|
||||
params: ReadConfigDocumentParams,
|
||||
) -> ConfigStoreResult<ConfigDocumentRead>;
|
||||
}
|
||||
119
codex-rs/config-store/src/types.rs
Normal file
119
codex-rs/config-store/src/types.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::ops::Range;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
/// Request to read one path-addressed config document.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ReadConfigDocumentParams {
|
||||
/// Absolute path to the config document to read.
|
||||
pub path: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
/// Byte span for a config document parse error.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ConfigDocumentErrorSpan {
|
||||
/// Inclusive byte offset where the error starts.
|
||||
pub start: usize,
|
||||
|
||||
/// Exclusive byte offset where the error ends.
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl From<Range<usize>> for ConfigDocumentErrorSpan {
|
||||
fn from(span: Range<usize>) -> Self {
|
||||
Self {
|
||||
start: span.start,
|
||||
end: span.end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigDocumentErrorSpan> for Range<usize> {
|
||||
fn from(span: ConfigDocumentErrorSpan) -> Self {
|
||||
span.start..span.end
|
||||
}
|
||||
}
|
||||
|
||||
/// Read and parse state for one config document.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "status", rename_all = "snake_case")]
|
||||
pub enum ConfigDocumentRead {
|
||||
/// The backing source was absent.
|
||||
Missing,
|
||||
|
||||
/// The backing source was present and parsed successfully.
|
||||
Present {
|
||||
/// Parsed TOML document.
|
||||
value: TomlValue,
|
||||
},
|
||||
|
||||
/// The backing source was present but could not be parsed as TOML.
|
||||
///
|
||||
/// This is distinct from ConfigDocumentRead::ReadError because project config parse errors are
|
||||
/// fatal only after Codex applies project trust policy.
|
||||
ParseError {
|
||||
/// Original TOML text that failed to parse.
|
||||
raw_toml: String,
|
||||
|
||||
/// User-facing parse failure message.
|
||||
message: String,
|
||||
|
||||
/// Optional byte span for the parse failure.
|
||||
span: Option<ConfigDocumentErrorSpan>,
|
||||
},
|
||||
|
||||
/// The provider could not read the backing source.
|
||||
ReadError {
|
||||
/// Primitive read failure kind, such as "permission_denied" or "other".
|
||||
kind: String,
|
||||
|
||||
/// User-facing read failure message.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_trait::async_trait;
|
||||
use pretty_assertions::assert_eq;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
use super::*;
|
||||
use crate::ConfigDocumentStore;
|
||||
use crate::ConfigStoreResult;
|
||||
|
||||
struct StaticDocumentStore {
|
||||
document: ConfigDocumentRead,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ConfigDocumentStore for StaticDocumentStore {
|
||||
async fn read_config_document(
|
||||
&self,
|
||||
_params: ReadConfigDocumentParams,
|
||||
) -> ConfigStoreResult<ConfigDocumentRead> {
|
||||
Ok(self.document.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn store_trait_can_return_config_documents() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let path =
|
||||
AbsolutePathBuf::from_absolute_path(temp_dir.path().join("config.toml")).expect("abs");
|
||||
let value = TomlValue::Table(toml::map::Map::new());
|
||||
let document = ConfigDocumentRead::Present { value };
|
||||
let store = StaticDocumentStore {
|
||||
document: document.clone(),
|
||||
};
|
||||
|
||||
let got = store
|
||||
.read_config_document(ReadConfigDocumentParams { path })
|
||||
.await
|
||||
.expect("read document");
|
||||
|
||||
assert_eq!(got, document);
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,18 @@ pub fn config_error_from_toml(
|
||||
ConfigError::new(path.as_ref().to_path_buf(), range, err.message())
|
||||
}
|
||||
|
||||
pub fn config_error_from_parse_message(
|
||||
path: impl AsRef<Path>,
|
||||
contents: &str,
|
||||
message: impl Into<String>,
|
||||
span: Option<std::ops::Range<usize>>,
|
||||
) -> ConfigError {
|
||||
let range = span
|
||||
.map(|span| text_range_from_span(contents, span))
|
||||
.unwrap_or_else(default_range);
|
||||
ConfigError::new(path.as_ref().to_path_buf(), range, message)
|
||||
}
|
||||
|
||||
pub fn config_error_from_typed_toml<T: DeserializeOwned>(
|
||||
path: impl AsRef<Path>,
|
||||
contents: &str,
|
||||
|
||||
@@ -51,6 +51,7 @@ pub use diagnostics::ConfigError;
|
||||
pub use diagnostics::ConfigLoadError;
|
||||
pub use diagnostics::TextPosition;
|
||||
pub use diagnostics::TextRange;
|
||||
pub use diagnostics::config_error_from_parse_message;
|
||||
pub use diagnostics::config_error_from_toml;
|
||||
pub use diagnostics::config_error_from_typed_toml;
|
||||
pub use diagnostics::first_layer_config_error;
|
||||
|
||||
@@ -33,6 +33,7 @@ codex-async-utils = { workspace = true }
|
||||
codex-code-mode = { workspace = true }
|
||||
codex-connectors = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-config-store = { workspace = true }
|
||||
codex-core-skills = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
|
||||
@@ -11,6 +11,11 @@ use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigRequirementsWithSources;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
use codex_config_store::ConfigDocumentRead;
|
||||
use codex_config_store::ConfigDocumentStore;
|
||||
use codex_config_store::ConfigStoreError;
|
||||
use codex_config_store::LocalConfigStore;
|
||||
use codex_config_store::ReadConfigDocumentParams;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
@@ -57,6 +62,8 @@ pub use codex_config::TextPosition;
|
||||
pub use codex_config::TextRange;
|
||||
pub use codex_config::WebSearchModeRequirement;
|
||||
pub(crate) use codex_config::build_cli_overrides_layer;
|
||||
pub(crate) use codex_config::config_error_from_parse_message;
|
||||
#[cfg(test)]
|
||||
pub(crate) use codex_config::config_error_from_toml;
|
||||
pub use codex_config::default_project_root_markers;
|
||||
pub use codex_config::format_config_error;
|
||||
@@ -123,6 +130,28 @@ pub async fn load_config_layers_state(
|
||||
cli_overrides: &[(String, TomlValue)],
|
||||
overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
) -> io::Result<ConfigLayerStack> {
|
||||
let config_store = LocalConfigStore;
|
||||
load_config_layers_state_with_store(
|
||||
codex_home,
|
||||
cwd,
|
||||
cli_overrides,
|
||||
overrides,
|
||||
cloud_requirements,
|
||||
&config_store,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Equivalent to [load_config_layers_state], using the supplied store for path-addressed
|
||||
/// config.toml reads.
|
||||
pub async fn load_config_layers_state_with_store(
|
||||
codex_home: &Path,
|
||||
cwd: Option<AbsolutePathBuf>,
|
||||
cli_overrides: &[(String, TomlValue)],
|
||||
overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
config_store: &(impl ConfigDocumentStore + ?Sized),
|
||||
) -> io::Result<ConfigLayerStack> {
|
||||
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||
|
||||
@@ -172,23 +201,26 @@ pub async fn load_config_layers_state(
|
||||
// Include an entry for the "system" config folder, loading its config.toml,
|
||||
// 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| {
|
||||
let system_layer = load_config_toml_for_required_layer(
|
||||
config_store,
|
||||
&system_config_toml_file,
|
||||
|config_toml| {
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::System {
|
||||
file: system_config_toml_file.clone(),
|
||||
},
|
||||
config_toml,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
layers.push(system_layer);
|
||||
|
||||
// Add a layer for $CODEX_HOME/config.toml if it exists. Note if the file
|
||||
// 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(config_store, &user_file, |config_toml| {
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
@@ -247,6 +279,7 @@ pub async fn load_config_layers_state(
|
||||
}
|
||||
};
|
||||
let project_layers = load_project_layers(
|
||||
config_store,
|
||||
&cwd,
|
||||
&project_trust_context.project_root,
|
||||
&project_trust_context,
|
||||
@@ -320,17 +353,14 @@ 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>,
|
||||
config_store: &(impl ConfigDocumentStore + ?Sized),
|
||||
config_toml: &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 {
|
||||
Ok(contents) => {
|
||||
let config: TomlValue = toml::from_str(&contents).map_err(|err| {
|
||||
let config_error = config_error_from_toml(toml_file, &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 toml_file = config_toml.as_path();
|
||||
let toml_value = match read_config_document(config_store, config_toml).await? {
|
||||
ConfigDocumentRead::Present { value } => {
|
||||
let config_parent = config_toml.parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
@@ -339,23 +369,71 @@ async fn load_config_toml_for_required_layer(
|
||||
),
|
||||
)
|
||||
})?;
|
||||
resolve_relative_paths_in_config_toml(config, config_parent)
|
||||
resolve_relative_paths_in_config_toml(value, config_parent.as_path())
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
Ok(TomlValue::Table(toml::map::Map::new()))
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
e.kind(),
|
||||
format!("Failed to read config file {}: {e}", toml_file.display()),
|
||||
))
|
||||
}
|
||||
ConfigDocumentRead::Missing => Ok(TomlValue::Table(toml::map::Map::new())),
|
||||
ConfigDocumentRead::ParseError {
|
||||
raw_toml,
|
||||
message,
|
||||
span,
|
||||
} => {
|
||||
let config_error = config_error_from_parse_message(
|
||||
toml_file,
|
||||
&raw_toml,
|
||||
message,
|
||||
span.map(Into::into),
|
||||
);
|
||||
Err(io_error_from_config_error(
|
||||
io::ErrorKind::InvalidData,
|
||||
config_error,
|
||||
/*source*/ None,
|
||||
))
|
||||
}
|
||||
ConfigDocumentRead::ReadError { kind, message } => Err(io::Error::new(
|
||||
io_error_kind_from_config_document_read_kind(&kind),
|
||||
format!(
|
||||
"Failed to read config file {}: {message}",
|
||||
toml_file.display()
|
||||
),
|
||||
)),
|
||||
}?;
|
||||
|
||||
Ok(create_entry(toml_value))
|
||||
}
|
||||
|
||||
async fn read_config_document(
|
||||
config_store: &(impl ConfigDocumentStore + ?Sized),
|
||||
path: &AbsolutePathBuf,
|
||||
) -> io::Result<ConfigDocumentRead> {
|
||||
config_store
|
||||
.read_config_document(ReadConfigDocumentParams { path: path.clone() })
|
||||
.await
|
||||
.map_err(config_store_error_to_io)
|
||||
}
|
||||
|
||||
fn config_store_error_to_io(error: ConfigStoreError) -> io::Error {
|
||||
match error {
|
||||
ConfigStoreError::InvalidRequest { message } => {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, message)
|
||||
}
|
||||
ConfigStoreError::ReadFailed { message } | ConfigStoreError::Internal { message } => {
|
||||
io::Error::other(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn io_error_kind_from_config_document_read_kind(kind: &str) -> io::ErrorKind {
|
||||
match kind {
|
||||
"permission_denied" => io::ErrorKind::PermissionDenied,
|
||||
"invalid_data" => io::ErrorKind::InvalidData,
|
||||
"invalid_input" => io::ErrorKind::InvalidInput,
|
||||
"timed_out" => io::ErrorKind::TimedOut,
|
||||
"interrupted" => io::ErrorKind::Interrupted,
|
||||
"unsupported" => io::ErrorKind::Unsupported,
|
||||
_ => io::ErrorKind::Other,
|
||||
}
|
||||
}
|
||||
|
||||
/// If available, apply requirements from the platform system
|
||||
/// `requirements.toml` location to `config_requirements_toml` by filling in
|
||||
/// any unset fields.
|
||||
@@ -766,6 +844,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(
|
||||
config_store: &(impl ConfigDocumentStore + ?Sized),
|
||||
cwd: &AbsolutePathBuf,
|
||||
project_root: &AbsolutePathBuf,
|
||||
trust_context: &ProjectTrustContext,
|
||||
@@ -810,32 +889,9 @@ async fn load_project_layers(
|
||||
continue;
|
||||
}
|
||||
let config_file = dot_codex_abs.join(CONFIG_TOML_FILE);
|
||||
match tokio::fs::read_to_string(&config_file).await {
|
||||
Ok(contents) => {
|
||||
let config: TomlValue = match toml::from_str(&contents) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
if decision.is_trusted() {
|
||||
let config_file_display = config_file.as_path().display();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Error parsing project config file {config_file_display}: {e}"
|
||||
),
|
||||
));
|
||||
}
|
||||
layers.push(project_layer_entry(
|
||||
trust_context,
|
||||
&dot_codex_abs,
|
||||
&layer_dir,
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
/*config_toml_exists*/ true,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let config =
|
||||
resolve_relative_paths_in_config_toml(config, dot_codex_abs.as_path())?;
|
||||
match read_config_document(config_store, &config_file).await? {
|
||||
ConfigDocumentRead::Present { value } => {
|
||||
let config = resolve_relative_paths_in_config_toml(value, dot_codex_abs.as_path())?;
|
||||
let entry = project_layer_entry(
|
||||
trust_context,
|
||||
&dot_codex_abs,
|
||||
@@ -845,25 +901,42 @@ async fn load_project_layers(
|
||||
);
|
||||
layers.push(entry);
|
||||
}
|
||||
Err(err) => {
|
||||
if err.kind() == io::ErrorKind::NotFound {
|
||||
// If there is no config.toml file, record an empty entry
|
||||
// for this project layer, as this may still have subfolders
|
||||
// that are significant in the overall ConfigLayerStack.
|
||||
layers.push(project_layer_entry(
|
||||
trust_context,
|
||||
&dot_codex_abs,
|
||||
&layer_dir,
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
/*config_toml_exists*/ false,
|
||||
));
|
||||
} else {
|
||||
ConfigDocumentRead::ParseError { message, .. } => {
|
||||
if decision.is_trusted() {
|
||||
let config_file_display = config_file.as_path().display();
|
||||
return Err(io::Error::new(
|
||||
err.kind(),
|
||||
format!("Failed to read project config file {config_file_display}: {err}"),
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Error parsing project config file {config_file_display}: {message}"
|
||||
),
|
||||
));
|
||||
}
|
||||
layers.push(project_layer_entry(
|
||||
trust_context,
|
||||
&dot_codex_abs,
|
||||
&layer_dir,
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
/*config_toml_exists*/ true,
|
||||
));
|
||||
}
|
||||
ConfigDocumentRead::Missing => {
|
||||
// If there is no config.toml file, record an empty entry
|
||||
// for this project layer, as this may still have subfolders
|
||||
// that are significant in the overall ConfigLayerStack.
|
||||
layers.push(project_layer_entry(
|
||||
trust_context,
|
||||
&dot_codex_abs,
|
||||
&layer_dir,
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
/*config_toml_exists*/ false,
|
||||
));
|
||||
}
|
||||
ConfigDocumentRead::ReadError { kind, message } => {
|
||||
let config_file_display = config_file.as_path().display();
|
||||
return Err(io::Error::new(
|
||||
io_error_kind_from_config_document_read_kind(&kind),
|
||||
format!("Failed to read project config file {config_file_display}: {message}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,17 @@ use crate::config_loader::ConfigRequirements;
|
||||
use crate::config_loader::ConfigRequirementsToml;
|
||||
use crate::config_loader::ConfigRequirementsWithSources;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::load_config_layers_state_with_store;
|
||||
use crate::config_loader::load_requirements_toml;
|
||||
use crate::config_loader::version_for_toml;
|
||||
use async_trait::async_trait;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
use codex_config_store::ConfigDocumentRead;
|
||||
use codex_config_store::ConfigDocumentStore;
|
||||
use codex_config_store::ConfigStoreResult;
|
||||
use codex_config_store::ReadConfigDocumentParams;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -36,6 +42,25 @@ fn config_error_from_io(err: &std::io::Error) -> &super::ConfigError {
|
||||
.expect("expected ConfigLoadError")
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MemoryConfigStore {
|
||||
documents: HashMap<AbsolutePathBuf, ConfigDocumentRead>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ConfigDocumentStore for MemoryConfigStore {
|
||||
async fn read_config_document(
|
||||
&self,
|
||||
params: ReadConfigDocumentParams,
|
||||
) -> ConfigStoreResult<ConfigDocumentRead> {
|
||||
Ok(self
|
||||
.documents
|
||||
.get(¶ms.path)
|
||||
.cloned()
|
||||
.unwrap_or(ConfigDocumentRead::Missing))
|
||||
}
|
||||
}
|
||||
|
||||
async fn make_config_for_test(
|
||||
codex_home: &Path,
|
||||
project_path: &Path,
|
||||
@@ -108,6 +133,99 @@ async fn returns_config_error_for_invalid_user_config_toml() {
|
||||
assert_eq!(config_error, &expected_config_error);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loads_user_config_from_config_store() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let cwd = tempdir()?;
|
||||
let user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home.path());
|
||||
let user_config = toml::from_str::<TomlValue>(r#"model = "store-user-model""#)?;
|
||||
let store = MemoryConfigStore {
|
||||
documents: HashMap::from([(
|
||||
user_file.clone(),
|
||||
ConfigDocumentRead::Present { value: user_config },
|
||||
)]),
|
||||
};
|
||||
|
||||
let state = load_config_layers_state_with_store(
|
||||
codex_home.path(),
|
||||
Some(AbsolutePathBuf::try_from(cwd.path())?),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let expected_user_layer = ConfigLayerEntry::new(
|
||||
super::ConfigLayerSource::User { file: user_file },
|
||||
toml::from_str::<TomlValue>(r#"model = "store-user-model""#)?,
|
||||
);
|
||||
assert_eq!(state.get_user_layer(), Some(&expected_user_layer));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loads_project_config_from_config_store() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let project = tempdir()?;
|
||||
let dot_codex = project.path().join(".codex");
|
||||
tokio::fs::create_dir(&dot_codex).await?;
|
||||
|
||||
let user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home.path());
|
||||
let project_root = AbsolutePathBuf::try_from(project.path())?;
|
||||
let project_config_file =
|
||||
AbsolutePathBuf::from_absolute_path(dot_codex.join(CONFIG_TOML_FILE))?;
|
||||
let user_config = toml::Value::try_from(ConfigToml {
|
||||
projects: Some(HashMap::from([(
|
||||
super::project_trust_key(project.path()),
|
||||
ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
},
|
||||
)])),
|
||||
..Default::default()
|
||||
})?;
|
||||
let project_config = toml::from_str::<TomlValue>(r#"model = "store-project-model""#)?;
|
||||
let store = MemoryConfigStore {
|
||||
documents: HashMap::from([
|
||||
(
|
||||
user_file,
|
||||
ConfigDocumentRead::Present { value: user_config },
|
||||
),
|
||||
(
|
||||
project_config_file.clone(),
|
||||
ConfigDocumentRead::Present {
|
||||
value: project_config.clone(),
|
||||
},
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
let state = load_config_layers_state_with_store(
|
||||
codex_home.path(),
|
||||
Some(project_root.clone()),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_layer = ConfigLayerEntry::new(
|
||||
super::ConfigLayerSource::Project {
|
||||
dot_codex_folder: project_config_file.parent().expect("project config parent"),
|
||||
},
|
||||
project_config,
|
||||
);
|
||||
assert_eq!(
|
||||
state
|
||||
.layers_high_to_low()
|
||||
.into_iter()
|
||||
.find(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. })),
|
||||
Some(&project_layer)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_config_error_for_invalid_managed_config_toml() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
Reference in New Issue
Block a user