zeph_tools/executor_delegate.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Boilerplate-reduction macros for [`ToolExecutor`](crate::ToolExecutor) and
5//! [`ErasedToolExecutor`](crate::ErasedToolExecutor) implementors.
6//!
7//! Issue #6019: both traits used to give the six risk-bearing methods
8//! (`requires_confirmation`, `execute_tool_call_confirmed`, the checkpoint trio, and
9//! `is_tool_speculatable` — plus their `_erased` counterparts) permissive default bodies.
10//! Wrapper types that forgot to override one silently inherited a default that could
11//! disable a security check, a checkpoint capability, or a confirmation gate. This
12//! recurred five times across prior PRs. The traits no longer provide those defaults —
13//! every implementor must now supply all six, and the compiler enforces it.
14//!
15//! These four macros exist only to keep that compiler-forced boilerplate short. They do
16//! **not** themselves close the defect class — only the removal of the default bodies
17//! does that. A macro invoked on the wrong type (e.g. `tool_executor_no_inner_defaults!()`
18//! on a wrapper that owns an inner executor) silently reintroduces the exact bug this
19//! issue fixes, because `macro_rules!` cannot check "this type has no delegate field."
20//! Read each macro's own doc comment before using it.
21
22/// Forwards the four mechanical capability methods of
23/// [`ToolExecutor`](crate::ToolExecutor) — the checkpoint trio and `is_tool_speculatable`
24/// — to `self.$inner`.
25///
26/// Use inside `impl ToolExecutor for YourWrapper` where `$inner` is the **field name**
27/// (an identifier, not an expression — macro hygiene forbids capturing `self` at item
28/// position) of a field whose type implements [`ToolExecutor`](crate::ToolExecutor).
29///
30/// The two policy methods, `requires_confirmation` and `execute_tool_call_confirmed`, are
31/// intentionally **not** emitted by this macro — wrappers that gate on confirmation or
32/// checkpoint policy must implement those two explicitly, so the compiler forces every
33/// wrapper author to make a deliberate decision about them instead of inheriting one
34/// silently.
35///
36/// # Examples
37///
38/// ```rust
39/// use zeph_tools::{ToolExecutor, ToolCall, ToolOutput, ToolError};
40///
41/// struct PassThrough<T> {
42/// inner: T,
43/// }
44///
45/// impl<T: ToolExecutor> ToolExecutor for PassThrough<T> {
46/// async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
47/// self.inner.execute(response).await
48/// }
49///
50/// fn requires_confirmation(&self, call: &ToolCall) -> bool {
51/// self.inner.requires_confirmation(call)
52/// }
53///
54/// async fn execute_tool_call_confirmed(
55/// &self,
56/// call: &ToolCall,
57/// ) -> Result<Option<ToolOutput>, ToolError> {
58/// self.inner.execute_tool_call_confirmed(call).await
59/// }
60///
61/// zeph_tools::tool_executor_forward!(inner);
62/// }
63/// ```
64#[macro_export]
65macro_rules! tool_executor_forward {
66 ($inner:ident) => {
67 fn checkpoint_undo(&self, n: usize) -> $crate::CheckpointActionResult {
68 $crate::ToolExecutor::checkpoint_undo(&self.$inner, n)
69 }
70
71 fn checkpoint_redo(&self) -> $crate::CheckpointActionResult {
72 $crate::ToolExecutor::checkpoint_redo(&self.$inner)
73 }
74
75 fn checkpoint_list(&self) -> $crate::CheckpointListResult {
76 $crate::ToolExecutor::checkpoint_list(&self.$inner)
77 }
78
79 fn is_tool_speculatable(&self, tool_id: &str) -> bool {
80 $crate::ToolExecutor::is_tool_speculatable(&self.$inner, tool_id)
81 }
82 };
83}
84
85/// Emits the six risk-bearing [`ToolExecutor`](crate::ToolExecutor) methods with the same
86/// trivial bodies the trait's removed defaults used to provide: no confirmation required,
87/// confirmed execution falls back to `execute_tool_call`, checkpoints unsupported, not
88/// speculatable.
89///
90/// # Use ONLY on leaf executors that own no wrapped executor
91///
92/// Invoking this macro on a type that wraps another [`ToolExecutor`](crate::ToolExecutor)
93/// (i.e. has an `inner`/delegate field) silently disables forwarding for all six methods
94/// and **reintroduces issue #6019**. Wrappers must use [`tool_executor_forward!`] for the
95/// mechanical four and hand-write `requires_confirmation` /
96/// `execute_tool_call_confirmed`. `macro_rules!` has no way to enforce this at compile
97/// time — review carefully.
98///
99/// # Examples
100///
101/// ```rust
102/// use zeph_tools::{ToolExecutor, ToolOutput, ToolError};
103///
104/// struct EchoExecutor;
105///
106/// impl ToolExecutor for EchoExecutor {
107/// async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
108/// Ok(None)
109/// }
110///
111/// zeph_tools::tool_executor_no_inner_defaults!();
112/// }
113/// ```
114#[macro_export]
115macro_rules! tool_executor_no_inner_defaults {
116 () => {
117 fn requires_confirmation(&self, _call: &$crate::ToolCall) -> bool {
118 false
119 }
120
121 fn execute_tool_call_confirmed(
122 &self,
123 call: &$crate::ToolCall,
124 ) -> impl ::std::future::Future<
125 Output = ::std::result::Result<Option<$crate::ToolOutput>, $crate::ToolError>,
126 > + Send {
127 $crate::ToolExecutor::execute_tool_call(self, call)
128 }
129
130 fn checkpoint_undo(&self, _n: usize) -> $crate::CheckpointActionResult {
131 $crate::CheckpointActionResult::unsupported()
132 }
133
134 fn checkpoint_redo(&self) -> $crate::CheckpointActionResult {
135 $crate::CheckpointActionResult::unsupported()
136 }
137
138 fn checkpoint_list(&self) -> $crate::CheckpointListResult {
139 $crate::CheckpointListResult::default()
140 }
141
142 fn is_tool_speculatable(&self, _tool_id: &str) -> bool {
143 false
144 }
145 };
146}
147
148/// Erased-trait counterpart of [`tool_executor_forward!`]: forwards the checkpoint trio
149/// and `is_tool_speculatable_erased` to `self.$inner`.
150///
151/// Use inside `impl ErasedToolExecutor for YourWrapper` where `$inner` is the **field
152/// name** (an identifier, not an expression — see [`tool_executor_forward!`] for why) of
153/// a field whose type implements [`ErasedToolExecutor`](crate::ErasedToolExecutor). As
154/// with the static-side macro, the policy methods `requires_confirmation_erased` and
155/// `execute_tool_call_confirmed_erased` are not emitted and must be hand-written.
156#[macro_export]
157macro_rules! erased_tool_executor_forward {
158 ($inner:ident) => {
159 fn checkpoint_undo_erased(&self, n: usize) -> $crate::CheckpointActionResult {
160 $crate::ErasedToolExecutor::checkpoint_undo_erased(&*self.$inner, n)
161 }
162
163 fn checkpoint_redo_erased(&self) -> $crate::CheckpointActionResult {
164 $crate::ErasedToolExecutor::checkpoint_redo_erased(&*self.$inner)
165 }
166
167 fn checkpoint_list_erased(&self) -> $crate::CheckpointListResult {
168 $crate::ErasedToolExecutor::checkpoint_list_erased(&*self.$inner)
169 }
170
171 fn is_tool_speculatable_erased(&self, tool_id: &str) -> bool {
172 $crate::ErasedToolExecutor::is_tool_speculatable_erased(&*self.$inner, tool_id)
173 }
174 };
175}
176
177/// Erased-trait counterpart of [`tool_executor_no_inner_defaults!`]: emits the six
178/// risk-bearing [`ErasedToolExecutor`](crate::ErasedToolExecutor) methods with the same
179/// trivial bodies the trait's removed defaults used to provide.
180///
181/// # Use ONLY on leaf executors that own no wrapped executor
182///
183/// Invoking this macro on a type that wraps another
184/// [`ErasedToolExecutor`](crate::ErasedToolExecutor) silently disables forwarding for all
185/// six methods and **reintroduces issue #6019**. Wrappers must use
186/// [`erased_tool_executor_forward!`] for the mechanical four and hand-write
187/// `requires_confirmation_erased` / `execute_tool_call_confirmed_erased`.
188#[macro_export]
189macro_rules! erased_tool_executor_no_inner_defaults {
190 () => {
191 fn requires_confirmation_erased(&self, _call: &$crate::ToolCall) -> bool {
192 true
193 }
194
195 fn execute_tool_call_confirmed_erased<'a>(
196 &'a self,
197 call: &'a $crate::ToolCall,
198 ) -> ::std::pin::Pin<
199 Box<
200 dyn ::std::future::Future<
201 Output = ::std::result::Result<
202 Option<$crate::ToolOutput>,
203 $crate::ToolError,
204 >,
205 > + Send
206 + 'a,
207 >,
208 > {
209 $crate::ErasedToolExecutor::execute_tool_call_erased(self, call)
210 }
211
212 fn checkpoint_undo_erased(&self, _n: usize) -> $crate::CheckpointActionResult {
213 $crate::CheckpointActionResult::unsupported()
214 }
215
216 fn checkpoint_redo_erased(&self) -> $crate::CheckpointActionResult {
217 $crate::CheckpointActionResult::unsupported()
218 }
219
220 fn checkpoint_list_erased(&self) -> $crate::CheckpointListResult {
221 $crate::CheckpointListResult::default()
222 }
223
224 fn is_tool_speculatable_erased(&self, _tool_id: &str) -> bool {
225 false
226 }
227 };
228}