mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add session config loader interface (#18208)
## Why Cloud-hosted sessions need a way for the service that starts or manages a thread to provide session-owned config without treating all config as if it came from the same user/project/workspace TOML stack. The important boundary is ownership: some values should be controlled by the session/orchestrator, some by the authenticated user, and later some may come from the executor. The earlier broad config-store shape made that boundary too fuzzy and overlapped heavily with the existing filesystem-backed config loader. This PR starts with the smaller piece we need now: a typed session config loader that can feed the existing config layer stack while preserving the normal precedence and merge behavior. ## What Changed - Added `ThreadConfigLoader` and related typed payloads in `codex-config`. - `SessionThreadConfig` currently supports `model_provider`, `model_providers`, and feature flags. - `UserThreadConfig` is present as an ownership boundary, but does not yet add TOML-backed fields. - `NoopThreadConfigLoader` preserves existing behavior when no external loader is configured. - `StaticThreadConfigLoader` supports tests and simple callers. - Taught thread config sources to produce ordinary `ConfigLayerEntry` values so the existing `ConfigLayerStack` remains the place where precedence and merging happen. - Wired the loader through `ConfigBuilder`, the config loader, and app-server startup paths so app-server can provide session-owned config before deriving a thread config. - Added coverage for: - translating typed thread config into config layers, - inserting thread config layers into the stack at the right precedence, - applying session-provided model provider and feature settings when app-server derives config from thread params. ## Follow-Ups This intentionally stops short of adding the remote/service transport. The next pieces are expected to be: 1. Define the proto/API shape for this interface. 2. Add a client implementation that can source session config from the service side. ## Verification - Added unit coverage in `codex-config` for the loader and layer conversion. - Added `codex-core` config loader coverage for thread config layer precedence. - Added app-server coverage that verifies session thread config wins over request-provided config for model provider and feature settings.
This commit is contained in:
committed by
GitHub
Unverified
parent
513dc28717
commit
7b994100b3
Generated
+3
@@ -1464,6 +1464,7 @@ dependencies = [
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
@@ -1518,6 +1519,7 @@ dependencies = [
|
||||
"codex-app-server",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-feedback",
|
||||
@@ -1866,6 +1868,7 @@ name = "codex-config"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"codex-app-server-protocol",
|
||||
"codex-execpolicy",
|
||||
"codex-features",
|
||||
|
||||
@@ -15,6 +15,7 @@ workspace = true
|
||||
codex-app-server = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
|
||||
@@ -41,6 +41,7 @@ use codex_app_server_protocol::Result as JsonRpcResult;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
@@ -385,6 +386,7 @@ impl InProcessClientStartArgs {
|
||||
cli_overrides: self.cli_overrides,
|
||||
loader_overrides: self.loader_overrides,
|
||||
cloud_requirements: self.cloud_requirements,
|
||||
thread_config_loader: Arc::new(NoopThreadConfigLoader),
|
||||
feedback: self.feedback,
|
||||
log_db: self.log_db,
|
||||
environment_manager: self.environment_manager,
|
||||
|
||||
@@ -97,6 +97,7 @@ axum = { workspace = true, default-features = false, features = [
|
||||
"tokio",
|
||||
] }
|
||||
core_test_support = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
opentelemetry = { workspace = true }
|
||||
opentelemetry_sdk = { workspace = true }
|
||||
|
||||
@@ -213,6 +213,7 @@ use codex_backend_client::AddCreditsNudgeCreditType as BackendAddCreditsNudgeCre
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_cloud_requirements::cloud_requirements_loader;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::ForkSnapshot;
|
||||
@@ -479,6 +480,7 @@ pub(crate) struct CodexMessageProcessor {
|
||||
cli_overrides: Arc<RwLock<Vec<(String, TomlValue)>>>,
|
||||
runtime_feature_enablement: Arc<RwLock<BTreeMap<String, bool>>>,
|
||||
cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
active_login: Arc<Mutex<Option<ActiveLogin>>>,
|
||||
pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
@@ -639,6 +641,7 @@ pub(crate) struct CodexMessageProcessorArgs {
|
||||
pub(crate) cli_overrides: Arc<RwLock<Vec<(String, TomlValue)>>>,
|
||||
pub(crate) runtime_feature_enablement: Arc<RwLock<BTreeMap<String, bool>>>,
|
||||
pub(crate) cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
||||
pub(crate) thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
pub(crate) feedback: CodexFeedback,
|
||||
pub(crate) log_db: Option<LogDbLayer>,
|
||||
}
|
||||
@@ -726,6 +729,7 @@ impl CodexMessageProcessor {
|
||||
cli_overrides,
|
||||
runtime_feature_enablement,
|
||||
cloud_requirements,
|
||||
thread_config_loader,
|
||||
feedback,
|
||||
log_db,
|
||||
} = args;
|
||||
@@ -740,6 +744,7 @@ impl CodexMessageProcessor {
|
||||
cli_overrides,
|
||||
runtime_feature_enablement,
|
||||
cloud_requirements,
|
||||
thread_config_loader,
|
||||
active_login: Arc::new(Mutex::new(None)),
|
||||
pending_thread_unloads: Arc::new(Mutex::new(HashSet::new())),
|
||||
thread_state_manager: ThreadStateManager::new(),
|
||||
@@ -2433,12 +2438,14 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
let request_trace = request_context.request_trace();
|
||||
let runtime_feature_enablement = self.current_runtime_feature_enablement();
|
||||
let thread_config_loader = Arc::clone(&self.thread_config_loader);
|
||||
let thread_start_task = async move {
|
||||
Self::thread_start_task(
|
||||
listener_task_context,
|
||||
cli_overrides,
|
||||
runtime_feature_enablement,
|
||||
cloud_requirements,
|
||||
thread_config_loader,
|
||||
request_id,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
@@ -2515,6 +2522,7 @@ impl CodexMessageProcessor {
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
runtime_feature_enablement: BTreeMap<String, bool>,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
request_id: ConnectionRequestId,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
@@ -2532,6 +2540,7 @@ impl CodexMessageProcessor {
|
||||
&cli_overrides,
|
||||
config_overrides.clone(),
|
||||
typesafe_overrides.clone(),
|
||||
Arc::clone(&thread_config_loader),
|
||||
&cloud_requirements,
|
||||
&listener_task_context.codex_home,
|
||||
&runtime_feature_enablement,
|
||||
@@ -2613,6 +2622,7 @@ impl CodexMessageProcessor {
|
||||
cli_overrides_for_reload,
|
||||
config_overrides,
|
||||
typesafe_overrides,
|
||||
thread_config_loader,
|
||||
&cloud_requirements,
|
||||
&listener_task_context.codex_home,
|
||||
&runtime_feature_enablement,
|
||||
@@ -6528,6 +6538,7 @@ impl CodexMessageProcessor {
|
||||
&cli_overrides,
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
self.thread_config_loader.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -9452,6 +9463,7 @@ async fn derive_config_from_params(
|
||||
cli_overrides: &[(String, TomlValue)],
|
||||
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
cloud_requirements: &CloudRequirementsLoader,
|
||||
codex_home: &Path,
|
||||
runtime_feature_enablement: &BTreeMap<String, bool>,
|
||||
@@ -9472,6 +9484,7 @@ async fn derive_config_from_params(
|
||||
.cli_overrides(merged_cli_overrides)
|
||||
.harness_overrides(typesafe_overrides)
|
||||
.cloud_requirements(cloud_requirements.clone())
|
||||
.thread_config_loader(thread_config_loader)
|
||||
.build()
|
||||
.await?;
|
||||
apply_runtime_feature_enablement(&mut config, runtime_feature_enablement);
|
||||
@@ -10402,6 +10415,11 @@ mod tests {
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_config::SessionThreadConfig;
|
||||
use codex_config::StaticThreadConfigLoader;
|
||||
use codex_config::ThreadConfigSource;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -10414,6 +10432,7 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
@@ -10566,6 +10585,64 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn derive_config_from_params_uses_session_thread_config_model_provider() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let session_provider = ModelProviderInfo {
|
||||
name: "session".to_string(),
|
||||
base_url: Some("http://127.0.0.1:8061/api/codex".to_string()),
|
||||
env_key: None,
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
auth: None,
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
stream_max_retries: None,
|
||||
stream_idle_timeout_ms: None,
|
||||
websocket_connect_timeout_ms: None,
|
||||
requires_openai_auth: false,
|
||||
supports_websockets: true,
|
||||
};
|
||||
let config = derive_config_from_params(
|
||||
&[],
|
||||
Some(HashMap::from([
|
||||
("model_provider".to_string(), json!("request")),
|
||||
("features.plugins".to_string(), json!(true)),
|
||||
(
|
||||
"model_providers.session".to_string(),
|
||||
json!({
|
||||
"name": "request",
|
||||
"base_url": "http://127.0.0.1:9999/api/codex",
|
||||
"wire_api": "responses",
|
||||
}),
|
||||
),
|
||||
])),
|
||||
ConfigOverrides::default(),
|
||||
Arc::new(StaticThreadConfigLoader::new(vec![
|
||||
ThreadConfigSource::Session(SessionThreadConfig {
|
||||
model_provider: Some("session".to_string()),
|
||||
model_providers: HashMap::from([(
|
||||
"session".to_string(),
|
||||
session_provider.clone(),
|
||||
)]),
|
||||
features: BTreeMap::from([("plugins".to_string(), false)]),
|
||||
}),
|
||||
])),
|
||||
&CloudRequirementsLoader::default(),
|
||||
temp_dir.path(),
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.model_provider_id, "session");
|
||||
assert_eq!(config.model_provider, session_provider);
|
||||
assert!(!config.features.enabled(Feature::Plugins));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_resume_override_mismatches_includes_service_tier() {
|
||||
let request = ThreadResumeParams {
|
||||
|
||||
@@ -75,6 +75,7 @@ use codex_app_server_protocol::Result;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
@@ -116,6 +117,8 @@ pub struct InProcessStartArgs {
|
||||
pub loader_overrides: LoaderOverrides,
|
||||
/// Preloaded cloud requirements provider.
|
||||
pub cloud_requirements: CloudRequirementsLoader,
|
||||
/// Loader used to fetch typed thread config sources before a thread starts.
|
||||
pub thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
/// Feedback sink used by app-server/core telemetry and logs.
|
||||
pub feedback: CodexFeedback,
|
||||
/// SQLite tracing layer used to flush recently emitted logs before feedback upload.
|
||||
@@ -397,6 +400,7 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
cli_overrides: args.cli_overrides,
|
||||
loader_overrides: args.loader_overrides,
|
||||
cloud_requirements: args.cloud_requirements,
|
||||
thread_config_loader: args.thread_config_loader,
|
||||
feedback: args.feedback,
|
||||
log_db: args.log_db,
|
||||
config_warnings: args.config_warnings,
|
||||
@@ -731,6 +735,7 @@ mod tests {
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_cloud_requirements::cloud_requirements_loader;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
@@ -416,6 +417,7 @@ pub async fn run_main_with_transport(
|
||||
}
|
||||
};
|
||||
let loader_overrides_for_config_api = loader_overrides.clone();
|
||||
let thread_config_loader = Arc::new(NoopThreadConfigLoader);
|
||||
let mut config_warnings = Vec::new();
|
||||
let config = match ConfigBuilder::default()
|
||||
.cli_overrides(cli_kv_overrides.clone())
|
||||
@@ -659,6 +661,7 @@ pub async fn run_main_with_transport(
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements: cloud_requirements.clone(),
|
||||
thread_config_loader,
|
||||
feedback: feedback.clone(),
|
||||
log_db,
|
||||
config_warnings,
|
||||
|
||||
@@ -63,6 +63,7 @@ use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::experimental_required_message;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
@@ -235,6 +236,7 @@ pub(crate) struct MessageProcessorArgs {
|
||||
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
|
||||
pub(crate) loader_overrides: LoaderOverrides,
|
||||
pub(crate) cloud_requirements: CloudRequirementsLoader,
|
||||
pub(crate) thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
pub(crate) feedback: CodexFeedback,
|
||||
pub(crate) log_db: Option<LogDbLayer>,
|
||||
pub(crate) config_warnings: Vec<ConfigWarningNotification>,
|
||||
@@ -256,6 +258,7 @@ impl MessageProcessor {
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
thread_config_loader,
|
||||
feedback,
|
||||
log_db,
|
||||
config_warnings,
|
||||
@@ -301,6 +304,7 @@ impl MessageProcessor {
|
||||
cli_overrides: cli_overrides.clone(),
|
||||
runtime_feature_enablement: runtime_feature_enablement.clone(),
|
||||
cloud_requirements: cloud_requirements.clone(),
|
||||
thread_config_loader,
|
||||
feedback,
|
||||
log_db,
|
||||
});
|
||||
|
||||
@@ -245,6 +245,7 @@ fn build_test_processor(
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
config_warnings: Vec::new(),
|
||||
|
||||
@@ -175,6 +175,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides,
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
|
||||
|
||||
@@ -9,6 +9,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod schema;
|
||||
pub mod shell_environment;
|
||||
mod skills_config;
|
||||
mod state;
|
||||
mod thread_config;
|
||||
pub mod types;
|
||||
|
||||
pub const CONFIG_TOML_FILE: &str = "config.toml";
|
||||
@@ -93,5 +94,14 @@ pub use state::ConfigLayerEntry;
|
||||
pub use state::ConfigLayerStack;
|
||||
pub use state::ConfigLayerStackOrdering;
|
||||
pub use state::LoaderOverrides;
|
||||
pub use thread_config::NoopThreadConfigLoader;
|
||||
pub use thread_config::SessionThreadConfig;
|
||||
pub use thread_config::StaticThreadConfigLoader;
|
||||
pub use thread_config::ThreadConfigContext;
|
||||
pub use thread_config::ThreadConfigLoadError;
|
||||
pub use thread_config::ThreadConfigLoadErrorCode;
|
||||
pub use thread_config::ThreadConfigLoader;
|
||||
pub use thread_config::ThreadConfigSource;
|
||||
pub use thread_config::UserThreadConfig;
|
||||
|
||||
pub use codex_app_server_protocol::ConfigLayerSource;
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use thiserror::Error;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
use crate::ConfigLayerEntry;
|
||||
|
||||
/// Context available to implementations when loading thread-scoped config.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct ThreadConfigContext {
|
||||
pub thread_id: Option<String>,
|
||||
pub cwd: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
/// Config values owned by the service that starts or manages the session.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct SessionThreadConfig {
|
||||
pub model_provider: Option<String>,
|
||||
pub model_providers: HashMap<String, ModelProviderInfo>,
|
||||
pub features: BTreeMap<String, bool>,
|
||||
}
|
||||
|
||||
/// Config values owned by the authenticated user.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct UserThreadConfig {}
|
||||
|
||||
/// A typed config payload paired with the authority that produced it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ThreadConfigSource {
|
||||
Session(SessionThreadConfig),
|
||||
User(UserThreadConfig),
|
||||
}
|
||||
|
||||
/// Stable category for failures returned while loading thread config.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ThreadConfigLoadErrorCode {
|
||||
Auth,
|
||||
Timeout,
|
||||
Parse,
|
||||
RequestFailed,
|
||||
Internal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Error, PartialEq)]
|
||||
#[error("{message}")]
|
||||
pub struct ThreadConfigLoadError {
|
||||
code: ThreadConfigLoadErrorCode,
|
||||
message: String,
|
||||
status_code: Option<u16>,
|
||||
}
|
||||
|
||||
impl ThreadConfigLoadError {
|
||||
pub fn new(
|
||||
code: ThreadConfigLoadErrorCode,
|
||||
status_code: Option<u16>,
|
||||
message: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
status_code,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn code(&self) -> ThreadConfigLoadErrorCode {
|
||||
self.code
|
||||
}
|
||||
|
||||
pub fn status_code(&self) -> Option<u16> {
|
||||
self.status_code
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads typed config sources for a new thread.
|
||||
///
|
||||
/// Implementations should fetch only the source-specific config they own and
|
||||
/// return typed payloads without applying precedence or merge rules. Callers
|
||||
/// are responsible for resolving the returned sources into the effective
|
||||
/// runtime config.
|
||||
#[async_trait]
|
||||
pub trait ThreadConfigLoader: Send + Sync {
|
||||
/// Load source-specific typed config.
|
||||
///
|
||||
/// Implementations should keep this method focused on fetching and parsing
|
||||
/// their owned sources. Most callers should use [`Self::load_config_layers`]
|
||||
/// so precedence and merging continue through the ordinary config layer
|
||||
/// stack.
|
||||
async fn load(
|
||||
&self,
|
||||
context: ThreadConfigContext,
|
||||
) -> Result<Vec<ThreadConfigSource>, ThreadConfigLoadError>;
|
||||
|
||||
async fn load_config_layers(
|
||||
&self,
|
||||
context: ThreadConfigContext,
|
||||
) -> Result<Vec<ConfigLayerEntry>, ThreadConfigLoadError> {
|
||||
let sources = self.load(context).await?;
|
||||
sources
|
||||
.into_iter()
|
||||
.map(thread_config_source_to_layer)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(|layers| layers.into_iter().flatten().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Loader backed by a static set of typed thread config sources.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct StaticThreadConfigLoader {
|
||||
sources: Vec<ThreadConfigSource>,
|
||||
}
|
||||
|
||||
impl StaticThreadConfigLoader {
|
||||
pub fn new(sources: Vec<ThreadConfigSource>) -> Self {
|
||||
Self { sources }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ThreadConfigLoader for StaticThreadConfigLoader {
|
||||
async fn load(
|
||||
&self,
|
||||
_context: ThreadConfigContext,
|
||||
) -> Result<Vec<ThreadConfigSource>, ThreadConfigLoadError> {
|
||||
Ok(self.sources.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Loader used when no external thread config source is configured.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NoopThreadConfigLoader;
|
||||
|
||||
#[async_trait]
|
||||
impl ThreadConfigLoader for NoopThreadConfigLoader {
|
||||
async fn load(
|
||||
&self,
|
||||
_context: ThreadConfigContext,
|
||||
) -> Result<Vec<ThreadConfigSource>, ThreadConfigLoadError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_config_source_to_layer(
|
||||
source: ThreadConfigSource,
|
||||
) -> Result<Option<ConfigLayerEntry>, ThreadConfigLoadError> {
|
||||
match source {
|
||||
ThreadConfigSource::Session(config) => {
|
||||
let config = session_thread_config_to_toml(config)?;
|
||||
if is_empty_table(&config) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
config,
|
||||
)))
|
||||
}
|
||||
}
|
||||
// UserThreadConfig has no TOML-backed fields yet. When it grows one,
|
||||
// fold it into the existing user layer instead of adding another
|
||||
// ConfigLayerSource variant.
|
||||
ThreadConfigSource::User(_config) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty_table(config: &TomlValue) -> bool {
|
||||
config.as_table().is_some_and(toml::map::Map::is_empty)
|
||||
}
|
||||
|
||||
fn session_thread_config_to_toml(
|
||||
config: SessionThreadConfig,
|
||||
) -> Result<TomlValue, ThreadConfigLoadError> {
|
||||
let mut table = toml::map::Map::new();
|
||||
|
||||
if let Some(model_provider) = config.model_provider {
|
||||
table.insert(
|
||||
"model_provider".to_string(),
|
||||
TomlValue::String(model_provider),
|
||||
);
|
||||
}
|
||||
|
||||
if !config.model_providers.is_empty() {
|
||||
let model_providers = TomlValue::try_from(config.model_providers).map_err(|err| {
|
||||
ThreadConfigLoadError::new(
|
||||
ThreadConfigLoadErrorCode::Parse,
|
||||
/*status_code*/ None,
|
||||
format!("failed to convert session model providers to config TOML: {err}"),
|
||||
)
|
||||
})?;
|
||||
table.insert("model_providers".to_string(), model_providers);
|
||||
}
|
||||
|
||||
if !config.features.is_empty() {
|
||||
let features = config
|
||||
.features
|
||||
.into_iter()
|
||||
.map(|(feature, enabled)| (feature, TomlValue::Boolean(enabled)))
|
||||
.collect();
|
||||
table.insert("features".to_string(), TomlValue::Table(features));
|
||||
}
|
||||
|
||||
Ok(TomlValue::Table(table))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn loader_returns_session_and_user_sources() {
|
||||
let loader = StaticThreadConfigLoader::new(vec![
|
||||
ThreadConfigSource::Session(SessionThreadConfig {
|
||||
model_provider: Some("local".to_string()),
|
||||
model_providers: HashMap::from([("local".to_string(), test_provider("local"))]),
|
||||
features: BTreeMap::from([("plugins".to_string(), false)]),
|
||||
}),
|
||||
ThreadConfigSource::User(UserThreadConfig::default()),
|
||||
]);
|
||||
|
||||
let sources = loader
|
||||
.load(ThreadConfigContext {
|
||||
thread_id: Some("thread-1".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("thread config loads");
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
ThreadConfigSource::Session(SessionThreadConfig {
|
||||
model_provider: Some("local".to_string()),
|
||||
model_providers: HashMap::from([("local".to_string(), test_provider("local"))]),
|
||||
features: BTreeMap::from([("plugins".to_string(), false)]),
|
||||
}),
|
||||
ThreadConfigSource::User(UserThreadConfig::default()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loader_translates_sources_to_config_layers() {
|
||||
let loader = StaticThreadConfigLoader::new(vec![
|
||||
ThreadConfigSource::User(UserThreadConfig::default()),
|
||||
ThreadConfigSource::Session(SessionThreadConfig {
|
||||
model_provider: Some("local".to_string()),
|
||||
model_providers: HashMap::from([("local".to_string(), test_provider("local"))]),
|
||||
features: BTreeMap::from([("plugins".to_string(), false)]),
|
||||
}),
|
||||
]);
|
||||
let layers = loader
|
||||
.load_config_layers(ThreadConfigContext {
|
||||
cwd: Some(
|
||||
AbsolutePathBuf::from_absolute_path_checked(
|
||||
std::env::temp_dir().join("project"),
|
||||
)
|
||||
.expect("absolute cwd"),
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("thread config layers load");
|
||||
|
||||
assert_eq!(
|
||||
layers,
|
||||
vec![ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::toml! {
|
||||
model_provider = "local"
|
||||
|
||||
[model_providers.local]
|
||||
name = "local"
|
||||
base_url = "http://127.0.0.1:8061/api/codex"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
supports_websockets = true
|
||||
|
||||
[features]
|
||||
plugins = false
|
||||
}
|
||||
.into()
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
fn test_provider(name: &str) -> ModelProviderInfo {
|
||||
ModelProviderInfo {
|
||||
name: name.to_string(),
|
||||
base_url: Some("http://127.0.0.1:8061/api/codex".to_string()),
|
||||
env_key: None,
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
auth: None,
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
stream_max_retries: None,
|
||||
stream_idle_timeout_ms: None,
|
||||
websocket_connect_timeout_ms: None,
|
||||
requires_openai_auth: false,
|
||||
supports_websockets: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2110,6 +2110,7 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> {
|
||||
&Vec::new(),
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let cfg =
|
||||
@@ -2244,6 +2245,7 @@ async fn managed_config_wins_over_cli_overrides() -> anyhow::Result<()> {
|
||||
&[("model".to_string(), TomlValue::String("cli".to_string()))],
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS;
|
||||
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_mode;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
use codex_config::config_toml::RealtimeAudioConfig;
|
||||
@@ -90,6 +91,7 @@ use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::permissions::compile_permission_profile;
|
||||
use crate::config::permissions::get_readable_roots_required_for_codex_runtime;
|
||||
@@ -646,13 +648,14 @@ impl AuthManagerConfig for Config {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ConfigBuilder {
|
||||
codex_home: Option<PathBuf>,
|
||||
cli_overrides: Option<Vec<(String, TomlValue)>>,
|
||||
harness_overrides: Option<ConfigOverrides>,
|
||||
loader_overrides: Option<LoaderOverrides>,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
|
||||
fallback_cwd: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -682,6 +685,14 @@ impl ConfigBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn thread_config_loader(
|
||||
mut self,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
) -> Self {
|
||||
self.thread_config_loader = Some(thread_config_loader);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fallback_cwd(mut self, fallback_cwd: Option<PathBuf>) -> Self {
|
||||
self.fallback_cwd = fallback_cwd;
|
||||
self
|
||||
@@ -694,6 +705,7 @@ impl ConfigBuilder {
|
||||
harness_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
thread_config_loader,
|
||||
fallback_cwd,
|
||||
} = self;
|
||||
let codex_home = match codex_home {
|
||||
@@ -716,6 +728,9 @@ impl ConfigBuilder {
|
||||
&cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
thread_config_loader
|
||||
.as_deref()
|
||||
.unwrap_or(&codex_config::NoopThreadConfigLoader),
|
||||
)
|
||||
.await?;
|
||||
let merged_toml = config_layer_stack.effective_config();
|
||||
@@ -894,6 +909,7 @@ pub async fn load_config_as_toml_with_cli_and_loader_overrides(
|
||||
&cli_overrides,
|
||||
loader_overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1065,6 +1081,7 @@ pub async fn load_global_mcp_servers(
|
||||
&cli_overrides,
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let merged_toml = config_layer_stack.effective_config();
|
||||
|
||||
@@ -431,6 +431,7 @@ impl ConfigService {
|
||||
&self.cli_overrides,
|
||||
self.loader_overrides.clone(),
|
||||
self.cloud_requirements.clone(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -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(fs, codex_home, cwd_opt, cli_overrides, overrides, cloud_requirements) -> ConfigLayerStack`
|
||||
- `load_config_layers_state(fs, codex_home, cwd_opt, cli_overrides, overrides, cloud_requirements, thread_config_loader) -> ConfigLayerStack`
|
||||
- `ConfigLayerStack`
|
||||
- `effective_config() -> toml::Value`
|
||||
- `origins() -> HashMap<String, ConfigLayerMetadata>`
|
||||
@@ -29,6 +29,9 @@ Precedence is **top overrides bottom**:
|
||||
3. **Session flags** (CLI overrides, applied as dotted-path TOML writes)
|
||||
4. **User** config (`config.toml`)
|
||||
|
||||
Thread config entries supplied by `thread_config_loader` are inserted according
|
||||
to their translated `ConfigLayerSource` precedence.
|
||||
|
||||
Layers with a `disabled_reason` are still surfaced for UI, but are ignored when
|
||||
computing the effective config and origins metadata. This is what
|
||||
`ConfigLayerStack::effective_config()` implements.
|
||||
@@ -41,6 +44,7 @@ Most callers want the effective config plus metadata:
|
||||
use codex_core::config_loader::{
|
||||
CloudRequirementsLoader, LoaderOverrides, load_config_layers_state,
|
||||
};
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use toml::Value as TomlValue;
|
||||
@@ -54,6 +58,7 @@ let layers = load_config_layers_state(
|
||||
&cli_overrides,
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&NoopThreadConfigLoader,
|
||||
).await?;
|
||||
|
||||
let effective = layers.effective_config();
|
||||
|
||||
@@ -9,6 +9,8 @@ use crate::config_loader::layer_io::LoadedConfigLayers;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigRequirementsWithSources;
|
||||
use codex_config::ThreadConfigContext;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
@@ -127,6 +129,7 @@ pub async fn load_config_layers_state(
|
||||
cli_overrides: &[(String, TomlValue)],
|
||||
overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
thread_config_loader: &dyn ThreadConfigLoader,
|
||||
) -> io::Result<ConfigLayerStack> {
|
||||
let ignore_user_config = overrides.ignore_user_config;
|
||||
let ignore_user_and_project_exec_policy_rules =
|
||||
@@ -161,6 +164,15 @@ pub async fn load_config_layers_state(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let thread_config_context = ThreadConfigContext {
|
||||
thread_id: None,
|
||||
cwd: cwd.clone(),
|
||||
};
|
||||
let thread_config_layers = thread_config_loader
|
||||
.load_config_layers(thread_config_context)
|
||||
.await
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
let mut layers = Vec::<ConfigLayerEntry>::new();
|
||||
|
||||
let cli_overrides_layer = if cli_overrides.is_empty() {
|
||||
@@ -283,6 +295,10 @@ pub async fn load_config_layers_state(
|
||||
));
|
||||
}
|
||||
|
||||
for thread_config_layer in thread_config_layers {
|
||||
insert_layer_by_precedence(&mut layers, thread_config_layer);
|
||||
}
|
||||
|
||||
// Make a best-effort to support the legacy `managed_config.toml` as a
|
||||
// config layer on top of everything else. For fields in
|
||||
// `managed_config.toml` that do not have an equivalent in
|
||||
@@ -332,6 +348,16 @@ pub async fn load_config_layers_state(
|
||||
.with_user_and_project_exec_policy_rules_ignored(ignore_user_and_project_exec_policy_rules))
|
||||
}
|
||||
|
||||
fn insert_layer_by_precedence(layers: &mut Vec<ConfigLayerEntry>, layer: ConfigLayerEntry) {
|
||||
match layers
|
||||
.iter()
|
||||
.position(|existing| existing.name.precedence() > layer.name.precedence())
|
||||
{
|
||||
Some(index) => layers.insert(index, layer),
|
||||
None => layers.push(layer),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to load a config.toml file from `config_toml`.
|
||||
/// - If the file exists and is valid TOML, passes the parsed `toml::Value` to
|
||||
/// `create_entry` and returns the resulting layer entry.
|
||||
|
||||
@@ -15,6 +15,9 @@ use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::load_requirements_toml;
|
||||
use crate::config_loader::version_for_toml;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::SessionThreadConfig;
|
||||
use codex_config::StaticThreadConfigLoader;
|
||||
use codex_config::ThreadConfigSource;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
@@ -100,6 +103,7 @@ async fn returns_config_error_for_invalid_user_config_toml() {
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.expect_err("expected error");
|
||||
@@ -131,6 +135,7 @@ async fn ignore_user_config_keeps_empty_user_layer() -> std::io::Result<()> {
|
||||
..Default::default()
|
||||
},
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -161,6 +166,7 @@ async fn ignore_rules_marks_config_stack_for_exec_policy_rule_skip() -> std::io:
|
||||
..Default::default()
|
||||
},
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -185,6 +191,7 @@ async fn returns_config_error_for_invalid_managed_config_toml() {
|
||||
&[] as &[(String, TomlValue)],
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.expect_err("expected error");
|
||||
@@ -270,6 +277,7 @@ extra = true
|
||||
&[] as &[(String, TomlValue)],
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.expect("load config");
|
||||
@@ -303,6 +311,7 @@ async fn returns_empty_when_all_layers_missing() {
|
||||
&[] as &[(String, TomlValue)],
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.expect("load layers");
|
||||
@@ -354,6 +363,56 @@ async fn returns_empty_when_all_layers_missing() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn includes_thread_config_layers_in_stack() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let cwd_dir = tmp.path().join("project");
|
||||
tokio::fs::create_dir_all(&cwd_dir).await?;
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(&cwd_dir)?;
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
tmp.path(),
|
||||
Some(cwd),
|
||||
&[("features.plugins".to_string(), TomlValue::Boolean(true))],
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&StaticThreadConfigLoader::new(vec![ThreadConfigSource::Session(SessionThreadConfig {
|
||||
features: BTreeMap::from([("plugins".to_string(), false)]),
|
||||
..Default::default()
|
||||
})]),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let layer_sources = layers
|
||||
.layers_high_to_low()
|
||||
.into_iter()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
layer_sources,
|
||||
vec![
|
||||
super::ConfigLayerSource::SessionFlags,
|
||||
super::ConfigLayerSource::SessionFlags,
|
||||
super::ConfigLayerSource::User {
|
||||
file: AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, tmp.path()),
|
||||
},
|
||||
super::ConfigLayerSource::System {
|
||||
file: super::system_config_toml_file()?,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
layers
|
||||
.effective_config()
|
||||
.get("features")
|
||||
.and_then(TomlValue::as_table)
|
||||
.and_then(|features| features.get("plugins")),
|
||||
Some(&TomlValue::Boolean(false))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn managed_preferences_take_highest_precedence() {
|
||||
@@ -396,6 +455,7 @@ flag = false
|
||||
&[] as &[(String, TomlValue)],
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.expect("load config");
|
||||
@@ -498,6 +558,7 @@ allowed_sandbox_modes = ["read-only"]
|
||||
&[] as &[(String, TomlValue)],
|
||||
loader_overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -560,6 +621,7 @@ allowed_approval_policies = ["never"]
|
||||
&[] as &[(String, TomlValue)],
|
||||
loader_overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -713,6 +775,7 @@ allowed_approval_policies = ["on-request"]
|
||||
guardian_policy_config: None,
|
||||
}))
|
||||
}),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -931,6 +994,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
cloud_requirements,
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -974,6 +1038,7 @@ async fn load_config_layers_fails_when_cloud_requirements_loader_fails() -> anyh
|
||||
"cloud requirements failed",
|
||||
))
|
||||
}),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.expect_err("cloud requirements failure should fail closed");
|
||||
@@ -1021,6 +1086,7 @@ async fn project_layers_prefer_closest_cwd() -> std::io::Result<()> {
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1166,6 +1232,7 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1206,6 +1273,7 @@ async fn codex_home_is_not_loaded_as_project_layer_from_home_dir() -> std::io::R
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1263,6 +1331,7 @@ async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Resul
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1334,6 +1403,7 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let project_layers_untrusted: Vec<_> = layers_untrusted
|
||||
@@ -1373,6 +1443,7 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let project_layers_unknown: Vec<_> = layers_unknown
|
||||
@@ -1439,6 +1510,7 @@ async fn project_trust_does_not_match_configured_alias_for_canonical_cwd() -> st
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1592,6 +1664,7 @@ async fn invalid_project_config_ignored_when_untrusted_or_unknown() -> std::io::
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let project_layers: Vec<_> = layers
|
||||
@@ -1660,6 +1733,7 @@ async fn project_layer_without_config_toml_is_disabled_when_untrusted_or_unknown
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let project_layers: Vec<_> = layers
|
||||
@@ -1720,6 +1794,7 @@ async fn cli_overrides_with_relative_paths_do_not_break_trust_check() -> std::io
|
||||
&cli_overrides,
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1763,6 +1838,7 @@ async fn project_root_markers_supports_alternate_markers() -> std::io::Result<()
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec<LayerMtime
|
||||
&cli_overrides,
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.context("failed to load Codex config")?;
|
||||
|
||||
@@ -545,6 +545,7 @@ pub async fn list_skills(sess: &Session, sub_id: String, cwds: Vec<PathBuf>, for
|
||||
empty_cli_overrides,
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user