mirror of
https://github.com/instructkr/claude-code.git
synced 2026-04-04 06:28:50 +03:00
Preserve plugin and hook semantics while finishing the hook-pipeline merge
The merge now keeps plugin lifecycle management, plugin tool permissions, hook abort/progress handling, permission-rule config, and shared slash-command help aligned across runtime and CLI codepaths. Constraint: Merge had to retain both plugin runtime behavior and hook-pipeline permission/abort features Rejected: Drop plugin-aware runtime paths during merge | would regress installed plugin hooks and lifecycle handling Rejected: Prefer hook-pipeline tool permissions over the global tool registry | would lose plugin tool permission mapping Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep runtime hook flow, permission policy wiring, and slash-command surfaces synchronized across crates during future merges Tested: cargo test; cargo fmt --all --check; git diff --check Not-tested: Live networked ANTHROPIC_API_KEY smoke path
This commit is contained in:
@@ -7,8 +7,10 @@ use crate::compact::{
|
||||
compact_session, estimate_session_tokens, CompactionConfig, CompactionResult,
|
||||
};
|
||||
use crate::config::RuntimeFeatureConfig;
|
||||
use crate::hooks::HookRunner;
|
||||
use crate::permissions::{PermissionOutcome, PermissionPolicy, PermissionPrompter};
|
||||
use crate::hooks::{HookAbortSignal, HookProgressReporter, HookRunResult, HookRunner};
|
||||
use crate::permissions::{
|
||||
PermissionContext, PermissionOutcome, PermissionPolicy, PermissionPrompter,
|
||||
};
|
||||
use crate::session::{ContentBlock, ConversationMessage, Session};
|
||||
use crate::usage::{TokenUsage, UsageTracker};
|
||||
|
||||
@@ -112,6 +114,8 @@ pub struct ConversationRuntime<C, T> {
|
||||
plugin_hook_runner: Option<PluginHookRunner>,
|
||||
plugin_registry: Option<PluginRegistry>,
|
||||
plugins_shutdown: bool,
|
||||
hook_abort_signal: HookAbortSignal,
|
||||
hook_progress_reporter: Option<Box<dyn HookProgressReporter>>,
|
||||
}
|
||||
|
||||
impl<C, T> ConversationRuntime<C, T> {
|
||||
@@ -176,6 +180,8 @@ where
|
||||
plugin_hook_runner: None,
|
||||
plugin_registry: None,
|
||||
plugins_shutdown: false,
|
||||
hook_abort_signal: HookAbortSignal::default(),
|
||||
hook_progress_reporter: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +227,92 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_hook_abort_signal(mut self, hook_abort_signal: HookAbortSignal) -> Self {
|
||||
self.hook_abort_signal = hook_abort_signal;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_hook_progress_reporter(
|
||||
mut self,
|
||||
hook_progress_reporter: Box<dyn HookProgressReporter>,
|
||||
) -> Self {
|
||||
self.hook_progress_reporter = Some(hook_progress_reporter);
|
||||
self
|
||||
}
|
||||
|
||||
fn run_pre_tool_use_hook(&mut self, tool_name: &str, input: &str) -> HookRunResult {
|
||||
if let Some(reporter) = self.hook_progress_reporter.as_mut() {
|
||||
self.hook_runner.run_pre_tool_use_with_context(
|
||||
tool_name,
|
||||
input,
|
||||
Some(&self.hook_abort_signal),
|
||||
Some(reporter.as_mut()),
|
||||
)
|
||||
} else {
|
||||
self.hook_runner.run_pre_tool_use_with_context(
|
||||
tool_name,
|
||||
input,
|
||||
Some(&self.hook_abort_signal),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_post_tool_use_hook(
|
||||
&mut self,
|
||||
tool_name: &str,
|
||||
input: &str,
|
||||
output: &str,
|
||||
is_error: bool,
|
||||
) -> HookRunResult {
|
||||
if let Some(reporter) = self.hook_progress_reporter.as_mut() {
|
||||
self.hook_runner.run_post_tool_use_with_context(
|
||||
tool_name,
|
||||
input,
|
||||
output,
|
||||
is_error,
|
||||
Some(&self.hook_abort_signal),
|
||||
Some(reporter.as_mut()),
|
||||
)
|
||||
} else {
|
||||
self.hook_runner.run_post_tool_use_with_context(
|
||||
tool_name,
|
||||
input,
|
||||
output,
|
||||
is_error,
|
||||
Some(&self.hook_abort_signal),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_post_tool_use_failure_hook(
|
||||
&mut self,
|
||||
tool_name: &str,
|
||||
input: &str,
|
||||
output: &str,
|
||||
) -> HookRunResult {
|
||||
if let Some(reporter) = self.hook_progress_reporter.as_mut() {
|
||||
self.hook_runner.run_post_tool_use_failure_with_context(
|
||||
tool_name,
|
||||
input,
|
||||
output,
|
||||
Some(&self.hook_abort_signal),
|
||||
Some(reporter.as_mut()),
|
||||
)
|
||||
} else {
|
||||
self.hook_runner.run_post_tool_use_failure_with_context(
|
||||
tool_name,
|
||||
input,
|
||||
output,
|
||||
Some(&self.hook_abort_signal),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn run_turn(
|
||||
&mut self,
|
||||
@@ -273,94 +365,124 @@ where
|
||||
}
|
||||
|
||||
for (tool_use_id, tool_name, input) in pending_tool_uses {
|
||||
let permission_outcome = if let Some(prompt) = prompter.as_mut() {
|
||||
self.permission_policy
|
||||
.authorize(&tool_name, &input, Some(*prompt))
|
||||
let pre_hook_result = self.run_pre_tool_use_hook(&tool_name, &input);
|
||||
let effective_input = pre_hook_result
|
||||
.updated_input()
|
||||
.map_or_else(|| input.clone(), ToOwned::to_owned);
|
||||
let permission_context = PermissionContext::new(
|
||||
pre_hook_result.permission_override(),
|
||||
pre_hook_result.permission_reason().map(ToOwned::to_owned),
|
||||
);
|
||||
|
||||
let permission_outcome = if pre_hook_result.is_cancelled() {
|
||||
PermissionOutcome::Deny {
|
||||
reason: format_hook_message(
|
||||
pre_hook_result.messages(),
|
||||
&format!("PreToolUse hook cancelled tool `{tool_name}`"),
|
||||
),
|
||||
}
|
||||
} else if pre_hook_result.is_denied() {
|
||||
PermissionOutcome::Deny {
|
||||
reason: format_hook_message(
|
||||
pre_hook_result.messages(),
|
||||
&format!("PreToolUse hook denied tool `{tool_name}`"),
|
||||
),
|
||||
}
|
||||
} else if let Some(prompt) = prompter.as_mut() {
|
||||
self.permission_policy.authorize_with_context(
|
||||
&tool_name,
|
||||
&effective_input,
|
||||
&permission_context,
|
||||
Some(*prompt),
|
||||
)
|
||||
} else {
|
||||
self.permission_policy.authorize(&tool_name, &input, None)
|
||||
self.permission_policy.authorize_with_context(
|
||||
&tool_name,
|
||||
&effective_input,
|
||||
&permission_context,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
let result_message = match permission_outcome {
|
||||
PermissionOutcome::Allow => {
|
||||
let pre_hook_result = self.hook_runner.run_pre_tool_use(&tool_name, &input);
|
||||
if pre_hook_result.is_denied() {
|
||||
let plugin_pre_hook_result =
|
||||
self.run_plugin_pre_tool_use(&tool_name, &effective_input);
|
||||
if plugin_pre_hook_result.is_denied() {
|
||||
let deny_message = format!("PreToolUse hook denied tool `{tool_name}`");
|
||||
let mut messages = pre_hook_result.messages().to_vec();
|
||||
messages.extend(plugin_pre_hook_result.messages().iter().cloned());
|
||||
ConversationMessage::tool_result(
|
||||
tool_use_id,
|
||||
tool_name,
|
||||
format_hook_message(pre_hook_result.messages(), &deny_message),
|
||||
format_hook_message(&messages, &deny_message),
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
let plugin_pre_hook_result =
|
||||
self.run_plugin_pre_tool_use(&tool_name, &input);
|
||||
if plugin_pre_hook_result.is_denied() {
|
||||
let deny_message =
|
||||
format!("PreToolUse hook denied tool `{tool_name}`");
|
||||
let mut messages = pre_hook_result.messages().to_vec();
|
||||
messages.extend(plugin_pre_hook_result.messages().iter().cloned());
|
||||
ConversationMessage::tool_result(
|
||||
tool_use_id,
|
||||
tool_name,
|
||||
format_hook_message(&messages, &deny_message),
|
||||
true,
|
||||
let (mut output, mut is_error) =
|
||||
match self.tool_executor.execute(&tool_name, &effective_input) {
|
||||
Ok(output) => (output, false),
|
||||
Err(error) => (error.to_string(), true),
|
||||
};
|
||||
output = merge_hook_feedback(pre_hook_result.messages(), output, false);
|
||||
output = merge_hook_feedback(
|
||||
plugin_pre_hook_result.messages(),
|
||||
output,
|
||||
false,
|
||||
);
|
||||
|
||||
let hook_output = output.clone();
|
||||
let post_hook_result = if is_error {
|
||||
self.run_post_tool_use_failure_hook(
|
||||
&tool_name,
|
||||
&effective_input,
|
||||
&hook_output,
|
||||
)
|
||||
} else {
|
||||
let (mut output, mut is_error) =
|
||||
match self.tool_executor.execute(&tool_name, &input) {
|
||||
Ok(output) => (output, false),
|
||||
Err(error) => (error.to_string(), true),
|
||||
};
|
||||
output =
|
||||
merge_hook_feedback(pre_hook_result.messages(), output, false);
|
||||
output = merge_hook_feedback(
|
||||
plugin_pre_hook_result.messages(),
|
||||
output,
|
||||
self.run_post_tool_use_hook(
|
||||
&tool_name,
|
||||
&effective_input,
|
||||
&hook_output,
|
||||
false,
|
||||
);
|
||||
|
||||
let hook_output = output.clone();
|
||||
let post_hook_result = self.hook_runner.run_post_tool_use(
|
||||
&tool_name,
|
||||
&input,
|
||||
&hook_output,
|
||||
is_error,
|
||||
);
|
||||
let plugin_post_hook_result = self.run_plugin_post_tool_use(
|
||||
&tool_name,
|
||||
&input,
|
||||
&hook_output,
|
||||
is_error,
|
||||
);
|
||||
if post_hook_result.is_denied() {
|
||||
is_error = true;
|
||||
}
|
||||
if plugin_post_hook_result.is_denied() {
|
||||
is_error = true;
|
||||
}
|
||||
output = merge_hook_feedback(
|
||||
post_hook_result.messages(),
|
||||
output,
|
||||
post_hook_result.is_denied(),
|
||||
);
|
||||
output = merge_hook_feedback(
|
||||
plugin_post_hook_result.messages(),
|
||||
output,
|
||||
plugin_post_hook_result.is_denied(),
|
||||
);
|
||||
|
||||
ConversationMessage::tool_result(
|
||||
tool_use_id,
|
||||
tool_name,
|
||||
output,
|
||||
is_error,
|
||||
)
|
||||
};
|
||||
let plugin_post_hook_result = self.run_plugin_post_tool_use(
|
||||
&tool_name,
|
||||
&effective_input,
|
||||
&hook_output,
|
||||
is_error,
|
||||
);
|
||||
if post_hook_result.is_denied()
|
||||
|| post_hook_result.is_cancelled()
|
||||
|| plugin_post_hook_result.is_denied()
|
||||
{
|
||||
is_error = true;
|
||||
}
|
||||
output = merge_hook_feedback(
|
||||
post_hook_result.messages(),
|
||||
output,
|
||||
post_hook_result.is_denied() || post_hook_result.is_cancelled(),
|
||||
);
|
||||
output = merge_hook_feedback(
|
||||
plugin_post_hook_result.messages(),
|
||||
output,
|
||||
plugin_post_hook_result.is_denied(),
|
||||
);
|
||||
|
||||
ConversationMessage::tool_result(
|
||||
tool_use_id,
|
||||
tool_name,
|
||||
output,
|
||||
is_error,
|
||||
)
|
||||
}
|
||||
}
|
||||
PermissionOutcome::Deny { reason } => {
|
||||
ConversationMessage::tool_result(tool_use_id, tool_name, reason, true)
|
||||
}
|
||||
PermissionOutcome::Deny { reason } => ConversationMessage::tool_result(
|
||||
tool_use_id,
|
||||
tool_name,
|
||||
merge_hook_feedback(pre_hook_result.messages(), reason, true),
|
||||
true,
|
||||
),
|
||||
};
|
||||
self.session.messages.push(result_message.clone());
|
||||
tool_results.push(result_message);
|
||||
@@ -870,6 +992,7 @@ mod tests {
|
||||
RuntimeFeatureConfig::default().with_hooks(RuntimeHookConfig::new(
|
||||
vec![shell_snippet("printf 'blocked by hook'; exit 2")],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)),
|
||||
);
|
||||
|
||||
@@ -936,6 +1059,7 @@ mod tests {
|
||||
RuntimeFeatureConfig::default().with_hooks(RuntimeHookConfig::new(
|
||||
vec![shell_snippet("printf 'pre hook ran'")],
|
||||
vec![shell_snippet("printf 'post hook ran'")],
|
||||
Vec::new(),
|
||||
)),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user