Sync remote plugin config toggles

This commit is contained in:
xli-oai
2026-04-28 19:06:42 -07:00
parent 80fb0704ee
commit d01cf95bba
4 changed files with 498 additions and 3 deletions
@@ -4,6 +4,61 @@ use crate::error_code::invalid_request;
use codex_app_server_protocol::PluginInstallPolicy;
impl CodexMessageProcessor {
pub(crate) async fn sync_remote_plugin_enabled_config_write(
&self,
plugin_id: String,
enabled: bool,
) -> Result<(), JSONRPCErrorError> {
if enabled {
let remote_marketplace_name =
self.remote_marketplace_name_for_plugin(&plugin_id).await?;
self.remote_plugin_install_response(remote_marketplace_name, plugin_id)
.await?;
} else {
self.remote_plugin_uninstall_response(plugin_id).await?;
}
Ok(())
}
async fn remote_marketplace_name_for_plugin(
&self,
plugin_id: &str,
) -> Result<String, JSONRPCErrorError> {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
if !config.features.enabled(Feature::Plugins)
|| !config.features.enabled(Feature::RemotePlugin)
{
return Err(invalid_request("remote plugin enable is not enabled"));
}
if plugin_id.is_empty() || !is_valid_remote_plugin_id(plugin_id) {
return Err(invalid_request(
"invalid remote plugin id: only ASCII letters, digits, `_`, `-`, and `~` are allowed",
));
}
let auth = self.auth_manager.auth().await;
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_marketplaces = codex_core_plugins::remote::fetch_remote_marketplaces(
&remote_plugin_service_config,
auth.as_ref(),
)
.await
.map_err(|err| remote_plugin_catalog_error_to_jsonrpc(err, "find remote plugin"))?;
remote_marketplaces
.into_iter()
.find_map(|marketplace| {
marketplace
.plugins
.iter()
.any(|plugin| plugin.id == plugin_id)
.then_some(marketplace.name)
})
.ok_or_else(|| invalid_request(format!("remote plugin `{plugin_id}` was not found")))
}
pub(super) async fn plugin_list(
&self,
request_id: ConnectionRequestId,
+149 -2
View File
@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::future::Future;
use std::sync::Arc;
@@ -38,8 +39,10 @@ use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientNotification;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigEdit;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::ConfigWriteResponse;
use codex_app_server_protocol::DeviceKeyCreateParams;
use codex_app_server_protocol::DeviceKeyPublicParams;
use codex_app_server_protocol::DeviceKeySignParams;
@@ -84,6 +87,7 @@ use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::W3cTraceContext;
use codex_state::log_db::LogDbLayer;
use futures::FutureExt;
use serde_json::Value as JsonValue;
use tokio::sync::broadcast;
use tokio::sync::watch;
use tokio::time::Duration;
@@ -823,10 +827,12 @@ impl MessageProcessor {
}
ClientRequest::ConfigValueWrite { request_id, params } => {
self.handle_config_value_write(request_id_for_connection(request_id), params)
.boxed()
.await;
}
ClientRequest::ConfigBatchWrite { request_id, params } => {
self.handle_config_batch_write(request_id_for_connection(request_id), params)
.boxed()
.await;
}
ClientRequest::ExperimentalFeatureEnablementSet { request_id, params } => {
@@ -989,7 +995,9 @@ impl MessageProcessor {
request_id: ConnectionRequestId,
params: ConfigValueWriteParams,
) {
let result = self.config_api.write_value(params).await;
let result = self
.write_config_value_with_remote_plugin_sync(params)
.await;
self.handle_config_mutation_result(request_id, result).await
}
@@ -998,10 +1006,122 @@ impl MessageProcessor {
request_id: ConnectionRequestId,
params: ConfigBatchWriteParams,
) {
let result = self.config_api.batch_write(params).await;
let result = self
.batch_write_config_with_remote_plugin_sync(params)
.await;
self.handle_config_mutation_result(request_id, result).await;
}
async fn write_config_value_with_remote_plugin_sync(
&self,
params: ConfigValueWriteParams,
) -> Result<ConfigWriteResponse, JSONRPCErrorError> {
let ConfigValueWriteParams {
key_path,
value,
merge_strategy,
file_path,
expected_version,
} = params;
if let Some((plugin_id, enabled)) = remote_plugin_enabled_config_edit(&key_path, &value) {
let response = self
.config_api
.batch_write(ConfigBatchWriteParams {
edits: Vec::new(),
file_path,
expected_version,
reload_user_config: false,
})
.await?;
self.codex_message_processor
.sync_remote_plugin_enabled_config_write(plugin_id, enabled)
.await?;
return Ok(response);
}
self.config_api
.write_value(ConfigValueWriteParams {
key_path,
value,
merge_strategy,
file_path,
expected_version,
})
.await
}
async fn batch_write_config_with_remote_plugin_sync(
&self,
params: ConfigBatchWriteParams,
) -> Result<ConfigWriteResponse, JSONRPCErrorError> {
let ConfigBatchWriteParams {
edits,
file_path,
expected_version,
reload_user_config,
} = params;
let mut local_edits = Vec::<ConfigEdit>::new();
let mut remote_plugin_toggles = BTreeMap::<String, bool>::new();
for edit in edits {
if let Some((plugin_id, enabled)) =
remote_plugin_enabled_config_edit(&edit.key_path, &edit.value)
{
remote_plugin_toggles.insert(plugin_id, enabled);
} else {
local_edits.push(edit);
}
}
if !remote_plugin_toggles.is_empty() && !local_edits.is_empty() {
return Err(invalid_request(
"remote plugin enablement edits cannot be batched with local config edits",
));
}
if remote_plugin_toggles.is_empty() {
return self
.config_api
.batch_write(ConfigBatchWriteParams {
edits: local_edits,
file_path,
expected_version,
reload_user_config,
})
.await;
}
let response = self
.config_api
.batch_write(ConfigBatchWriteParams {
edits: Vec::new(),
file_path: file_path.clone(),
expected_version,
reload_user_config: false,
})
.await?;
for (plugin_id, enabled) in remote_plugin_toggles {
self.codex_message_processor
.sync_remote_plugin_enabled_config_write(plugin_id, enabled)
.await?;
}
if reload_user_config {
self.config_api
.batch_write(ConfigBatchWriteParams {
edits: Vec::new(),
file_path,
expected_version: None,
reload_user_config: true,
})
.await?;
}
Ok(response)
}
async fn handle_experimental_feature_enablement_set(
&self,
request_id: ConnectionRequestId,
@@ -1284,6 +1404,33 @@ fn migration_items_need_runtime_refresh(items: &[ExternalAgentConfigMigrationIte
})
}
fn remote_plugin_enabled_config_edit(key_path: &str, value: &JsonValue) -> Option<(String, bool)> {
let enabled = value.as_bool()?;
let mut segments = key_path.split('.');
let table = segments.next()?;
let plugin_id = segments.next()?;
let field = segments.next()?;
if table == "plugins"
&& field == "enabled"
&& segments.next().is_none()
&& is_remote_plugin_config_id(plugin_id)
{
return Some((plugin_id.to_string(), enabled));
}
None
}
fn is_remote_plugin_config_id(plugin_id: &str) -> bool {
!plugin_id.is_empty()
&& plugin_id
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '~')
&& (plugin_id.starts_with("plugins~")
|| plugin_id.starts_with("app_")
|| plugin_id.starts_with("asdk_app_")
|| plugin_id.starts_with("connector_"))
}
#[cfg(test)]
mod tracing_tests;