fix: harden public hook actions
This commit is contained in:
parent
21b6848e21
commit
2f020ed0bb
|
|
@ -2,8 +2,7 @@
|
|||
//!
|
||||
//! Hooks are the **public** orchestration extension point. They receive
|
||||
//! read-only summary information about each event in the Worker
|
||||
//! execution loop and return a control-flow action
|
||||
//! (continue / skip / abort / pause).
|
||||
//! execution loop and return a safe public control-flow action.
|
||||
//!
|
||||
//! Hooks intentionally cannot mutate the Worker's context, history, tool
|
||||
//! call, or tool result. Internal mechanisms that need such access (e.g.
|
||||
|
|
@ -18,7 +17,7 @@ use async_trait::async_trait;
|
|||
use llm_worker::interceptor::{
|
||||
PostToolAction, PreRequestAction, PreToolAction, PromptAction, TurnEndAction,
|
||||
};
|
||||
use llm_worker::tool::ToolOutput;
|
||||
use llm_worker::tool::{ToolOutput, ToolResult};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Hook-facing prompt-submit action.
|
||||
|
|
@ -32,7 +31,7 @@ use serde_json::Value;
|
|||
pub enum HookPromptAction {
|
||||
/// Proceed normally.
|
||||
Continue,
|
||||
/// Cancel with a reason.
|
||||
/// Cancel this submitted prompt with a reason.
|
||||
Cancel(String),
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +44,112 @@ impl From<HookPromptAction> for PromptAction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Hook-facing pre-LLM-request action.
|
||||
///
|
||||
/// Public hooks may observe the request boundary, cancel the run, or yield
|
||||
/// control back to the caller. They cannot return
|
||||
/// `PreRequestAction::ContinueWith(Vec<Item>)`; model-visible request/history
|
||||
/// additions must use durable host-owned paths such as notifications or
|
||||
/// system-item commits.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum HookPreRequestAction {
|
||||
/// Proceed normally.
|
||||
Continue,
|
||||
/// Cancel the run with a reason.
|
||||
Cancel(String),
|
||||
/// Yield control to the caller for host-owned processing/resume.
|
||||
Yield,
|
||||
}
|
||||
|
||||
impl From<HookPreRequestAction> for PreRequestAction {
|
||||
fn from(action: HookPreRequestAction) -> Self {
|
||||
match action {
|
||||
HookPreRequestAction::Continue => PreRequestAction::Continue,
|
||||
HookPreRequestAction::Cancel(reason) => PreRequestAction::Cancel(reason),
|
||||
HookPreRequestAction::Yield => PreRequestAction::Yield,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hook-facing pre-tool-call action.
|
||||
///
|
||||
/// Hooks may continue, skip/pause/abort the call, or deny it with an error
|
||||
/// string that Pod converts into a synthetic tool result for the current
|
||||
/// tool call. Hooks cannot mutate the tool call arguments or construct
|
||||
/// arbitrary `ToolResult` values.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum HookPreToolAction {
|
||||
/// Proceed with tool execution.
|
||||
Continue,
|
||||
/// Skip this tool call without executing it.
|
||||
Skip,
|
||||
/// Deny this tool call and commit a synthetic error result.
|
||||
Deny(String),
|
||||
/// Abort the entire run.
|
||||
Abort(String),
|
||||
/// Pause execution.
|
||||
Pause,
|
||||
}
|
||||
|
||||
impl HookPreToolAction {
|
||||
pub(crate) fn into_worker_action(self, call_id: String) -> PreToolAction {
|
||||
match self {
|
||||
HookPreToolAction::Continue => PreToolAction::Continue,
|
||||
HookPreToolAction::Skip => PreToolAction::Skip,
|
||||
HookPreToolAction::Deny(reason) => {
|
||||
PreToolAction::SyntheticResult(ToolResult::error(call_id, reason))
|
||||
}
|
||||
HookPreToolAction::Abort(reason) => PreToolAction::Abort(reason),
|
||||
HookPreToolAction::Pause => PreToolAction::Pause,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hook-facing post-tool-call action.
|
||||
///
|
||||
/// Post-tool hooks are observational except that they may abort the run. They
|
||||
/// cannot rewrite the tool output; adding an explicit bounded transform would
|
||||
/// require a separate safe public type.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum HookPostToolAction {
|
||||
/// Proceed normally.
|
||||
Continue,
|
||||
/// Abort the entire run.
|
||||
Abort(String),
|
||||
}
|
||||
|
||||
impl From<HookPostToolAction> for PostToolAction {
|
||||
fn from(action: HookPostToolAction) -> Self {
|
||||
match action {
|
||||
HookPostToolAction::Continue => PostToolAction::Continue,
|
||||
HookPostToolAction::Abort(reason) => PostToolAction::Abort(reason),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hook-facing turn-end action.
|
||||
///
|
||||
/// Turn-end hooks may observe a completed turn and optionally pause further
|
||||
/// execution. They cannot return
|
||||
/// `TurnEndAction::ContinueWithMessages(Vec<Item>)`; public hooks must not
|
||||
/// append arbitrary model-visible messages at turn boundaries.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum HookTurnEndAction {
|
||||
/// Finish the turn normally.
|
||||
Finish,
|
||||
/// Pause execution.
|
||||
Pause,
|
||||
}
|
||||
|
||||
impl From<HookTurnEndAction> for TurnEndAction {
|
||||
fn from(action: HookTurnEndAction) -> Self {
|
||||
match action {
|
||||
HookTurnEndAction::Finish => TurnEndAction::Finish,
|
||||
HookTurnEndAction::Pause => TurnEndAction::Pause,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook input summary types (read-only)
|
||||
// =============================================================================
|
||||
|
|
@ -121,8 +226,8 @@ pub struct AbortInfo {
|
|||
|
||||
/// Marker trait for hook event kinds.
|
||||
///
|
||||
/// Each event kind specifies its read-only input and the control-flow
|
||||
/// action returned by hooks.
|
||||
/// Each event kind specifies its read-only input and the safe public
|
||||
/// control-flow action returned by hooks.
|
||||
pub trait HookEventKind: Send + Sync + 'static {
|
||||
/// Read-only input passed to the hook.
|
||||
type Input: Send + Sync;
|
||||
|
|
@ -130,17 +235,17 @@ pub trait HookEventKind: Send + Sync + 'static {
|
|||
type Output;
|
||||
}
|
||||
|
||||
/// After receiving user input, before adding to history.
|
||||
/// After receiving user input, before adding to history; may continue or cancel.
|
||||
pub struct OnPromptSubmit;
|
||||
/// Before each LLM request.
|
||||
/// Before each LLM request; may continue, cancel, or yield.
|
||||
pub struct PreLlmRequest;
|
||||
/// Before each tool is executed.
|
||||
/// Before each tool is executed; may continue, skip, deny, abort, or pause.
|
||||
pub struct PreToolCall;
|
||||
/// After each tool completes.
|
||||
/// After each tool completes; observational except it may abort the run.
|
||||
pub struct PostToolCall;
|
||||
/// When a turn ends with no tool calls.
|
||||
/// When a turn ends with no tool calls; observational except it may pause.
|
||||
pub struct OnTurnEnd;
|
||||
/// When execution is interrupted.
|
||||
/// When execution is interrupted; observational only.
|
||||
pub struct OnAbort;
|
||||
|
||||
impl HookEventKind for OnPromptSubmit {
|
||||
|
|
@ -150,22 +255,22 @@ impl HookEventKind for OnPromptSubmit {
|
|||
|
||||
impl HookEventKind for PreLlmRequest {
|
||||
type Input = PreRequestInfo;
|
||||
type Output = PreRequestAction;
|
||||
type Output = HookPreRequestAction;
|
||||
}
|
||||
|
||||
impl HookEventKind for PreToolCall {
|
||||
type Input = ToolCallSummary;
|
||||
type Output = PreToolAction;
|
||||
type Output = HookPreToolAction;
|
||||
}
|
||||
|
||||
impl HookEventKind for PostToolCall {
|
||||
type Input = ToolResultSummary;
|
||||
type Output = PostToolAction;
|
||||
type Output = HookPostToolAction;
|
||||
}
|
||||
|
||||
impl HookEventKind for OnTurnEnd {
|
||||
type Input = TurnEndInfo;
|
||||
type Output = TurnEndAction;
|
||||
type Output = HookTurnEndAction;
|
||||
}
|
||||
|
||||
impl HookEventKind for OnAbort {
|
||||
|
|
@ -180,9 +285,9 @@ impl HookEventKind for OnAbort {
|
|||
/// Async hook for a specific event kind.
|
||||
///
|
||||
/// Hooks receive a shared reference to the event's read-only input
|
||||
/// and return a control-flow action. Multiple hooks can be registered
|
||||
/// per event; they are evaluated in registration order and
|
||||
/// short-circuit on the first non-Continue (or non-Finish) result.
|
||||
/// and return a safe public control-flow action. Multiple hooks can be
|
||||
/// registered per event; they are evaluated in registration order and
|
||||
/// short-circuit on the first non-continue action.
|
||||
#[async_trait]
|
||||
pub trait Hook<E: HookEventKind>: Send + Sync {
|
||||
async fn call(&self, input: &E::Input) -> E::Output;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ use session_store::{SystemItem, SystemReminder};
|
|||
use tools::{TaskEntry, TaskStatus, TaskStore};
|
||||
|
||||
use crate::hook::{
|
||||
AbortInfo, HookPromptAction, HookRegistry, PreRequestInfo, PromptSubmitInfo, ToolCallSummary,
|
||||
AbortInfo, HookPostToolAction, HookPreRequestAction, HookPreToolAction, HookPromptAction,
|
||||
HookRegistry, HookTurnEndAction, PreRequestInfo, PromptSubmitInfo, ToolCallSummary,
|
||||
ToolResultSummary, TurnEndInfo,
|
||||
};
|
||||
use crate::ipc::notify_buffer::{NotifyBuffer, build_system_item};
|
||||
|
|
@ -343,8 +344,8 @@ impl Interceptor for PodInterceptor {
|
|||
};
|
||||
for hook in &self.registry.pre_llm_request {
|
||||
let action = hook.call(&info).await;
|
||||
if !matches!(action, PreRequestAction::Continue) {
|
||||
return action;
|
||||
if !matches!(action, HookPreRequestAction::Continue) {
|
||||
return action.into();
|
||||
}
|
||||
}
|
||||
PreRequestAction::Continue
|
||||
|
|
@ -358,8 +359,8 @@ impl Interceptor for PodInterceptor {
|
|||
};
|
||||
for hook in &self.registry.pre_tool_call {
|
||||
let action = hook.call(&summary).await;
|
||||
if !matches!(action, PreToolAction::Continue) {
|
||||
return action;
|
||||
if !matches!(action, HookPreToolAction::Continue) {
|
||||
return action.into_worker_action(summary.call_id.clone());
|
||||
}
|
||||
}
|
||||
if is_task_management_tool(&info.call.name) {
|
||||
|
|
@ -381,8 +382,8 @@ impl Interceptor for PodInterceptor {
|
|||
};
|
||||
for hook in &self.registry.post_tool_call {
|
||||
let action = hook.call(&summary).await;
|
||||
if !matches!(action, PostToolAction::Continue) {
|
||||
return action;
|
||||
if !matches!(action, HookPostToolAction::Continue) {
|
||||
return action.into();
|
||||
}
|
||||
}
|
||||
PostToolAction::Continue
|
||||
|
|
@ -403,8 +404,8 @@ impl Interceptor for PodInterceptor {
|
|||
};
|
||||
for hook in &self.registry.on_turn_end {
|
||||
let action = hook.call(&info).await;
|
||||
if !matches!(action, TurnEndAction::Finish) {
|
||||
return action;
|
||||
if !matches!(action, HookTurnEndAction::Finish) {
|
||||
return action.into();
|
||||
}
|
||||
}
|
||||
TurnEndAction::Finish
|
||||
|
|
@ -480,16 +481,19 @@ mod tests {
|
|||
use std::sync::atomic::{AtomicBool, AtomicUsize};
|
||||
|
||||
use super::*;
|
||||
use crate::hook::{Hook, HookRegistryBuilder, PreLlmRequest};
|
||||
use crate::hook::{
|
||||
Hook, HookPostToolAction, HookPreRequestAction, HookPreToolAction, HookRegistryBuilder,
|
||||
HookTurnEndAction, OnTurnEnd, PostToolCall, PreLlmRequest, PreToolCall,
|
||||
};
|
||||
use session_store::SystemReminderSource;
|
||||
|
||||
struct CountingHook(Arc<AtomicUsize>);
|
||||
|
||||
#[async_trait]
|
||||
impl Hook<PreLlmRequest> for CountingHook {
|
||||
async fn call(&self, _info: &PreRequestInfo) -> PreRequestAction {
|
||||
async fn call(&self, _info: &PreRequestInfo) -> HookPreRequestAction {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
PreRequestAction::Continue
|
||||
HookPreRequestAction::Continue
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -516,7 +520,7 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
async fn call_pre_tool(interceptor: &PodInterceptor, name: &str) {
|
||||
fn task_tool_call_info(name: &str, input: serde_json::Value) -> ToolCallInfo {
|
||||
let def = tools::task_tools(TaskStore::new())
|
||||
.into_iter()
|
||||
.find(|def| {
|
||||
|
|
@ -525,15 +529,19 @@ mod tests {
|
|||
})
|
||||
.expect("task tool definition");
|
||||
let (meta, tool) = def();
|
||||
let mut info = ToolCallInfo {
|
||||
ToolCallInfo {
|
||||
call: llm_worker::tool::ToolCall {
|
||||
id: "call-id".into(),
|
||||
name: name.into(),
|
||||
input: serde_json::json!({}),
|
||||
input,
|
||||
},
|
||||
meta,
|
||||
tool,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_pre_tool(interceptor: &PodInterceptor, name: &str) {
|
||||
let mut info = task_tool_call_info(name, serde_json::json!({}));
|
||||
let action = interceptor.pre_tool_call(&mut info).await;
|
||||
assert!(matches!(action, PreToolAction::Continue));
|
||||
}
|
||||
|
|
@ -739,12 +747,160 @@ mod tests {
|
|||
|
||||
#[async_trait]
|
||||
impl Hook<PreLlmRequest> for AbortingHook {
|
||||
async fn call(&self, _info: &PreRequestInfo) -> PreRequestAction {
|
||||
async fn call(&self, _info: &PreRequestInfo) -> HookPreRequestAction {
|
||||
self.0.store(true, Ordering::Relaxed);
|
||||
PreRequestAction::Cancel("nope".into())
|
||||
HookPreRequestAction::Cancel("nope".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn public_pre_tool_hook_deny_becomes_synthetic_error_and_short_circuits() {
|
||||
struct DenyToolHook(Arc<AtomicUsize>);
|
||||
struct CountingToolHook(Arc<AtomicUsize>);
|
||||
|
||||
#[async_trait]
|
||||
impl Hook<PreToolCall> for DenyToolHook {
|
||||
async fn call(&self, input: &ToolCallSummary) -> HookPreToolAction {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
assert_eq!(input.call_id, "call-id");
|
||||
assert_eq!(input.tool_name, "TaskList");
|
||||
assert_eq!(input.arguments, serde_json::json!({"scope": "all"}));
|
||||
HookPreToolAction::Deny("blocked by public hook".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Hook<PreToolCall> for CountingToolHook {
|
||||
async fn call(&self, _input: &ToolCallSummary) -> HookPreToolAction {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
HookPreToolAction::Continue
|
||||
}
|
||||
}
|
||||
|
||||
let first_count = Arc::new(AtomicUsize::new(0));
|
||||
let second_count = Arc::new(AtomicUsize::new(0));
|
||||
let mut builder = HookRegistryBuilder::new();
|
||||
builder.add_pre_tool_call(DenyToolHook(first_count.clone()));
|
||||
builder.add_pre_tool_call(CountingToolHook(second_count.clone()));
|
||||
let registry = Arc::new(builder.build());
|
||||
let interceptor = PodInterceptor::new(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
NotifyBuffer::new(),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
TaskStore::new(),
|
||||
Arc::new(TaskReminderState::new()),
|
||||
PromptCatalog::builtins_only().unwrap(),
|
||||
None,
|
||||
);
|
||||
let mut info = task_tool_call_info("TaskList", serde_json::json!({"scope": "all"}));
|
||||
|
||||
let action = interceptor.pre_tool_call(&mut info).await;
|
||||
|
||||
match action {
|
||||
PreToolAction::SyntheticResult(result) => {
|
||||
assert_eq!(result.tool_use_id, "call-id");
|
||||
assert_eq!(result.summary, "blocked by public hook");
|
||||
assert_eq!(result.content, None);
|
||||
assert!(result.is_error);
|
||||
}
|
||||
other => panic!("expected synthetic denial, got {other:?}"),
|
||||
}
|
||||
assert_eq!(first_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(second_count.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn public_post_tool_hooks_observe_output_but_only_abort() {
|
||||
struct AbortAfterToolHook(Arc<AtomicUsize>);
|
||||
|
||||
#[async_trait]
|
||||
impl Hook<PostToolCall> for AbortAfterToolHook {
|
||||
async fn call(&self, input: &ToolResultSummary) -> HookPostToolAction {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
assert_eq!(input.call_id, "call-id");
|
||||
assert_eq!(input.tool_name, "TaskList");
|
||||
assert!(!input.is_error);
|
||||
assert_eq!(input.output.summary, "ok");
|
||||
assert_eq!(input.output.content.as_deref(), Some("full"));
|
||||
HookPostToolAction::Abort("post tool abort".into())
|
||||
}
|
||||
}
|
||||
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let mut builder = HookRegistryBuilder::new();
|
||||
builder.add_post_tool_call(AbortAfterToolHook(count.clone()));
|
||||
let registry = Arc::new(builder.build());
|
||||
let interceptor = PodInterceptor::new(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
NotifyBuffer::new(),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
TaskStore::new(),
|
||||
Arc::new(TaskReminderState::new()),
|
||||
PromptCatalog::builtins_only().unwrap(),
|
||||
None,
|
||||
);
|
||||
let info = task_tool_call_info("TaskList", serde_json::json!({}));
|
||||
let mut result_info = ToolResultInfo {
|
||||
call: info.call,
|
||||
result: llm_worker::tool::ToolResult::from_output(
|
||||
"call-id",
|
||||
ToolOutput {
|
||||
summary: "ok".into(),
|
||||
content: Some("full".into()),
|
||||
},
|
||||
),
|
||||
meta: info.meta,
|
||||
tool: info.tool,
|
||||
};
|
||||
|
||||
let action = interceptor.post_tool_call(&mut result_info).await;
|
||||
|
||||
assert_eq!(action, PostToolAction::Abort("post tool abort".to_string()));
|
||||
assert_eq!(count.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn public_turn_end_hooks_are_observational_or_pause_only() {
|
||||
struct PauseTurnEndHook(Arc<AtomicUsize>);
|
||||
|
||||
#[async_trait]
|
||||
impl Hook<OnTurnEnd> for PauseTurnEndHook {
|
||||
async fn call(&self, input: &TurnEndInfo) -> HookTurnEndAction {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
assert_eq!(input.turn_index, 0);
|
||||
assert_eq!(input.tool_calls_count, 0);
|
||||
assert_eq!(input.final_text_preview, "done");
|
||||
HookTurnEndAction::Pause
|
||||
}
|
||||
}
|
||||
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let mut builder = HookRegistryBuilder::new();
|
||||
builder.add_on_turn_end(PauseTurnEndHook(count.clone()));
|
||||
let registry = Arc::new(builder.build());
|
||||
let interceptor = PodInterceptor::new(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
NotifyBuffer::new(),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
TaskStore::new(),
|
||||
Arc::new(TaskReminderState::new()),
|
||||
PromptCatalog::builtins_only().unwrap(),
|
||||
None,
|
||||
);
|
||||
let history = vec![Item::user_message("hi"), Item::assistant_message("done")];
|
||||
|
||||
let action = interceptor.on_turn_end(&history).await;
|
||||
|
||||
assert!(matches!(action, TurnEndAction::Pause));
|
||||
assert_eq!(count.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pending_history_appends_drains_buffer_into_items() {
|
||||
let registry = Arc::new(HookRegistryBuilder::new().build());
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
use async_trait::async_trait;
|
||||
use llm_worker::interceptor::PreToolAction;
|
||||
use llm_worker::llm_client::client::LlmClient;
|
||||
use llm_worker::tool::ToolResult;
|
||||
use manifest::{ToolPermissionAction, ToolPermissionConfig};
|
||||
use serde_json::Value;
|
||||
use session_store::Store;
|
||||
|
||||
use crate::Pod;
|
||||
use crate::hook::{Hook, PreToolCall, ToolCallSummary};
|
||||
use crate::hook::{Hook, HookPreToolAction, PreToolCall, ToolCallSummary};
|
||||
|
||||
/// Built-in manifest permission policy for `PreToolCall`.
|
||||
///
|
||||
|
|
@ -47,34 +45,28 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
|
||||
#[async_trait]
|
||||
impl Hook<PreToolCall> for PermissionHook {
|
||||
async fn call(&self, input: &ToolCallSummary) -> PreToolAction {
|
||||
async fn call(&self, input: &ToolCallSummary) -> HookPreToolAction {
|
||||
match self.action_for(input) {
|
||||
ToolPermissionAction::Allow => PreToolAction::Continue,
|
||||
ToolPermissionAction::Deny => PreToolAction::SyntheticResult(permission_denied(input)),
|
||||
ToolPermissionAction::Allow => HookPreToolAction::Continue,
|
||||
ToolPermissionAction::Deny => HookPreToolAction::Deny(permission_denied_message(input)),
|
||||
ToolPermissionAction::Ask => {
|
||||
PreToolAction::SyntheticResult(permission_ask_unsupported(input))
|
||||
HookPreToolAction::Deny(permission_ask_unsupported_message(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn permission_denied(input: &ToolCallSummary) -> ToolResult {
|
||||
ToolResult::error(
|
||||
input.call_id.clone(),
|
||||
format!(
|
||||
"permission denied: tool `{}` arguments matched the manifest permission policy",
|
||||
input.tool_name
|
||||
),
|
||||
fn permission_denied_message(input: &ToolCallSummary) -> String {
|
||||
format!(
|
||||
"permission denied: tool `{}` arguments matched the manifest permission policy",
|
||||
input.tool_name
|
||||
)
|
||||
}
|
||||
|
||||
fn permission_ask_unsupported(input: &ToolCallSummary) -> ToolResult {
|
||||
ToolResult::error(
|
||||
input.call_id.clone(),
|
||||
format!(
|
||||
"permission ask unsupported: tool `{}` requires approval, but this runtime has no permission approval protocol; denied fail-closed",
|
||||
input.tool_name
|
||||
),
|
||||
fn permission_ask_unsupported_message(input: &ToolCallSummary) -> String {
|
||||
format!(
|
||||
"permission ask unsupported: tool `{}` requires approval, but this runtime has no permission approval protocol; denied fail-closed",
|
||||
input.tool_name
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -123,6 +115,7 @@ fn wildcard_match(pattern: &str, text: &str) -> bool {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hook::HookPreToolAction;
|
||||
use manifest::ToolPermissionRule;
|
||||
|
||||
fn summary(tool_name: &str, arguments: Value) -> ToolCallSummary {
|
||||
|
|
@ -168,6 +161,45 @@ mod tests {
|
|||
assert_eq!(hook.action_for(&input), ToolPermissionAction::Deny);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deny_and_ask_fail_closed_as_public_deny_actions() {
|
||||
let deny = PermissionHook::new(ToolPermissionConfig {
|
||||
default_action: ToolPermissionAction::Deny,
|
||||
rules: Vec::new(),
|
||||
});
|
||||
let denied = deny
|
||||
.call(&summary(
|
||||
"Bash",
|
||||
serde_json::json!({ "command": "rm -rf target" }),
|
||||
))
|
||||
.await;
|
||||
match denied {
|
||||
HookPreToolAction::Deny(message) => {
|
||||
assert!(message.contains("permission denied"));
|
||||
assert!(message.contains("Bash"));
|
||||
}
|
||||
other => panic!("expected fail-closed deny action, got {other:?}"),
|
||||
}
|
||||
|
||||
let ask = PermissionHook::new(ToolPermissionConfig {
|
||||
default_action: ToolPermissionAction::Ask,
|
||||
rules: Vec::new(),
|
||||
});
|
||||
let asked = ask
|
||||
.call(&summary(
|
||||
"Bash",
|
||||
serde_json::json!({ "command": "git status" }),
|
||||
))
|
||||
.await;
|
||||
match asked {
|
||||
HookPreToolAction::Deny(message) => {
|
||||
assert!(message.contains("permission ask unsupported"));
|
||||
assert!(message.contains("denied fail-closed"));
|
||||
}
|
||||
other => panic!("expected ask fail-closed deny action, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_prefers_known_builtin_argument_fields() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ use manifest::{
|
|||
use crate::compact::state::CompactState;
|
||||
use crate::compact::usage_tracker::UsageTracker;
|
||||
use crate::hook::{
|
||||
Hook, HookRegistryBuilder, OnAbort, OnPromptSubmit, OnTurnEnd, PostToolCall, PreLlmRequest,
|
||||
PreRequestInfo, PreToolCall,
|
||||
Hook, HookPreRequestAction, HookRegistryBuilder, OnAbort, OnPromptSubmit, OnTurnEnd,
|
||||
PostToolCall, PreLlmRequest, PreRequestInfo, PreToolCall,
|
||||
};
|
||||
use crate::ipc::alerter::Alerter;
|
||||
use crate::ipc::interceptor::{PodInterceptor, TaskReminderState};
|
||||
|
|
@ -43,7 +43,6 @@ use crate::runtime::dir;
|
|||
use crate::runtime::pod_registry::{self, ScopeAllocationGuard, ScopeLockError};
|
||||
use crate::workflow::WorkflowResolveError;
|
||||
use async_trait::async_trait;
|
||||
use llm_worker::interceptor::PreRequestAction;
|
||||
use protocol::{
|
||||
AlertLevel, AlertSource, Event, RewindSummary, RewindTarget, RewindTargetId, Segment,
|
||||
};
|
||||
|
|
@ -221,9 +220,9 @@ struct UsageTrackingHook {
|
|||
|
||||
#[async_trait]
|
||||
impl Hook<PreLlmRequest> for UsageTrackingHook {
|
||||
async fn call(&self, info: &PreRequestInfo) -> PreRequestAction {
|
||||
async fn call(&self, info: &PreRequestInfo) -> HookPreRequestAction {
|
||||
self.tracker.note_request(info.item_count);
|
||||
PreRequestAction::Continue
|
||||
HookPreRequestAction::Continue
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user