Compare commits

...

1 Commits

Author SHA1 Message Date
Rasmus Rygaard
6e9afdf9a1 Add a ConfigStore interface 2026-04-15 11:25:35 -07:00
8 changed files with 230 additions and 0 deletions

14
codex-rs/Cargo.lock generated
View File

@@ -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"

View File

@@ -23,6 +23,7 @@ members = [
"collaboration-mode-templates",
"connectors",
"config",
"config-store",
"shell-command",
"shell-escalation",
"skills",

View File

@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "config-store",
crate_name = "codex_config_store",
)

View File

@@ -0,0 +1,24 @@
[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 }
toml = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt"] }

View 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,
},
}

View File

@@ -0,0 +1,19 @@
//! 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 store;
mod types;
pub use error::ConfigStoreError;
pub use error::ConfigStoreResult;
pub use store::ConfigDocumentStore;
pub use types::ConfigDocumentErrorSpan;
pub use types::ConfigDocumentRead;
pub use types::ReadConfigDocumentParams;

View 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>;
}

View 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);
}
}