mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add remote thread config endpoint (#18908)
## Why App-server needs a way to fetch thread-scoped config from the remote thread config service when the user config opts into that behavior. This mirrors the existing experimental remote thread store endpoint while keeping local/noop behavior as the default. Startup paths also need to avoid silently dropping the remote config endpoint after the first config load. The stdio app-server path discovers the endpoint from the initial config and installs the real thread config loader for later config builds, while in-process clients used by TUI/exec now select the same remote loader directly from their provided config. ## What changed - Added `experimental_thread_config_endpoint` to `ConfigToml`, `Config`, and `core/config.schema.json`. - Added config parsing coverage for the new setting. - Updated app-server startup to select `RemoteThreadConfigLoader` from the initially loaded config, falling back to `NoopThreadConfigLoader` when unset. - Let `ConfigManager` replace its thread config loader after startup discovery so later config loads use the selected loader. - Updated in-process app-server client startup to pass `RemoteThreadConfigLoader` when its config has `experimental_thread_config_endpoint` set. ## Verification - Added `experimental_thread_config_endpoint_loads_from_config_toml`. - Added `runtime_start_args_use_remote_thread_config_loader_when_configured`. - Ran `cargo check -p codex-app-server --lib`. - Ran `cargo test -p codex-app-server-client`.
This commit is contained in:
committed by
GitHub
Unverified
parent
cff337e4e3
commit
f11583b8f6
@@ -42,6 +42,8 @@ use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_config::RemoteThreadConfigLoader;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
@@ -357,6 +359,13 @@ pub struct InProcessClientStartArgs {
|
||||
pub channel_capacity: usize,
|
||||
}
|
||||
|
||||
fn configured_thread_config_loader(config: &Config) -> Arc<dyn ThreadConfigLoader> {
|
||||
match config.experimental_thread_config_endpoint.as_deref() {
|
||||
Some(endpoint) => Arc::new(RemoteThreadConfigLoader::new(endpoint)),
|
||||
None => Arc::new(NoopThreadConfigLoader),
|
||||
}
|
||||
}
|
||||
|
||||
impl InProcessClientStartArgs {
|
||||
/// Builds initialize params from caller-provided metadata.
|
||||
pub fn initialize_params(&self) -> InitializeParams {
|
||||
@@ -381,13 +390,14 @@ impl InProcessClientStartArgs {
|
||||
|
||||
fn into_runtime_start_args(self) -> InProcessStartArgs {
|
||||
let initialize = self.initialize_params();
|
||||
let thread_config_loader = configured_thread_config_loader(&self.config);
|
||||
InProcessStartArgs {
|
||||
arg0_paths: self.arg0_paths,
|
||||
config: self.config,
|
||||
cli_overrides: self.cli_overrides,
|
||||
loader_overrides: self.loader_overrides,
|
||||
cloud_requirements: self.cloud_requirements,
|
||||
thread_config_loader: Arc::new(NoopThreadConfigLoader),
|
||||
thread_config_loader,
|
||||
feedback: self.feedback,
|
||||
log_db: self.log_db,
|
||||
environment_manager: self.environment_manager,
|
||||
@@ -2013,6 +2023,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_start_args_use_remote_thread_config_loader_when_configured() {
|
||||
let mut config = build_test_config().await;
|
||||
config.experimental_thread_config_endpoint = Some("not-a-valid-endpoint".to_string());
|
||||
|
||||
let runtime_args = InProcessClientStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(config),
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Exec,
|
||||
enable_codex_api_key_env: false,
|
||||
client_name: "codex-app-server-client-test".to_string(),
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
}
|
||||
.into_runtime_start_args();
|
||||
|
||||
let err = runtime_args
|
||||
.thread_config_loader
|
||||
.load(Default::default())
|
||||
.await
|
||||
.expect_err("configured remote loader should try to connect");
|
||||
assert_eq!(
|
||||
err.code(),
|
||||
codex_config::ThreadConfigLoadErrorCode::RequestFailed
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shutdown_completes_promptly_without_retained_managers() {
|
||||
let client = start_test_client(SessionSource::Cli).await;
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(crate) struct ConfigManager {
|
||||
loader_overrides: LoaderOverrides,
|
||||
cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
thread_config_loader: Arc<RwLock<Arc<dyn ThreadConfigLoader>>>,
|
||||
host_name: Option<String>,
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ impl ConfigManager {
|
||||
loader_overrides,
|
||||
cloud_requirements: Arc::new(RwLock::new(cloud_requirements)),
|
||||
arg0_paths,
|
||||
thread_config_loader,
|
||||
thread_config_loader: Arc::new(RwLock::new(thread_config_loader)),
|
||||
host_name,
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,24 @@ impl ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace_thread_config_loader(
|
||||
&self,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
) {
|
||||
if let Ok(mut guard) = self.thread_config_loader.write() {
|
||||
*guard = thread_config_loader;
|
||||
} else {
|
||||
warn!("failed to update thread config loader");
|
||||
}
|
||||
}
|
||||
|
||||
fn current_thread_config_loader(&self) -> Arc<dyn ThreadConfigLoader> {
|
||||
self.thread_config_loader
|
||||
.read()
|
||||
.map(|guard| Arc::clone(&*guard))
|
||||
.unwrap_or_else(|_| Arc::new(codex_config::NoopThreadConfigLoader))
|
||||
}
|
||||
|
||||
pub(crate) async fn sync_default_client_residency_requirement(&self) {
|
||||
match self.load_latest_config(/*fallback_cwd*/ None).await {
|
||||
Ok(config) => {
|
||||
@@ -210,7 +228,7 @@ impl ConfigManager {
|
||||
.harness_overrides(typesafe_overrides)
|
||||
.fallback_cwd(fallback_cwd)
|
||||
.cloud_requirements(self.current_cloud_requirements())
|
||||
.thread_config_loader(Arc::clone(&self.thread_config_loader))
|
||||
.thread_config_loader(self.current_thread_config_loader())
|
||||
.host_name(self.host_name.clone())
|
||||
.build()
|
||||
.await?;
|
||||
@@ -230,6 +248,7 @@ impl ConfigManager {
|
||||
&self,
|
||||
cwd: Option<AbsolutePathBuf>,
|
||||
) -> std::io::Result<ConfigLayerStack> {
|
||||
let thread_config_loader = self.current_thread_config_loader();
|
||||
load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&self.codex_home,
|
||||
@@ -237,7 +256,7 @@ impl ConfigManager {
|
||||
&self.current_cli_overrides(),
|
||||
self.loader_overrides.clone(),
|
||||
self.current_cloud_requirements(),
|
||||
self.thread_config_loader.as_ref(),
|
||||
thread_config_loader.as_ref(),
|
||||
self.host_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_config::RemoteThreadConfigLoader;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
@@ -109,6 +110,13 @@ enum LogFormat {
|
||||
|
||||
type StderrLogLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
|
||||
|
||||
fn configured_thread_config_loader(config: &Config) -> Arc<dyn ThreadConfigLoader> {
|
||||
match config.experimental_thread_config_endpoint.as_deref() {
|
||||
Some(endpoint) => Arc::new(RemoteThreadConfigLoader::new(endpoint)),
|
||||
None => Arc::new(NoopThreadConfigLoader),
|
||||
}
|
||||
}
|
||||
|
||||
/// Control-plane messages from the processor/transport side to the outbound router task.
|
||||
///
|
||||
/// `run_main_with_transport` now uses two loops/tasks:
|
||||
@@ -384,14 +392,13 @@ pub async fn run_main_with_transport(
|
||||
)
|
||||
})?;
|
||||
let codex_home = find_codex_home()?;
|
||||
let thread_config_loader: Arc<dyn ThreadConfigLoader> = Arc::new(NoopThreadConfigLoader);
|
||||
let config_manager = ConfigManager::new(
|
||||
codex_home.to_path_buf(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides,
|
||||
Default::default(),
|
||||
arg0_paths.clone(),
|
||||
thread_config_loader.clone(),
|
||||
Arc::new(NoopThreadConfigLoader),
|
||||
);
|
||||
match config_manager
|
||||
.load_latest_config(/*fallback_cwd*/ None)
|
||||
@@ -415,6 +422,9 @@ pub async fn run_main_with_transport(
|
||||
}
|
||||
}
|
||||
|
||||
let discovered_thread_config_loader = configured_thread_config_loader(&config);
|
||||
config_manager
|
||||
.replace_thread_config_loader(Arc::clone(&discovered_thread_config_loader));
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false);
|
||||
config_manager.replace_cloud_requirements_loader(auth_manager, config.chatgpt_base_url);
|
||||
|
||||
@@ -313,6 +313,10 @@ pub struct ConfigToml {
|
||||
/// Experimental / do not use. When set, app-server uses a remote thread
|
||||
/// store at this endpoint instead of the local filesystem/SQLite store.
|
||||
pub experimental_thread_store_endpoint: Option<String>,
|
||||
|
||||
/// Experimental / do not use. When set, app-server fetches thread-scoped
|
||||
/// config from a remote service at this endpoint.
|
||||
pub experimental_thread_config_endpoint: Option<String>,
|
||||
pub projects: Option<HashMap<String, ProjectConfig>>,
|
||||
|
||||
/// Controls the web search tool mode: disabled, cached, or live.
|
||||
|
||||
@@ -2484,6 +2484,10 @@
|
||||
"description": "Experimental / do not use. Replaces the synthesized realtime startup context appended to websocket session instructions. An empty string disables startup context injection entirely.",
|
||||
"type": "string"
|
||||
},
|
||||
"experimental_thread_config_endpoint": {
|
||||
"description": "Experimental / do not use. When set, app-server fetches thread-scoped config from a remote service at this endpoint.",
|
||||
"type": "string"
|
||||
},
|
||||
"experimental_thread_store_endpoint": {
|
||||
"description": "Experimental / do not use. When set, app-server uses a remote thread store at this endpoint instead of the local filesystem/SQLite store.",
|
||||
"type": "string"
|
||||
|
||||
@@ -5237,6 +5237,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_backend_prompt: None,
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
experimental_thread_store_endpoint: None,
|
||||
experimental_thread_config_endpoint: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
guardian_policy_config: None,
|
||||
@@ -5433,6 +5434,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_backend_prompt: None,
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
experimental_thread_store_endpoint: None,
|
||||
experimental_thread_config_endpoint: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
guardian_policy_config: None,
|
||||
@@ -5583,6 +5585,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_backend_prompt: None,
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
experimental_thread_store_endpoint: None,
|
||||
experimental_thread_config_endpoint: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
guardian_policy_config: None,
|
||||
@@ -5718,6 +5721,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_backend_prompt: None,
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
experimental_thread_store_endpoint: None,
|
||||
experimental_thread_config_endpoint: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
guardian_policy_config: None,
|
||||
@@ -7240,6 +7244,35 @@ experimental_realtime_start_instructions = "start instructions from config"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_thread_config_endpoint_loads_from_config_toml() -> std::io::Result<()> {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
experimental_thread_config_endpoint = "http://127.0.0.1:8061"
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed");
|
||||
|
||||
assert_eq!(
|
||||
cfg.experimental_thread_config_endpoint.as_deref(),
|
||||
Some("http://127.0.0.1:8061")
|
||||
);
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.experimental_thread_config_endpoint.as_deref(),
|
||||
Some("http://127.0.0.1:8061")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_realtime_ws_base_url_loads_from_config_toml() -> std::io::Result<()> {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
|
||||
@@ -543,6 +543,10 @@ pub struct Config {
|
||||
/// Experimental / do not use. When set, app-server uses a remote thread
|
||||
/// store at this endpoint instead of the local filesystem/SQLite store.
|
||||
pub experimental_thread_store_endpoint: Option<String>,
|
||||
|
||||
/// Experimental / do not use. When set, app-server fetches thread-scoped
|
||||
/// config from a remote service at this endpoint.
|
||||
pub experimental_thread_config_endpoint: Option<String>,
|
||||
/// When set, restricts ChatGPT login to a specific workspace identifier.
|
||||
pub forced_chatgpt_workspace_id: Option<String>,
|
||||
|
||||
@@ -2419,6 +2423,7 @@ impl Config {
|
||||
experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context,
|
||||
experimental_realtime_start_instructions: cfg.experimental_realtime_start_instructions,
|
||||
experimental_thread_store_endpoint: cfg.experimental_thread_store_endpoint,
|
||||
experimental_thread_config_endpoint: cfg.experimental_thread_config_endpoint,
|
||||
forced_chatgpt_workspace_id,
|
||||
forced_login_method,
|
||||
include_apply_patch_tool: include_apply_patch_tool_flag,
|
||||
|
||||
Reference in New Issue
Block a user