mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Trust hooks from materialized workspace plugins (#32301)
## What changed - Carry newly installed or updated remote plugin metadata through effective-plugin refresh callbacks, including coalesced refreshes. - After a successful refresh, record the current hook hashes for listed workspace plugins associated with the active account while preserving existing hook settings and unrelated state. - Serialize the background trust write with config mutations, and leave hooks untrusted if the write fails or the active account changes. ## Testing - Cover eligible plugin selection and escaped hook config keys. - Verify end-to-end trust for newly materialized plugin hooks, preservation of existing config, and fail-closed behavior when config cannot be written. GitOrigin-RevId: 0b150766415be6fccc117f7856a241e10ad456f2
This commit is contained in:
1 parent
dffe1f02a3
commit
076a110eb8
13 files changed
+815
-139
No files matched your search
@@ -0,0 +1,181 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::ConfigEdit;
|
||||
use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core_plugins::EffectivePluginsChange;
|
||||
use codex_core_plugins::remote::RemotePluginMaterialization;
|
||||
use codex_core_plugins::remote::RemotePluginScope;
|
||||
use codex_core_plugins::remote::RemotePluginShareDiscoverability;
|
||||
use codex_login::AuthManager;
|
||||
use serde_json::json;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::config_manager::ConfigManager;
|
||||
use crate::request_processors::ConfigRequestProcessor;
|
||||
use crate::request_serialization::RequestSerializationAccess;
|
||||
use crate::request_serialization::RequestSerializationQueueKey;
|
||||
use crate::request_serialization::RequestSerializationQueues;
|
||||
|
||||
/// Refresh plugin consumers and trust hooks from newly materialized Workspace + Listed bundles.
|
||||
pub(crate) fn effective_plugins_changed_callback(
|
||||
auth_manager: Arc<AuthManager>,
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
config_manager: ConfigManager,
|
||||
config_processor: ConfigRequestProcessor,
|
||||
request_serialization_queues: RequestSerializationQueues,
|
||||
) -> Arc<dyn Fn(EffectivePluginsChange) + Send + Sync> {
|
||||
Arc::new(move |change| {
|
||||
thread_manager.plugins_manager().clear_cache();
|
||||
thread_manager.skills_service().clear_cache();
|
||||
|
||||
let refresh_thread_manager = Arc::clone(&thread_manager);
|
||||
let refresh_config_manager = config_manager.clone();
|
||||
tokio::spawn(async move {
|
||||
if refresh_thread_manager.list_thread_ids().await.is_empty() {
|
||||
return;
|
||||
}
|
||||
crate::mcp_refresh::queue_best_effort_refresh(
|
||||
&refresh_thread_manager,
|
||||
&refresh_config_manager,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
if change.materialized_remote_plugins.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let trust_auth_manager = Arc::clone(&auth_manager);
|
||||
let trust_thread_manager = Arc::clone(&thread_manager);
|
||||
let trust_config_manager = config_manager.clone();
|
||||
let trust_config_processor = config_processor.clone();
|
||||
let trust_request_serialization_queues = request_serialization_queues.clone();
|
||||
tokio::spawn(async move {
|
||||
trust_request_serialization_queues
|
||||
.enqueue_background(
|
||||
RequestSerializationQueueKey::Global("config"),
|
||||
RequestSerializationAccess::Exclusive,
|
||||
async move {
|
||||
if let Err(err) = trust_materialized_plugin_hooks(
|
||||
change.materialized_remote_plugins,
|
||||
&trust_auth_manager,
|
||||
&trust_thread_manager,
|
||||
&trust_config_manager,
|
||||
&trust_config_processor,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(error = %err, "failed to trust materialized plugin hooks");
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn workspace_listed_plugin_ids(
|
||||
materializations: Vec<RemotePluginMaterialization>,
|
||||
current_account_id: &str,
|
||||
) -> BTreeSet<String> {
|
||||
materializations
|
||||
.into_iter()
|
||||
.filter(|plugin| {
|
||||
plugin.scope == RemotePluginScope::Workspace
|
||||
&& plugin.discoverability == Some(RemotePluginShareDiscoverability::Listed)
|
||||
&& plugin.authenticated_account_id.as_deref() == Some(current_account_id)
|
||||
})
|
||||
.map(|plugin| plugin.plugin_id.as_key())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn hook_trusted_hash_edit(hook_key: &str, current_hash: &str) -> ConfigEdit {
|
||||
let escaped_hook_key = hook_key.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
ConfigEdit {
|
||||
key_path: format!(r#"hooks.state."{escaped_hook_key}".trusted_hash"#),
|
||||
value: json!(current_hash),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
}
|
||||
}
|
||||
|
||||
async fn trust_materialized_plugin_hooks(
|
||||
materializations: Vec<RemotePluginMaterialization>,
|
||||
auth_manager: &AuthManager,
|
||||
thread_manager: &ThreadManager,
|
||||
config_manager: &ConfigManager,
|
||||
config_processor: &ConfigRequestProcessor,
|
||||
) -> Result<(), String> {
|
||||
let Some(current_account_id) = auth_manager
|
||||
.auth_cached()
|
||||
.and_then(|auth| auth.get_account_id())
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let plugin_ids = workspace_listed_plugin_ids(materializations, ¤t_account_id);
|
||||
if plugin_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let config = config_manager
|
||||
.load_latest_config(/*fallback_cwd*/ None)
|
||||
.await
|
||||
.map_err(|err| format!("failed to reload config: {err}"))?;
|
||||
let plugin_outcome = thread_manager
|
||||
.plugins_manager()
|
||||
.plugins_for_config(&config.plugins_config_input())
|
||||
.await;
|
||||
let hooks = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: true,
|
||||
bypass_hook_trust: config.bypass_hook_trust,
|
||||
config_layer_stack: Some(config.config_layer_stack),
|
||||
plugin_hook_sources: plugin_outcome.effective_plugin_hook_sources(),
|
||||
plugin_hook_load_warnings: plugin_outcome.effective_plugin_hook_warnings(),
|
||||
..Default::default()
|
||||
});
|
||||
if !hooks.warnings.is_empty() {
|
||||
warn!(
|
||||
warnings = ?hooks.warnings,
|
||||
"hook discovery reported warnings while trusting materialized plugins"
|
||||
);
|
||||
}
|
||||
let edits = hooks
|
||||
.hooks
|
||||
.into_iter()
|
||||
.filter(|hook| {
|
||||
hook.plugin_id
|
||||
.as_ref()
|
||||
.is_some_and(|plugin_id| plugin_ids.contains(plugin_id))
|
||||
})
|
||||
.map(|hook| hook_trusted_hash_edit(&hook.key, &hook.current_hash))
|
||||
.collect::<Vec<_>>();
|
||||
if edits.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if auth_manager
|
||||
.auth_cached()
|
||||
.and_then(|auth| auth.get_account_id())
|
||||
.as_deref()
|
||||
!= Some(current_account_id.as_str())
|
||||
{
|
||||
warn!("skipping materialized plugin hook trust after account changed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let params = ConfigBatchWriteParams {
|
||||
edits,
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
reload_user_config: true,
|
||||
};
|
||||
config_processor
|
||||
.batch_write(params)
|
||||
.await
|
||||
.map_err(|err| format!("failed to write hook trust: {}", err.message))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "effective_plugin_change_tests.rs"]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user