mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
Sync remote plugin config toggles
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user