feat: plugins progress

This commit is contained in:
Yeachan-Heo
2026-04-01 06:25:27 +00:00
parent 8f6d8db958
commit f8d4da3e68
4 changed files with 413 additions and 31 deletions

View File

@@ -72,6 +72,21 @@ impl PluginHooks {
} }
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginLifecycle {
#[serde(rename = "Init", default)]
pub init: Vec<String>,
#[serde(rename = "Shutdown", default)]
pub shutdown: Vec<String>,
}
impl PluginLifecycle {
#[must_use]
pub fn is_empty(&self) -> bool {
self.init.is_empty() && self.shutdown.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginManifest { pub struct PluginManifest {
pub name: String, pub name: String,
@@ -81,6 +96,8 @@ pub struct PluginManifest {
pub default_enabled: bool, pub default_enabled: bool,
#[serde(default)] #[serde(default)]
pub hooks: PluginHooks, pub hooks: PluginHooks,
#[serde(default)]
pub lifecycle: PluginLifecycle,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -112,24 +129,30 @@ pub struct InstalledPluginRegistry {
pub struct BuiltinPlugin { pub struct BuiltinPlugin {
metadata: PluginMetadata, metadata: PluginMetadata,
hooks: PluginHooks, hooks: PluginHooks,
lifecycle: PluginLifecycle,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct BundledPlugin { pub struct BundledPlugin {
metadata: PluginMetadata, metadata: PluginMetadata,
hooks: PluginHooks, hooks: PluginHooks,
lifecycle: PluginLifecycle,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalPlugin { pub struct ExternalPlugin {
metadata: PluginMetadata, metadata: PluginMetadata,
hooks: PluginHooks, hooks: PluginHooks,
lifecycle: PluginLifecycle,
} }
pub trait Plugin { pub trait Plugin {
fn metadata(&self) -> &PluginMetadata; fn metadata(&self) -> &PluginMetadata;
fn hooks(&self) -> &PluginHooks; fn hooks(&self) -> &PluginHooks;
fn lifecycle(&self) -> &PluginLifecycle;
fn validate(&self) -> Result<(), PluginError>; fn validate(&self) -> Result<(), PluginError>;
fn initialize(&self) -> Result<(), PluginError>;
fn shutdown(&self) -> Result<(), PluginError>;
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -148,9 +171,21 @@ impl Plugin for BuiltinPlugin {
&self.hooks &self.hooks
} }
fn lifecycle(&self) -> &PluginLifecycle {
&self.lifecycle
}
fn validate(&self) -> Result<(), PluginError> { fn validate(&self) -> Result<(), PluginError> {
Ok(()) Ok(())
} }
fn initialize(&self) -> Result<(), PluginError> {
Ok(())
}
fn shutdown(&self) -> Result<(), PluginError> {
Ok(())
}
} }
impl Plugin for BundledPlugin { impl Plugin for BundledPlugin {
@@ -162,8 +197,26 @@ impl Plugin for BundledPlugin {
&self.hooks &self.hooks
} }
fn lifecycle(&self) -> &PluginLifecycle {
&self.lifecycle
}
fn validate(&self) -> Result<(), PluginError> { fn validate(&self) -> Result<(), PluginError> {
validate_hook_paths(self.metadata.root.as_deref(), &self.hooks) validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)?;
validate_lifecycle_paths(self.metadata.root.as_deref(), &self.lifecycle)
}
fn initialize(&self) -> Result<(), PluginError> {
run_lifecycle_commands(self.metadata(), self.lifecycle(), "init", &self.lifecycle.init)
}
fn shutdown(&self) -> Result<(), PluginError> {
run_lifecycle_commands(
self.metadata(),
self.lifecycle(),
"shutdown",
&self.lifecycle.shutdown,
)
} }
} }
@@ -176,8 +229,26 @@ impl Plugin for ExternalPlugin {
&self.hooks &self.hooks
} }
fn lifecycle(&self) -> &PluginLifecycle {
&self.lifecycle
}
fn validate(&self) -> Result<(), PluginError> { fn validate(&self) -> Result<(), PluginError> {
validate_hook_paths(self.metadata.root.as_deref(), &self.hooks) validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)?;
validate_lifecycle_paths(self.metadata.root.as_deref(), &self.lifecycle)
}
fn initialize(&self) -> Result<(), PluginError> {
run_lifecycle_commands(self.metadata(), self.lifecycle(), "init", &self.lifecycle.init)
}
fn shutdown(&self) -> Result<(), PluginError> {
run_lifecycle_commands(
self.metadata(),
self.lifecycle(),
"shutdown",
&self.lifecycle.shutdown,
)
} }
} }
@@ -198,6 +269,14 @@ impl Plugin for PluginDefinition {
} }
} }
fn lifecycle(&self) -> &PluginLifecycle {
match self {
Self::Builtin(plugin) => plugin.lifecycle(),
Self::Bundled(plugin) => plugin.lifecycle(),
Self::External(plugin) => plugin.lifecycle(),
}
}
fn validate(&self) -> Result<(), PluginError> { fn validate(&self) -> Result<(), PluginError> {
match self { match self {
Self::Builtin(plugin) => plugin.validate(), Self::Builtin(plugin) => plugin.validate(),
@@ -205,6 +284,22 @@ impl Plugin for PluginDefinition {
Self::External(plugin) => plugin.validate(), Self::External(plugin) => plugin.validate(),
} }
} }
fn initialize(&self) -> Result<(), PluginError> {
match self {
Self::Builtin(plugin) => plugin.initialize(),
Self::Bundled(plugin) => plugin.initialize(),
Self::External(plugin) => plugin.initialize(),
}
}
fn shutdown(&self) -> Result<(), PluginError> {
match self {
Self::Builtin(plugin) => plugin.shutdown(),
Self::Bundled(plugin) => plugin.shutdown(),
Self::External(plugin) => plugin.shutdown(),
}
}
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -241,6 +336,14 @@ impl RegisteredPlugin {
self.definition.validate() self.definition.validate()
} }
pub fn initialize(&self) -> Result<(), PluginError> {
self.definition.initialize()
}
pub fn shutdown(&self) -> Result<(), PluginError> {
self.definition.shutdown()
}
#[must_use] #[must_use]
pub fn summary(&self) -> PluginSummary { pub fn summary(&self) -> PluginSummary {
PluginSummary { PluginSummary {
@@ -299,6 +402,21 @@ impl PluginRegistry {
Ok(acc.merged_with(plugin.hooks())) Ok(acc.merged_with(plugin.hooks()))
}) })
} }
pub fn initialize(&self) -> Result<(), PluginError> {
for plugin in self.plugins.iter().filter(|plugin| plugin.is_enabled()) {
plugin.validate()?;
plugin.initialize()?;
}
Ok(())
}
pub fn shutdown(&self) -> Result<(), PluginError> {
for plugin in self.plugins.iter().rev().filter(|plugin| plugin.is_enabled()) {
plugin.shutdown()?;
}
Ok(())
}
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -687,6 +805,7 @@ pub fn builtin_plugins() -> Vec<PluginDefinition> {
root: None, root: None,
}, },
hooks: PluginHooks::default(), hooks: PluginHooks::default(),
lifecycle: PluginLifecycle::default(),
})] })]
} }
@@ -708,10 +827,23 @@ fn load_plugin_definition(
root: Some(root.to_path_buf()), root: Some(root.to_path_buf()),
}; };
let hooks = resolve_hooks(root, &manifest.hooks); let hooks = resolve_hooks(root, &manifest.hooks);
let lifecycle = resolve_lifecycle(root, &manifest.lifecycle);
Ok(match kind { Ok(match kind {
PluginKind::Builtin => PluginDefinition::Builtin(BuiltinPlugin { metadata, hooks }), PluginKind::Builtin => PluginDefinition::Builtin(BuiltinPlugin {
PluginKind::Bundled => PluginDefinition::Bundled(BundledPlugin { metadata, hooks }), metadata,
PluginKind::External => PluginDefinition::External(ExternalPlugin { metadata, hooks }), hooks,
lifecycle,
}),
PluginKind::Bundled => PluginDefinition::Bundled(BundledPlugin {
metadata,
hooks,
lifecycle,
}),
PluginKind::External => PluginDefinition::External(ExternalPlugin {
metadata,
hooks,
lifecycle,
}),
}) })
} }
@@ -719,6 +851,7 @@ fn load_validated_manifest_from_root(root: &Path) -> Result<PluginManifest, Plug
let manifest = load_manifest_from_root(root)?; let manifest = load_manifest_from_root(root)?;
validate_manifest(&manifest)?; validate_manifest(&manifest)?;
validate_hook_paths(Some(root), &manifest.hooks)?; validate_hook_paths(Some(root), &manifest.hooks)?;
validate_lifecycle_paths(Some(root), &manifest.lifecycle)?;
Ok(manifest) Ok(manifest)
} }
@@ -767,13 +900,47 @@ fn resolve_hooks(root: &Path, hooks: &PluginHooks) -> PluginHooks {
} }
} }
fn resolve_lifecycle(root: &Path, lifecycle: &PluginLifecycle) -> PluginLifecycle {
PluginLifecycle {
init: lifecycle
.init
.iter()
.map(|entry| resolve_hook_entry(root, entry))
.collect(),
shutdown: lifecycle
.shutdown
.iter()
.map(|entry| resolve_hook_entry(root, entry))
.collect(),
}
}
fn validate_hook_paths(root: Option<&Path>, hooks: &PluginHooks) -> Result<(), PluginError> { fn validate_hook_paths(root: Option<&Path>, hooks: &PluginHooks) -> Result<(), PluginError> {
let Some(root) = root else { let Some(root) = root else {
return Ok(()); return Ok(());
}; };
for entry in hooks.pre_tool_use.iter().chain(hooks.post_tool_use.iter()) { for entry in hooks.pre_tool_use.iter().chain(hooks.post_tool_use.iter()) {
validate_command_path(root, entry, "hook")?;
}
Ok(())
}
fn validate_lifecycle_paths(
root: Option<&Path>,
lifecycle: &PluginLifecycle,
) -> Result<(), PluginError> {
let Some(root) = root else {
return Ok(());
};
for entry in lifecycle.init.iter().chain(lifecycle.shutdown.iter()) {
validate_command_path(root, entry, "lifecycle command")?;
}
Ok(())
}
fn validate_command_path(root: &Path, entry: &str, kind: &str) -> Result<(), PluginError> {
if is_literal_command(entry) { if is_literal_command(entry) {
continue; return Ok(());
} }
let path = if Path::new(entry).is_absolute() { let path = if Path::new(entry).is_absolute() {
PathBuf::from(entry) PathBuf::from(entry)
@@ -782,11 +949,10 @@ fn validate_hook_paths(root: Option<&Path>, hooks: &PluginHooks) -> Result<(), P
}; };
if !path.exists() { if !path.exists() {
return Err(PluginError::InvalidManifest(format!( return Err(PluginError::InvalidManifest(format!(
"hook path `{}` does not exist", "{kind} path `{}` does not exist",
path.display() path.display()
))); )));
} }
}
Ok(()) Ok(())
} }
@@ -802,6 +968,48 @@ fn is_literal_command(entry: &str) -> bool {
!entry.starts_with("./") && !entry.starts_with("../") !entry.starts_with("./") && !entry.starts_with("../")
} }
fn run_lifecycle_commands(
metadata: &PluginMetadata,
lifecycle: &PluginLifecycle,
phase: &str,
commands: &[String],
) -> Result<(), PluginError> {
if lifecycle.is_empty() || commands.is_empty() {
return Ok(());
}
for command in commands {
let output = if Path::new(command).exists() {
if cfg!(windows) {
Command::new("cmd").arg("/C").arg(command).output()?
} else {
Command::new("sh").arg(command).output()?
}
} else if cfg!(windows) {
Command::new("cmd").arg("/C").arg(command).output()?
} else {
Command::new("sh").arg("-lc").arg(command).output()?
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(PluginError::CommandFailed(format!(
"plugin `{}` {} failed for `{}`: {}",
metadata.id,
phase,
command,
if stderr.is_empty() {
format!("exit status {}", output.status)
} else {
stderr
}
)));
}
}
Ok(())
}
fn resolve_local_source(source: &str) -> Result<PathBuf, PluginError> { fn resolve_local_source(source: &str) -> Result<PathBuf, PluginError> {
let path = PathBuf::from(source); let path = PathBuf::from(source);
if path.exists() { if path.exists() {
@@ -992,6 +1200,30 @@ mod tests {
.expect("write broken manifest"); .expect("write broken manifest");
} }
fn write_lifecycle_plugin(root: &Path, name: &str, version: &str) -> PathBuf {
fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
fs::create_dir_all(root.join("lifecycle")).expect("lifecycle dir");
let log_path = root.join("lifecycle.log");
fs::write(
root.join("lifecycle").join("init.sh"),
"#!/bin/sh\nprintf 'init\\n' >> \"$(dirname \"$0\")/../lifecycle.log\"\n",
)
.expect("write init hook");
fs::write(
root.join("lifecycle").join("shutdown.sh"),
"#!/bin/sh\nprintf 'shutdown\\n' >> \"$(dirname \"$0\")/../lifecycle.log\"\n",
)
.expect("write shutdown hook");
fs::write(
root.join(MANIFEST_RELATIVE_PATH),
format!(
"{{\n \"name\": \"{name}\",\n \"version\": \"{version}\",\n \"description\": \"lifecycle plugin\",\n \"lifecycle\": {{\n \"Init\": [\"./lifecycle/init.sh\"],\n \"Shutdown\": [\"./lifecycle/shutdown.sh\"]\n }}\n}}"
),
)
.expect("write manifest");
log_path
}
#[test] #[test]
fn validates_manifest_shape() { fn validates_manifest_shape() {
let error = validate_manifest(&PluginManifest { let error = validate_manifest(&PluginManifest {
@@ -1127,4 +1359,26 @@ mod tests {
let _ = fs::remove_dir_all(config_home); let _ = fs::remove_dir_all(config_home);
let _ = fs::remove_dir_all(source_root); let _ = fs::remove_dir_all(source_root);
} }
#[test]
fn plugin_registry_runs_initialize_and_shutdown_for_enabled_plugins() {
let config_home = temp_dir("lifecycle-home");
let source_root = temp_dir("lifecycle-source");
let log_path = write_lifecycle_plugin(&source_root, "lifecycle-demo", "1.0.0");
let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
manager
.install(source_root.to_str().expect("utf8 path"))
.expect("install should succeed");
let registry = manager.plugin_registry().expect("registry should build");
registry.initialize().expect("init should succeed");
registry.shutdown().expect("shutdown should succeed");
let log = fs::read_to_string(&log_path).expect("lifecycle log should exist");
assert_eq!(log, "init\nshutdown\n");
let _ = fs::remove_dir_all(config_home);
let _ = fs::remove_dir_all(source_root);
}
} }

View File

@@ -8,6 +8,7 @@ publish.workspace = true
[dependencies] [dependencies]
sha2 = "0.10" sha2 = "0.10"
glob = "0.3" glob = "0.3"
plugins = { path = "../plugins" }
regex = "1" regex = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View File

@@ -1,6 +1,8 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use plugins::PluginRegistry;
use crate::compact::{ use crate::compact::{
compact_session, estimate_session_tokens, CompactionConfig, CompactionResult, compact_session, estimate_session_tokens, CompactionConfig, CompactionResult,
}; };
@@ -107,6 +109,8 @@ pub struct ConversationRuntime<C, T> {
usage_tracker: UsageTracker, usage_tracker: UsageTracker,
hook_runner: HookRunner, hook_runner: HookRunner,
auto_compaction_input_tokens_threshold: u32, auto_compaction_input_tokens_threshold: u32,
plugin_registry: Option<PluginRegistry>,
plugins_shutdown: bool,
} }
impl<C, T> ConversationRuntime<C, T> impl<C, T> ConversationRuntime<C, T>
@@ -153,9 +157,36 @@ where
usage_tracker, usage_tracker,
hook_runner: HookRunner::from_feature_config(&feature_config), hook_runner: HookRunner::from_feature_config(&feature_config),
auto_compaction_input_tokens_threshold: auto_compaction_threshold_from_env(), auto_compaction_input_tokens_threshold: auto_compaction_threshold_from_env(),
plugin_registry: None,
plugins_shutdown: false,
} }
} }
#[allow(clippy::needless_pass_by_value)]
pub fn new_with_plugins(
session: Session,
api_client: C,
tool_executor: T,
permission_policy: PermissionPolicy,
system_prompt: Vec<String>,
feature_config: RuntimeFeatureConfig,
plugin_registry: PluginRegistry,
) -> Result<Self, RuntimeError> {
plugin_registry
.initialize()
.map_err(|error| RuntimeError::new(format!("plugin initialization failed: {error}")))?;
let mut runtime = Self::new_with_features(
session,
api_client,
tool_executor,
permission_policy,
system_prompt,
feature_config,
);
runtime.plugin_registry = Some(plugin_registry);
Ok(runtime)
}
#[must_use] #[must_use]
pub fn with_max_iterations(mut self, max_iterations: usize) -> Self { pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
self.max_iterations = max_iterations; self.max_iterations = max_iterations;
@@ -304,8 +335,22 @@ where
} }
#[must_use] #[must_use]
pub fn into_session(self) -> Session { pub fn into_session(mut self) -> Session {
self.session let _ = self.shutdown_plugins();
std::mem::take(&mut self.session)
}
pub fn shutdown_plugins(&mut self) -> Result<(), RuntimeError> {
if self.plugins_shutdown {
return Ok(());
}
if let Some(registry) = &self.plugin_registry {
registry
.shutdown()
.map_err(|error| RuntimeError::new(format!("plugin shutdown failed: {error}")))?;
}
self.plugins_shutdown = true;
Ok(())
} }
fn maybe_auto_compact(&mut self) -> Option<AutoCompactionEvent> { fn maybe_auto_compact(&mut self) -> Option<AutoCompactionEvent> {
@@ -334,6 +379,12 @@ where
} }
} }
impl<C, T> Drop for ConversationRuntime<C, T> {
fn drop(&mut self) {
let _ = self.shutdown_plugins();
}
}
#[must_use] #[must_use]
pub fn auto_compaction_threshold_from_env() -> u32 { pub fn auto_compaction_threshold_from_env() -> u32 {
parse_auto_compaction_threshold( parse_auto_compaction_threshold(
@@ -472,7 +523,11 @@ mod tests {
use crate::prompt::{ProjectContext, SystemPromptBuilder}; use crate::prompt::{ProjectContext, SystemPromptBuilder};
use crate::session::{ContentBlock, MessageRole, Session}; use crate::session::{ContentBlock, MessageRole, Session};
use crate::usage::TokenUsage; use crate::usage::TokenUsage;
use plugins::{PluginManager, PluginManagerConfig};
use std::fs;
use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
struct ScriptedApiClient { struct ScriptedApiClient {
call_count: usize, call_count: usize,
@@ -534,6 +589,38 @@ mod tests {
} }
} }
fn temp_dir(label: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time should be after epoch")
.as_nanos();
std::env::temp_dir().join(format!("runtime-plugin-{label}-{nanos}"))
}
fn write_lifecycle_plugin(root: &Path, name: &str) -> PathBuf {
fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
fs::create_dir_all(root.join("lifecycle")).expect("lifecycle dir");
let log_path = root.join("lifecycle.log");
fs::write(
root.join("lifecycle").join("init.sh"),
"#!/bin/sh\nprintf 'init\\n' >> \"$(dirname \"$0\")/../lifecycle.log\"\n",
)
.expect("write init script");
fs::write(
root.join("lifecycle").join("shutdown.sh"),
"#!/bin/sh\nprintf 'shutdown\\n' >> \"$(dirname \"$0\")/../lifecycle.log\"\n",
)
.expect("write shutdown script");
fs::write(
root.join(".claude-plugin").join("plugin.json"),
format!(
"{{\n \"name\": \"{name}\",\n \"version\": \"1.0.0\",\n \"description\": \"runtime lifecycle plugin\",\n \"lifecycle\": {{\n \"Init\": [\"./lifecycle/init.sh\"],\n \"Shutdown\": [\"./lifecycle/shutdown.sh\"]\n }}\n}}"
),
)
.expect("write plugin manifest");
log_path
}
#[test] #[test]
fn runs_user_to_tool_to_result_loop_end_to_end_and_tracks_usage() { fn runs_user_to_tool_to_result_loop_end_to_end_and_tracks_usage() {
let api_client = ScriptedApiClient { call_count: 0 }; let api_client = ScriptedApiClient { call_count: 0 };
@@ -775,6 +862,42 @@ mod tests {
); );
} }
#[test]
fn initializes_and_shuts_down_plugins_with_runtime_lifecycle() {
let config_home = temp_dir("config");
let source_root = temp_dir("source");
let log_path = write_lifecycle_plugin(&source_root, "runtime-lifecycle");
let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
manager
.install(source_root.to_str().expect("utf8 path"))
.expect("install should succeed");
let registry = manager.plugin_registry().expect("registry should load");
{
let runtime = ConversationRuntime::new_with_plugins(
Session::new(),
ScriptedApiClient { call_count: 0 },
StaticToolExecutor::new().register("add", |_input| Ok("4".to_string())),
PermissionPolicy::new(PermissionMode::WorkspaceWrite),
vec!["system".to_string()],
RuntimeFeatureConfig::default(),
registry,
)
.expect("runtime should initialize plugins");
let log = fs::read_to_string(&log_path).expect("init log should exist");
assert_eq!(log, "init\n");
drop(runtime);
}
let log = fs::read_to_string(&log_path).expect("shutdown log should exist");
assert_eq!(log, "init\nshutdown\n");
let _ = fs::remove_dir_all(config_home);
let _ = fs::remove_dir_all(source_root);
}
#[test] #[test]
fn reconstructs_usage_tracker_from_restored_session() { fn reconstructs_usage_tracker_from_restored_session() {
struct SimpleApi; struct SimpleApi;

View File

@@ -23,7 +23,7 @@ use commands::{
}; };
use compat_harness::{extract_manifest, UpstreamPaths}; use compat_harness::{extract_manifest, UpstreamPaths};
use init::initialize_repo; use init::initialize_repo;
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginSummary}; use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginRegistry, PluginSummary};
use render::{MarkdownStreamState, Spinner, TerminalRenderer}; use render::{MarkdownStreamState, Spinner, TerminalRenderer};
use runtime::{ use runtime::{
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt, clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
@@ -2456,20 +2456,22 @@ fn build_system_prompt() -> Result<Vec<String>, Box<dyn std::error::Error>> {
)?) )?)
} }
fn build_runtime_feature_config( fn build_runtime_plugin_state(
) -> Result<runtime::RuntimeFeatureConfig, Box<dyn std::error::Error>> { ) -> Result<(runtime::RuntimeFeatureConfig, PluginRegistry), Box<dyn std::error::Error>> {
let cwd = env::current_dir()?; let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd); let loader = ConfigLoader::default_for(&cwd);
let runtime_config = loader.load()?; let runtime_config = loader.load()?;
let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config); let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
let plugin_hooks = plugin_manager.aggregated_hooks()?; let plugin_registry = plugin_manager.plugin_registry()?;
Ok(runtime_config let plugin_hooks = plugin_registry.aggregated_hooks()?;
let feature_config = runtime_config
.feature_config() .feature_config()
.clone() .clone()
.with_hooks(runtime_config.hooks().merged(&RuntimeHookConfig::new( .with_hooks(runtime_config.hooks().merged(&RuntimeHookConfig::new(
plugin_hooks.pre_tool_use, plugin_hooks.pre_tool_use,
plugin_hooks.post_tool_use, plugin_hooks.post_tool_use,
)))) )));
Ok((feature_config, plugin_registry))
} }
fn build_plugin_manager( fn build_plugin_manager(
@@ -2519,14 +2521,16 @@ fn build_runtime(
permission_mode: PermissionMode, permission_mode: PermissionMode,
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>> ) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
{ {
Ok(ConversationRuntime::new_with_features( let (feature_config, plugin_registry) = build_runtime_plugin_state()?;
Ok(ConversationRuntime::new_with_plugins(
session, session,
AnthropicRuntimeClient::new(model, enable_tools, emit_output, allowed_tools.clone())?, AnthropicRuntimeClient::new(model, enable_tools, emit_output, allowed_tools.clone())?,
CliToolExecutor::new(allowed_tools, emit_output), CliToolExecutor::new(allowed_tools, emit_output),
permission_policy(permission_mode), permission_policy(permission_mode),
system_prompt, system_prompt,
build_runtime_feature_config()?, feature_config,
)) plugin_registry,
)?)
} }
struct CliPermissionPrompter { struct CliPermissionPrompter {