mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add hooks/list app-server RPC (#19778)
## Why We need a way to list the available hooks to expose via the TUI and App so users can view and manage their hooks ## What - Adds `hooks/list` for one or more `cwd` values that returns discovered hook metadata ## Stack 1. openai/codex#19705 2. This PR - openai/codex#19778 3. openai/codex#19840 4. openai/codex#19882 ## Review Notes The generated schema files account for most of the raw diff, these files have the core change: - `hooks/src/engine/discovery.rs` builds the inventory entries during hook discovery while leaving runtime handlers focused on execution. - `app-server/src/codex_message_processor.rs` wires `hooks/list` into the app-server flow for each requested `cwd`. - `app-server-protocol/src/protocol/v2.rs` defines the new v2 request/response payloads exposed on the wire. ### Core Changes `core/src/plugins/manager.rs` adds `plugins_for_layer_stack(...)` so `skills/list` and `hooks/list`can resolve plugin state for each requested `cwd` --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
6eab7519b4
commit
8774229a89
@@ -373,6 +373,7 @@ pub struct PluginsManager {
|
||||
#[derive(Clone)]
|
||||
struct CachedPluginLoadOutcome {
|
||||
config_version: String,
|
||||
plugin_hooks_enabled: bool,
|
||||
outcome: PluginLoadOutcome,
|
||||
}
|
||||
|
||||
@@ -443,9 +444,12 @@ impl PluginsManager {
|
||||
return PluginLoadOutcome::default();
|
||||
}
|
||||
|
||||
let plugin_hooks_enabled = config.features.enabled(Feature::PluginHooks);
|
||||
let config_version = version_for_toml(&config.config_layer_stack.effective_config());
|
||||
|
||||
if !force_reload && let Some(outcome) = self.cached_enabled_outcome(&config_version) {
|
||||
if !force_reload
|
||||
&& let Some(outcome) =
|
||||
self.cached_enabled_outcome(&config_version, plugin_hooks_enabled)
|
||||
{
|
||||
return outcome;
|
||||
}
|
||||
|
||||
@@ -454,6 +458,7 @@ impl PluginsManager {
|
||||
self.remote_installed_plugin_configs(config),
|
||||
&self.store,
|
||||
self.restriction_product,
|
||||
plugin_hooks_enabled,
|
||||
)
|
||||
.await;
|
||||
log_plugin_load_errors(&outcome);
|
||||
@@ -463,6 +468,7 @@ impl PluginsManager {
|
||||
};
|
||||
*cache = Some(CachedPluginLoadOutcome {
|
||||
config_version,
|
||||
plugin_hooks_enabled,
|
||||
outcome: outcome.clone(),
|
||||
});
|
||||
outcome
|
||||
@@ -485,35 +491,61 @@ impl PluginsManager {
|
||||
*cached_enabled_outcome = None;
|
||||
}
|
||||
|
||||
/// Resolve plugin skill roots for a config layer stack without touching the plugins cache.
|
||||
pub async fn effective_skill_roots_for_layer_stack(
|
||||
/// Load plugins for a config layer stack without touching the plugins cache.
|
||||
pub async fn plugins_for_layer_stack(
|
||||
&self,
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
config: &Config,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
plugin_hooks_feature_enabled: bool,
|
||||
) -> PluginLoadOutcome {
|
||||
if !config.features.enabled(Feature::Plugins) {
|
||||
return Vec::new();
|
||||
return PluginLoadOutcome::default();
|
||||
}
|
||||
load_plugins_from_layer_stack(
|
||||
config_layer_stack,
|
||||
self.remote_installed_plugin_configs(config),
|
||||
&self.store,
|
||||
self.restriction_product,
|
||||
plugin_hooks_feature_enabled,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Resolve plugin skill roots for a config layer stack without touching the plugins cache.
|
||||
pub async fn effective_skill_roots_for_layer_stack(
|
||||
&self,
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
config: &Config,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
self.plugins_for_layer_stack(
|
||||
config_layer_stack,
|
||||
config,
|
||||
config.features.enabled(Feature::PluginHooks),
|
||||
)
|
||||
.await
|
||||
.effective_skill_roots()
|
||||
}
|
||||
|
||||
fn cached_enabled_outcome(&self, config_version: &str) -> Option<PluginLoadOutcome> {
|
||||
fn cached_enabled_outcome(
|
||||
&self,
|
||||
config_version: &str,
|
||||
plugin_hooks_enabled: bool,
|
||||
) -> Option<PluginLoadOutcome> {
|
||||
match self.cached_enabled_outcome.read() {
|
||||
Ok(cache) => cache
|
||||
.as_ref()
|
||||
.filter(|cached| cached.config_version == config_version)
|
||||
.filter(|cached| {
|
||||
cached.config_version == config_version
|
||||
&& cached.plugin_hooks_enabled == plugin_hooks_enabled
|
||||
})
|
||||
.map(|cached| cached.outcome.clone()),
|
||||
Err(err) => err
|
||||
.into_inner()
|
||||
.as_ref()
|
||||
.filter(|cached| cached.config_version == config_version)
|
||||
.filter(|cached| {
|
||||
cached.config_version == config_version
|
||||
&& cached.plugin_hooks_enabled == plugin_hooks_enabled
|
||||
})
|
||||
.map(|cached| cached.outcome.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,18 @@ fn run_git(repo: &Path, args: &[&str]) {
|
||||
}
|
||||
|
||||
fn plugin_config_toml(enabled: bool, plugins_feature_enabled: bool) -> String {
|
||||
plugin_config_toml_with_plugin_hooks(
|
||||
enabled,
|
||||
plugins_feature_enabled,
|
||||
/*plugin_hooks_feature_enabled*/ false,
|
||||
)
|
||||
}
|
||||
|
||||
fn plugin_config_toml_with_plugin_hooks(
|
||||
enabled: bool,
|
||||
plugins_feature_enabled: bool,
|
||||
plugin_hooks_feature_enabled: bool,
|
||||
) -> String {
|
||||
let mut root = toml::map::Map::new();
|
||||
|
||||
let mut features = toml::map::Map::new();
|
||||
@@ -104,6 +116,10 @@ fn plugin_config_toml(enabled: bool, plugins_feature_enabled: bool) -> String {
|
||||
"plugins".to_string(),
|
||||
Value::Boolean(plugins_feature_enabled),
|
||||
);
|
||||
features.insert(
|
||||
"plugin_hooks".to_string(),
|
||||
Value::Boolean(plugin_hooks_feature_enabled),
|
||||
);
|
||||
root.insert("features".to_string(), Value::Table(features));
|
||||
|
||||
let mut plugin = toml::map::Map::new();
|
||||
@@ -1067,6 +1083,61 @@ async fn load_plugins_returns_empty_when_feature_disabled() {
|
||||
assert_eq!(outcome, PluginLoadOutcome::default());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_for_config_reloads_when_plugin_hooks_enablement_changes() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let plugin_root = codex_home
|
||||
.path()
|
||||
.join("plugins/cache")
|
||||
.join("test/sample/local");
|
||||
|
||||
write_file(
|
||||
&plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{"name":"sample"}"#,
|
||||
);
|
||||
write_file(
|
||||
&plugin_root.join("hooks/hooks.json"),
|
||||
r#"{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"hooks": [{ "type": "command", "command": "echo plugin hook" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
write_file(
|
||||
&codex_home.path().join(CONFIG_TOML_FILE),
|
||||
&plugin_config_toml_with_plugin_hooks(
|
||||
/*enabled*/ true, /*plugins_feature_enabled*/ true,
|
||||
/*plugin_hooks_feature_enabled*/ false,
|
||||
),
|
||||
);
|
||||
let config_without_plugin_hooks = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let without_plugin_hooks = manager
|
||||
.plugins_for_config(&config_without_plugin_hooks)
|
||||
.await;
|
||||
assert!(
|
||||
without_plugin_hooks
|
||||
.effective_plugin_hook_sources()
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
write_file(
|
||||
&codex_home.path().join(CONFIG_TOML_FILE),
|
||||
&plugin_config_toml_with_plugin_hooks(
|
||||
/*enabled*/ true, /*plugins_feature_enabled*/ true,
|
||||
/*plugin_hooks_feature_enabled*/ true,
|
||||
),
|
||||
);
|
||||
let config_with_plugin_hooks = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let with_plugin_hooks = manager.plugins_for_config(&config_with_plugin_hooks).await;
|
||||
assert_eq!(with_plugin_hooks.effective_plugin_hook_sources().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_plugins_rejects_invalid_plugin_keys() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -3435,6 +3506,7 @@ async fn load_plugins_ignores_project_config_files() {
|
||||
std::collections::HashMap::new(),
|
||||
&PluginStore::new(codex_home.path().to_path_buf()),
|
||||
Some(Product::Codex),
|
||||
/*plugin_hooks_enabled*/ false,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user